C # va bene con il confronto dei tipi di valore con null


85

Mi sono imbattuto in questo oggi e non ho idea del motivo per cui il compilatore C # non genera un errore.

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

Sono confuso su come x potrebbe mai essere nullo. Soprattutto perché questa assegnazione genera sicuramente un errore del compilatore:

Int32 x = null;

È possibile che x possa diventare nullo, Microsoft ha appena deciso di non inserire questo controllo nel compilatore o è stato perso completamente?

Aggiornamento: dopo aver manipolato il codice per scrivere questo articolo, improvvisamente il compilatore ha avvertito che l'espressione non sarebbe mai stata vera. Adesso sono davvero perso. Ho inserito l'oggetto in una classe e ora l'avvertimento è sparito ma è rimasto con la domanda, un tipo di valore può finire per essere nullo.

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

    public Test ()
    {
        Test test = new Test();
        if (test.ADate == null)
        {
            Console.WriteLine("What the?");
        }
    }
}

9
Puoi anche scrivere if (1 == 2). Non è compito del compilatore eseguire l'analisi del percorso del codice; ecco a cosa servono gli strumenti di analisi statica e gli unit test.
Aaronaught

Perché l'avvertimento è andato via, vedi la mia risposta; e no - non può essere un null.
Marc Gravell

1
Concordato il (1 == 2), mi chiedevo di più sulla situazione (1 == null)
Joshua Belden

Grazie a tutti quelli che hanno risposto. Tutto ha un senso adesso.
Joshua Belden

Riguardo al problema con o senza avviso: Se la struttura in questione è un cosiddetto "tipo semplice", come int, il compilatore genera dei simpatici avvisi. Per i tipi semplici l' ==operatore è definito dalla specifica del linguaggio C #. Per altre strutture (di tipo non semplice), il compilatore si dimentica di emettere un avviso. Vedere avviso del compilatore errato durante il confronto di struct con null per i dettagli. Per le strutture che non sono tipi semplici, l' ==operatore deve essere sovraccaricato da un opeartor ==metodo che è un membro della struttura (altrimenti non ==è consentito).
Jeppe Stig Nielsen

Risposte:


119

Questo è legale perché la risoluzione del sovraccarico dell'operatore ha un unico migliore operatore da scegliere. È presente un operatore == che accetta due int nullable. Il locale int è convertibile in un int nullable. Il valore letterale null è convertibile in un int nullable. Pertanto questo è un utilizzo legale dell'operatore == e risulterà sempre falso.

Allo stesso modo, ti permettiamo anche di dire "if (x == 12.6)", che sarà sempre falso. L'int locale è convertibile in double, il letterale è convertibile in double e ovviamente non saranno mai uguali.



5
@ James: (ritiro il mio precedente commento errato, che ho cancellato.) I tipi di valore definiti dall'utente che hanno un operatore di uguaglianza definito dall'utente definito anche per impostazione predefinita hanno un operatore di uguaglianza definito dall'utente sollevato generato per loro. L'operatore di uguaglianza definito dall'utente sollevato è applicabile per il motivo indicato: tutti i tipi di valore sono convertibili in modo implicito nel loro tipo nullable corrispondente, così come il valore letterale null. È non il caso che un tipo di valore definito dall'utente che manca di un operatore di confronto definita dall'utente è paragonabile al letterale nullo.
Eric Lippert

3
@ James: Certo, puoi implementare il tuo operatore == e operatore! = Che accettano strutture nullable. Se esistono, il compilatore li utilizzerà invece di generarli automaticamente. (E per inciso mi dispiace che l'avvertimento per l'operatore lifted senza senso su operandi non annullabili non produca un avviso; questo è un errore nel compilatore che non siamo riusciti a correggere.)
Eric Lippert

2
Vogliamo il nostro avvertimento! Ce lo meritiamo
Jeppe Stig Nielsen

3
@JamesDunne: Che ne dici di definire una static bool operator == (SomeID a, String b)e contrassegnarla con Obsolete? Se il secondo operando è un letterale non tipizzato null, sarebbe una corrispondenza migliore rispetto a qualsiasi forma che richiede l'uso di operatori lifted, ma se è SomeID?uguale a null, l'operatore lifted vincerebbe.
supercat

17

Non è un errore, poiché c'è una int?conversione ( ); genera un avviso nell'esempio fornito:

Il risultato dell'espressione è sempre "false" poiché un valore di tipo "int" non è mai uguale a "null" di tipo "int?"

Se controlli l'IL, vedrai che rimuove completamente il ramo irraggiungibile - non esiste in una build di rilascio.

Si noti tuttavia che non genera questo avviso per strutture personalizzate con operatori di uguaglianza. Era usato nella 2.0, ma non nel compilatore 3.0. Il codice è ancora rimosso (quindi sa che il codice non è raggiungibile), ma non viene generato alcun avviso:

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
    }
}

Con l'IL (per Main) - nota che tutto tranne MyValue(1)(che potrebbe avere effetti collaterali) è stato rimosso:

.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 
}

questo è fondamentalmente:

private static void Main()
{
    MyValue v = new MyValue(1);
}

1
Qualcuno internamente mi ha segnalato anche questo di recente. Non so perché abbiamo smesso di produrre quell'avvertimento. L'abbiamo inserito come bug.
Eric Lippert


5

Il fatto che un confronto non possa mai essere vero non significa che sia illegale. Tuttavia, no, un tipo di valore può mai essere null.


1
Ma un tipo di valore potrebbe essere uguale a null. Considera int?, quale è lo zucchero sintattico per Nullable<Int32>, quale è un tipo di valore. Una variabile di tipo int?potrebbe certamente essere uguale a null.
Greg

1
@ Greg: Sì, può essere uguale a null, assumendo che "uguale" a cui ti riferisci sia il risultato ==dell'operatore. È importante notare che l'istanza non è effettivamente nulla, però.
Adam Robinson


1

Un tipo di valore non può essere null, sebbene possa essere uguale a null(considera Nullable<>). Nel tuo caso la intvariabile e nullsono implicitamente cast Nullable<Int32>e confrontati.


0

Sospetto che il tuo particolare test sia stato ottimizzato dal compilatore quando genera l'IL poiché il test non sarà mai falso.

Nota a margine: è possibile che un Int32 nullable usi Int32? x invece.


0

Immagino che questo sia perché "==" è uno zucchero di sintassi che in realtà rappresenta la chiamata al System.Object.Equalsmetodo che accetta il System.Objectparametro. Null dalla specifica ECMA è un tipo speciale da cui è ovviamente derivato System.Object.

Ecco perché c'è solo un avvertimento.


Questo non è corretto per due ragioni. Primo, == non ha la stessa semantica di Object.Equals quando uno dei suoi argomenti è un tipo di riferimento. Secondo, null non è un tipo. Vedere la sezione 7.9.6 della specifica se si desidera capire come funziona l'operatore di uguaglianza di riferimento.
Eric Lippert

"Il valore letterale nullo (§9.4.4.6) restituisce il valore nullo, che viene utilizzato per denotare un riferimento che non punta a nessun oggetto o matrice, o l'assenza di un valore. Il tipo nullo ha un unico valore, che è il nullo valore. Quindi un'espressione il cui tipo è il tipo null può valutare solo il valore null. Non c'è modo di scrivere esplicitamente il tipo null e, quindi, non è possibile utilizzarlo in un tipo dichiarato. " - questa è una citazione dell'ECMA. Di cosa stai parlando? Inoltre quale versione di ECMA usi? Non vedo 7.9.6 nel mio.
Vitaly

0

[MODIFICATO: ha trasformato gli avvisi in errori e ha reso gli operatori espliciti su nullable piuttosto che sulla stringa hack.]

Secondo il suggerimento intelligente di @ supercat in un commento sopra, i seguenti overload di operatori consentono di generare un errore sui confronti del tipo di valore personalizzato con null.

Implementando operatori che confrontano con le versioni nullable del tuo tipo, l'uso di null in un confronto corrisponde alla versione nullable dell'operatore, che ti consente di generare l'errore tramite l'attributo Obsolete.

Fino a quando Microsoft non ci restituirà il nostro avviso del compilatore, vado con questa soluzione alternativa, grazie @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;
    }
}

A meno che non mi manchi qualcosa, il tuo approccio farà gridare al compilatore Foo a; Foo? b; ... if (a == b)..., anche se un tale confronto dovrebbe essere perfettamente legittimo. Il motivo per cui ho suggerito lo "string hack" è che consentirebbe il confronto di cui sopra, ma strillerebbe if (a == null). Invece di usare string, si potrebbe sostituire qualsiasi tipo di riferimento diverso da Objecto ValueType; se lo si desidera, si potrebbe definire una classe fittizia con un costruttore privato che non potrebbe mai essere chiamato e autorizzarlo ReferenceThatCanOnlyBeNull.
supercat

Hai assolutamente ragione. Avrei dovuto chiarire che il mio suggerimento rompe l'uso di nullables ... che nel codice base su cui sto lavorando sono comunque considerati peccaminosi (boxe indesiderate, ecc.). ;)
yoyo

0

Penso che la migliore risposta al motivo per cui il compilatore lo accetta sia per le classi generiche. Considera la seguente classe ...

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

Se il compilatore non accetta confronti con i nulltipi di valore, allora essenzialmente interromperà questa classe, avendo un vincolo implicito collegato al suo parametro di tipo (cioè funzionerebbe solo con i tipi non basati sul valore).


0

Il compilatore ti consentirà di confrontare qualsiasi struttura che implementa ==con null. Ti consente anche di confrontare un int con null (riceverai un avviso però).

Ma se disassembli il codice vedrai che il confronto viene risolto quando il codice viene compilato. Quindi, ad esempio, questo codice (dove Fooè implementata una struttura ==):

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

Genera questo 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

Come potete vedere:

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

È tradotto in:

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

Mentre:

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

Viene tradotto in falso:

IL_001e:  ldc.i4.0
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.