C # est d'accord pour comparer les types valeur à null

85

J'ai rencontré cela aujourd'hui et je n'ai aucune idée de la raison pour laquelle le compilateur C # ne lève pas d'erreur.

Int32 x = 1;
if (x == null)
{
    Console.WriteLine("What the?");
}

Je ne sais pas comment x pourrait être nul. D'autant plus que cette affectation lève définitivement une erreur de compilation:

Int32 x = null;

Est-il possible que x devienne nul, Microsoft a-t-il simplement décidé de ne pas mettre cette vérification dans le compilateur ou a-t-elle été complètement manquée?

Mise à jour: Après avoir manipulé le code pour écrire cet article, le compilateur a soudainement lancé un avertissement indiquant que l'expression ne serait jamais vraie. Maintenant je suis vraiment perdu. J'ai mis l'objet dans une classe et maintenant l'avertissement a disparu mais reste avec la question, un type de valeur peut-il finir par être nul.

public class Test
{
    public DateTime ADate = DateTime.Now;

    public Test ()
    {
        Test test = new Test();
        if (test.ADate == null)
        {
            Console.WriteLine("What the?");
        }
    }
}
Joshua Belden
la source
9
Vous pouvez également écrire if (1 == 2). Ce n'est pas le travail du compilateur d'effectuer une analyse de chemin de code; c'est à cela que servent les outils d'analyse statique et les tests unitaires.
Aaronaught
Pour savoir pourquoi l'avertissement a disparu, voyez ma réponse; et non - cela ne peut pas être nul.
Marc Gravell
1
D'accord sur le (1 == 2), je m'interrogeais davantage sur la situation (1 == null)
Joshua Belden
Merci à tous ceux qui ont répondu. Tout a du sens maintenant.
Joshua Belden
Concernant le problème d'avertissement ou pas d'avertissement: Si la structure en question est un soi-disant "type simple", comme int, le compilateur génère de beaux avertissements. Pour les types simples, l' ==opérateur est défini par la spécification du langage C #. Pour les autres structures (pas de type simple), le compilateur oublie d'émettre un avertissement. Consultez l' avertissement du compilateur incorrect lors de la comparaison de struct à null pour plus de détails. Pour les structures qui ne sont pas des types simples, l' ==opérateur doit être surchargé par une opeartor ==méthode qui est membre de la structure (sinon no ==est autorisé).
Jeppe Stig Nielsen

Réponses:

119

Ceci est légal car la résolution de surcharge de l'opérateur a un meilleur opérateur unique à choisir. Il existe un opérateur == qui prend deux entiers Nullable. L'int local est convertible en un int nullable. Le littéral nul est convertible en un entier nullable. Il s'agit donc d'une utilisation légale de l'opérateur ==, et aboutira toujours à false.

De même, nous vous permettons également de dire "si (x == 12.6)", qui sera également toujours faux. L'int local est convertible en double, le littéral est convertible en double, et évidemment ils ne seront jamais égaux.

Eric Lippert
la source
4
Re votre commentaire: connect.microsoft.com/VisualStudio/feedback
Marc Gravell
5
@James: (Je retire mon commentaire erroné précédent, que j'ai supprimé.) Les types de valeur définis par l'utilisateur qui ont un opérateur d'égalité défini par l' utilisateur défini également par défaut ont un opérateur d'égalité défini par l' utilisateur levé généré pour eux. L'opérateur d'égalité défini par l'utilisateur levé est applicable pour la raison que vous indiquez: tous les types valeur sont implicitement convertibles en leur type Nullable correspondant, tout comme le littéral Null. Il n'est pas vrai qu'un type de valeur défini par l'utilisateur qui ne dispose pas d' un opérateur de comparaison défini par l'utilisateur soit comparable au littéral nul.
Eric Lippert
3
@James: Bien sûr, vous pouvez implémenter votre propre opérateur == et opérateur! = Qui prennent des structures Nullable. Si ceux-ci existent, le compilateur les utilisera plutôt que de les générer automatiquement pour vous. (Et d'ailleurs, je regrette que l'avertissement pour l'opérateur levé sans signification sur les opérandes non nullables ne produise pas d'avertissement; c'est une erreur dans le compilateur que nous n'avons pas réussi à corriger.)
Eric Lippert
2
Nous voulons notre avertissement! Nous le méritons.
Jeppe Stig Nielsen
3
@JamesDunne: Qu'en est-il de définir un static bool operator == (SomeID a, String b)et de le taguer Obsolete? Si le deuxième opérande est un littéral non typé null, ce serait une meilleure correspondance que toute forme nécessitant l'utilisation d'opérateurs levés, mais si c'est un SomeID?qui se trouve être égal null, l'opérateur levé gagnerait.
supercat
17

Ce n'est pas une erreur, car il y a une int?conversion ( ); il génère un avertissement dans l'exemple donné:

Le résultat de l'expression est toujours «faux» car une valeur de type «int» n'est jamais égale à «null» de type «int?»

Si vous vérifiez l'IL, vous verrez qu'il supprime complètement la branche inaccessible - elle n'existe pas dans une version de version.

Notez cependant qu'il ne génère pas cet avertissement pour les structures personnalisées avec des opérateurs d'égalité. Il le faisait auparavant dans 2.0, mais pas dans le compilateur 3.0. Le code est toujours supprimé (il sait donc que le code est inaccessible), mais aucun avertissement n'est généré:

using System;

struct MyValue
{
    private readonly int value;
    public MyValue(int value) { this.value = value; }
    public static bool operator ==(MyValue x, MyValue y) {
        return x.value == y.value;
    }
    public static bool operator !=(MyValue x, MyValue y) {
        return x.value != y.value;
    }
}
class Program
{
    static void Main()
    {
        int i = 1;
        MyValue v = new MyValue(1);
        if (i == null) { Console.WriteLine("a"); } // warning
        if (v == null) { Console.WriteLine("a"); } // no warning
    }
}

Avec l'IL (pour Main) - notez que tout sauf le MyValue(1)(qui pourrait avoir des effets secondaires) a été supprimé:

.method private hidebysig static void Main() cil managed
{
    .entrypoint
    .maxstack 2
    .locals init (
        [0] int32 i,
        [1] valuetype MyValue v)
    L_0000: ldc.i4.1 
    L_0001: stloc.0 
    L_0002: ldloca.s v
    L_0004: ldc.i4.1 
    L_0005: call instance void MyValue::.ctor(int32)
    L_000a: ret 
}

c'est essentiellement:

private static void Main()
{
    MyValue v = new MyValue(1);
}
Marc Gravell
la source
1
Quelqu'un m'a récemment signalé cela en interne. Je ne sais pas pourquoi nous avons arrêté de produire cet avertissement. Nous l'avons entré comme un bogue.
Eric Lippert
1
Ici vous allez: connect.microsoft.com/VisualStudio/feedback/…
Marc Gravell
5

Le fait qu'une comparaison ne puisse jamais être vraie ne signifie pas qu'elle est illégale. Néanmoins, non, un type valeur peut jamais être null.

Adam Robinson
la source
1
Mais un type valeur peut être égal à null. Considérez int?, quel est le sucre syntaxique pour Nullable<Int32>, qui est un type valeur. Une variable de type int?pourrait certainement être égale à null.
Greg
1
@Greg: Oui, il peut être égal à null, en supposant que le "égal" auquel vous faites référence est le résultat de l' ==opérateur. Cependant, il est important de noter que l'instance n'est pas réellement nulle.
Adam Robinson
1

Un type valeur ne peut pas être null, bien qu'il puisse être égal à null(considérer Nullable<>). Dans votre cas, la intvariable et nullsont implicitement converties Nullable<Int32>et comparées.

Greg
la source
0

Je soupçonne que votre test particulier est simplement optimisé par le compilateur lorsqu'il génère l'IL puisque le test ne sera jamais faux.

Note latérale: Il est possible qu'un Int32 nullable utilise Int32? x à la place.

GrayWizardx
la source
0

Je suppose que c'est parce que "==" est un sucre de syntaxe qui représente en fait un appel à une System.Object.Equalsméthode qui accepte un System.Objectparamètre. La spécification Null par ECMA est un type spécial qui est bien sûr dérivé de System.Object.

C'est pourquoi il n'y a qu'un avertissement.

Vitaly
la source
Ce n'est pas correct, pour deux raisons. Premièrement, == n'a pas la même sémantique que Object.Equals quand l'un de ses arguments est un type référence. Deuxièmement, null n'est pas un type. Voir la section 7.9.6 de la spécification si vous voulez comprendre comment fonctionne l'opérateur d'égalité de référence.
Eric Lippert
"Le littéral nul (§9.4.4.6) prend la valeur nulle, qui est utilisée pour désigner une référence ne pointant sur aucun objet ou tableau, ou l'absence de valeur. Le type nul a une valeur unique, qui est la valeur nulle value. Par conséquent, une expression dont le type est le type null ne peut être évaluée qu'à la valeur null. Il n'y a aucun moyen d'écrire explicitement le type null et, par conséquent, aucun moyen de l'utiliser dans un type déclaré. " - c'est une citation de l'ECMA. Qu'est-ce que tu racontes? Quelle version d'ECMA utilisez-vous également? Je ne vois pas 7.9.6 dans le mien.
Vitaly
0

[EDITED: a fait des avertissements en erreurs, et a rendu les opérateurs explicites sur nullable plutôt que sur le hack de chaîne.]

Conformément à la suggestion intelligente de @ supercat dans un commentaire ci-dessus, les surcharges d'opérateurs suivantes vous permettent de générer une erreur sur les comparaisons de votre type de valeur personnalisé à null.

En implémentant des opérateurs qui comparent aux versions Nullable de votre type, l'utilisation de Null dans une comparaison correspond à la version Nullable de l'opérateur, ce qui vous permet de générer l'erreur via l'attribut Obsolete.

Jusqu'à ce que Microsoft nous rende notre avertissement de compilateur, je vais utiliser cette solution de contournement, merci @supercat!

public struct Foo
{
    private readonly int x;
    public Foo(int x)
    {
        this.x = x;
    }

    public override string ToString()
    {
        return string.Format("Foo {{x={0}}}", x);
    }

    public override int GetHashCode()
    {
        return x.GetHashCode();
    }

    public override bool Equals(Object obj)
    {
        return x.Equals(obj);
    }

    public static bool operator ==(Foo a, Foo b)
    {
        return a.x == b.x;
    }

    public static bool operator !=(Foo a, Foo b)
    {
        return a.x != b.x;
    }

    [Obsolete("The result of the expression is always 'false' since a value of type 'Foo' is never equal to 'null'", true)]
    public static bool operator ==(Foo a, Foo? b)
    {
        return false;
    }
    [Obsolete("The result of the expression is always 'true' since a value of type 'Foo' is never equal to 'null'", true)]
    public static bool operator !=(Foo a, Foo? b)
    {
        return true;
    }
    [Obsolete("The result of the expression is always 'false' since a value of type 'Foo' is never equal to 'null'", true)]
    public static bool operator ==(Foo? a, Foo b)
    {
        return false;
    }
    [Obsolete("The result of the expression is always 'true' since a value of type 'Foo' is never equal to 'null'", true)]
    public static bool operator !=(Foo? a, Foo b)
    {
        return true;
    }
}
yo-yo
la source
À moins que je ne manque quelque chose, votre approche fera grincer le compilateur Foo a; Foo? b; ... if (a == b)..., même si une telle comparaison devrait être parfaitement légitime. La raison pour laquelle j'ai suggéré le "hack de chaîne" est que cela permettrait la comparaison ci-dessus mais grincer des dents if (a == null). Au lieu d'utiliser string, on pourrait remplacer n'importe quel type de référence autre que Objectou ValueType; si on le souhaite, on pourrait définir une classe factice avec un constructeur privé qui ne pourrait jamais être appelé et lui donner le droit ReferenceThatCanOnlyBeNull.
supercat du
Vous avez tout à fait raison. J'aurais dû préciser que ma suggestion rompt l'utilisation des nullables ... qui, dans la base de code sur laquelle je travaille, sont de toute façon considérés comme un péché (boxe indésirable, etc.). ;)
yoyo
0

Je pense que la meilleure réponse pour savoir pourquoi le compilateur accepte cela concerne les classes génériques. Considérez la classe suivante ...

public class NullTester<T>
{
    public bool IsNull(T value)
    {
        return (value == null);
    }
}

Si le compilateur n'acceptait pas les comparaisons avec les nulltypes valeur, alors il casserait essentiellement cette classe, ayant une contrainte implicite attachée à son paramètre de type (c'est-à-dire qu'il ne fonctionnerait qu'avec des types non basés sur des valeurs).

Lee.J.Baxter
la source
0

Le compilateur vous permettra de comparer toute structure implémentant le == à null. Il vous permet même de comparer un int à null (vous obtiendrez cependant un avertissement).

Mais si vous démontez le code, vous verrez que la comparaison est en cours de résolution lorsque le code est compilé. Donc, par exemple, ce code (où Fooest une struct implémentation ==):

public static void Main()
{
    Console.WriteLine(new Foo() == new Foo());
    Console.WriteLine(new Foo() == null);
    Console.WriteLine(5 == null);
    Console.WriteLine(new Foo() != null);
}

Génère cet IL:

.method public hidebysig static void  Main() cil managed
{
  .entrypoint
  // Code size       45 (0x2d)
  .maxstack  2
  .locals init ([0] valuetype test3.Program/Foo V_0)
  IL_0000:  nop
  IL_0001:  ldloca.s   V_0
  IL_0003:  initobj    test3.Program/Foo
  IL_0009:  ldloc.0
  IL_000a:  ldloca.s   V_0
  IL_000c:  initobj    test3.Program/Foo
  IL_0012:  ldloc.0
  IL_0013:  call       bool test3.Program/Foo::op_Equality(valuetype test3.Program/Foo,
                                                           valuetype test3.Program/Foo)
  IL_0018:  call       void [mscorlib]System.Console::WriteLine(bool)
  IL_001d:  nop
  IL_001e:  ldc.i4.0
  IL_001f:  call       void [mscorlib]System.Console::WriteLine(bool)
  IL_0024:  nop
  IL_0025:  ldc.i4.1
  IL_0026:  call       void [mscorlib]System.Console::WriteLine(bool)
  IL_002b:  nop
  IL_002c:  ret
} // end of method Program::Main

Comme vous pouvez le voir:

Console.WriteLine(new Foo() == new Foo());

Est traduit en:

IL_0013:  call       bool test3.Program/Foo::op_Equality(valuetype test3.Program/Foo,
                                                               valuetype test3.Program/Foo)

Tandis que:

Console.WriteLine(new Foo() == null);

Est traduit en faux:

IL_001e:  ldc.i4.0
codé en dur
la source