C # dobrze, porównując typy wartości z null


85

Natknąłem się na to dzisiaj i nie mam pojęcia, dlaczego kompilator C # nie zgłasza błędu.

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

Nie wiem, jak x może kiedykolwiek być zerowe. Zwłaszcza, że ​​to przypisanie zdecydowanie generuje błąd kompilatora:

Int32 x = null;

Czy to możliwe, że x może stać się zerowe, czy Microsoft po prostu zdecydował nie umieszczać tego sprawdzenia w kompilatorze, czy też całkowicie go pominięto?

Aktualizacja: Po pomieszaniu z kodem, aby napisać ten artykuł, nagle kompilator pojawił się z ostrzeżeniem, że wyrażenie nigdy nie będzie prawdziwe. Teraz jestem naprawdę zagubiony. Umieściłem obiekt w klasie i teraz ostrzeżenie zniknęło, ale zostawiłem pytanie, czy typ wartości może być pusty.

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

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

9
Ty też możesz pisać if (1 == 2). Nie jest zadaniem kompilatora przeprowadzanie analizy ścieżki kodu; do tego służą narzędzia do analizy statycznej i testy jednostkowe.
Aaronaught

Dlaczego ostrzeżenie odeszło, zobacz moją odpowiedź; i nie - to nie może być zerowe.
Marc Gravell

1
Zgoda na (1 == 2), bardziej zastanawiałem się nad sytuacją (1 == null)
Joshua Belden,

Dziękuję wszystkim, którzy odpowiedzieli. Teraz wszystko ma sens.
Joshua Belden

Odnośnie ostrzeżenia lub braku ostrzeżenia: Jeśli dana struktura jest tak zwanym „typem prostym”, na przykład int, kompilator generuje ładne ostrzeżenia. W przypadku typów prostych ==operator jest zdefiniowany w specyfikacji języka C #. W przypadku innych struktur (nie typu prostego) kompilator zapomina o wyświetleniu ostrzeżenia. Aby uzyskać szczegółowe informacje, zobacz nieprawidłowe ostrzeżenie kompilatora podczas porównywania struktury z wartością null . W przypadku struktur, które nie są typami prostymi, ==operator musi być przeciążony opeartor ==metodą, która jest elementem członkowskim struktury (w przeciwnym razie nie ==jest dozwolone).
Jeppe Stig Nielsen

Odpowiedzi:


119

Jest to zgodne z prawem, ponieważ rozwiązanie przeciążenia operatora ma do wyboru jedynego najlepszego operatora. Istnieje operator ==, który przyjmuje dwie liczby całkowite dopuszczające wartość null. Int local można zamienić na int nullable. Literał o wartości null można zamienić na int o wartości null. Dlatego jest to legalne użycie operatora == i zawsze skutkuje fałszem.

Podobnie, pozwalamy ci powiedzieć „if (x == 12,6)”, co również zawsze będzie fałszywe. Int local można zamienić na double, literał można zamienić na double i oczywiście nigdy nie będą równe.



5
@James: (Wycofuję swój wcześniejszy błędny komentarz, który usunąłem). Typy wartości zdefiniowane przez użytkownika, które mają zdefiniowany przez użytkownika operator równości, również domyślnie mają wygenerowany dla nich podniesiony operator równości zdefiniowany przez użytkownika . Podniesiony operator równości zdefiniowany przez użytkownika ma zastosowanie z podanego powodu: wszystkie typy wartości są niejawnie konwertowane na odpowiadający im typ dopuszczający wartość null, podobnie jak literał null. To nie jest tak, że typ wartości zdefiniowanych przez użytkownika, które nie posiada operator porównania zdefiniowany przez użytkownika jest porównywalna zerowej dosłownym.
Eric Lippert

3
@James: Jasne, możesz zaimplementować własny operator == i operator! =, Które przyjmują struktury dopuszczające wartość null. Jeśli takie istnieją, kompilator użyje ich zamiast generować je automatycznie. (I nawiasem mówiąc, żałuję, że ostrzeżenie dla bezsensownego podniesionego operatora na operandach nie dopuszczających wartości null nie generuje ostrzeżenia; to błąd w kompilatorze, którego nie naprawiliśmy.)
Eric Lippert

2
Chcemy naszego ostrzeżenia! Zasługujemy na to.
Jeppe Stig Nielsen

3
@JamesDunne: A co z definiowaniem static bool operator == (SomeID a, String b)i tagowaniem go Obsolete? Jeśli drugi operand jest nietypowym literałem null, byłoby to lepsze dopasowanie niż jakakolwiek forma wymagająca użycia podniesionych operatorów, ale jeśli jest to a, SomeID?co jest równe null, wygrałby operator podniesiony.
supercat

17

To nie jest błąd, ponieważ występuje int?konwersja ( ); generuje ostrzeżenie w podanym przykładzie:

Wynikiem wyrażenia jest zawsze „false”, ponieważ wartość typu „int” nigdy nie jest równa „null” typu „int?”

Jeśli zaznaczysz IL, zobaczysz, że całkowicie usuwa nieosiągalną gałąź - nie istnieje w kompilacji wydania.

Należy jednak pamiętać, że nie generuje tego ostrzeżenia dla niestandardowych struktur z operatorami równości. Kiedyś w 2.0, ale nie w kompilatorze 3.0. Kod jest nadal usuwany (dzięki czemu wie, że kod jest nieosiągalny), ale nie jest generowane żadne ostrzeżenie:

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

Z IL (for Main) - zauważ, że wszystko oprócz MyValue(1)(które może mieć skutki uboczne) zostało usunięte:

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

to jest w zasadzie:

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

1
Ktoś też ostatnio mi to zgłosił wewnętrznie. Nie wiem, dlaczego przestaliśmy publikować to ostrzeżenie. Wpisaliśmy to jako błąd.
Eric Lippert


5

Fakt, że porównanie nigdy nie może być prawdziwe, nie oznacza, że ​​jest nielegalne. Niemniej jednak nie, typ wartości może być kiedykolwiek null.


1
Ale typ wartości może być równa się null. Zastanów się int?, dla czego jest cukier syntaktyczny Nullable<Int32>, który jest typem wartości. Z int?pewnością zmienna typu mogłaby być równa null.
Greg

1
@Greg: Tak, może być równe null, zakładając, że „równy”, do którego się odnosisz, jest wynikiem działania ==operatora. Należy jednak pamiętać, że instancja nie jest w rzeczywistości zerowa.
Adam Robinson


1

Typ wartości nie może być null, chociaż może być równy null(rozważ Nullable<>). W twoim przypadku intzmienne i nullsą niejawnie rzutowane Nullable<Int32>i porównywane.


0

Podejrzewam, że twój konkretny test jest właśnie optymalizowany przez kompilator, kiedy generuje IL, ponieważ test nigdy nie będzie fałszywy.

Uwaga boczna: możliwe jest użycie Int32 z dopuszczalną wartością null? x zamiast tego.


0

Wydaje mi się, że dzieje się tak, ponieważ „==” to cukier składni, który w rzeczywistości reprezentuje wywołanie System.Object.Equalsmetody, która akceptuje System.Objectparametr. Specyfikacja Null według ECMA to specjalny typ, z którego oczywiście wywodzi się System.Object.

Dlatego jest tylko ostrzeżenie.


Nie jest to poprawne z dwóch powodów. Po pierwsze, == nie ma takiej samej semantyki jak Object. Jest równe, gdy jeden z jego argumentów jest typem referencyjnym. Po drugie, null nie jest typem. Zobacz sekcję 7.9.6 specyfikacji, jeśli chcesz zrozumieć, jak działa operator równości odniesienia.
Eric Lippert

„Literał null (§9.4.4.6) zwraca wartość null, która jest używana do oznaczenia odniesienia nie wskazującego na żaden obiekt ani tablicę lub braku wartości. Typ null ma jedną wartość, która jest wartością zerową W związku z tym wyrażenie, którego typ jest typem zerowym, może obliczyć tylko wartość null. Nie ma możliwości jawnego zapisania typu null, a zatem nie ma możliwości użycia go w zadeklarowanym typie. " - to cytat z ECMA. O czym mówisz? Której wersji ECMA używasz? W moim nie widzę 7.9.6.
Witalij

0

[EDYTOWANO: zamieniono ostrzeżenia w błędy i wyraźnie zaznaczono, że operatory mają wartość null, a nie ciąg znaków.]

Zgodnie ze sprytną sugestią @ supercat w powyższym komentarzu, poniższe przeciążenia operatorów pozwalają wygenerować błąd dotyczący porównania niestandardowego typu wartości do null.

Implementując operatory, które porównują się z wersjami dopuszczającymi wartość null danego typu, użycie wartości null w porównaniu jest zgodne z wersją operatora dopuszczającą wartość null, co umożliwia wygenerowanie błędu za pomocą atrybutu Obsolete.

Dopóki Microsoft nie zwróci nam naszego ostrzeżenia kompilatora, będę stosować to obejście, dzięki @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;
    }
}

O ile czegoś nie brakuje, twoje podejście spowoduje, że kompilator zacznie wrzeszczeć Foo a; Foo? b; ... if (a == b)..., nawet jeśli takie porównanie powinno być całkowicie uzasadnione. Powodem, dla którego zasugerowałem "łamanie ciągów" jest to, że pozwoliłoby to na powyższe porównanie, ale wrzeszczało if (a == null). Zamiast używać string, można by zastąpić dowolny typ referencyjny inny niż Objectlub ValueType; w razie potrzeby można zdefiniować atrapę klasy z prywatnym konstruktorem, którego nigdy nie można wywołać i nadać jej uprawnienia ReferenceThatCanOnlyBeNull.
supercat

Masz całkowitą rację. Powinienem był wyjaśnić, że moja sugestia łamie użycie wartości null ... które w bazie kodu, nad którą pracuję, są i tak uważane za grzeszne (niechciane boksowanie itp.). ;)
yoyo

0

Myślę, że najlepszą odpowiedzią na pytanie, dlaczego kompilator akceptuje to jest dla klas ogólnych. Rozważ następującą klasę ...

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

Gdyby kompilator nie akceptował porównań z nulltypami wartości, to zasadniczo przerwałby tę klasę, mając niejawne ograniczenie dołączone do jej parametru typu (tj. Działałby tylko z typami nieopartymi na wartościach).


0

Kompilator pozwoli ci porównać dowolną strukturę implementującą == to null. Pozwala nawet porównać wartość int do null (ale pojawi się ostrzeżenie).

Ale jeśli zdemontujesz kod, zobaczysz, że porównanie jest rozwiązywane, gdy kod jest kompilowany. Na przykład ten kod (gdzie Foojest implementacją struktury ==):

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

Generuje ten 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

Jak widzisz:

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

Jest tłumaczone na:

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

Natomiast:

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

Jest tłumaczone na fałsz:

IL_001e:  ldc.i4.0
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.