C # нормально сравнивает типы значений с null

85

Я столкнулся с этим сегодня и понятия не имею, почему компилятор C # не выдает ошибку.

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

Я не понимаю, как x может быть нулевым. Тем более, что это назначение определенно вызывает ошибку компилятора:

Int32 x = null;

Возможно ли, что x может стать нулевым, Microsoft просто решила не помещать эту проверку в компилятор или она была полностью пропущена?

Обновление: после того, как я возился с кодом для написания этой статьи, компилятор неожиданно выдал предупреждение о том, что выражение никогда не будет истинным. Теперь я действительно потерялся. Я поместил объект в класс, и теперь предупреждение исчезло, но остался вопрос, может ли тип значения оказаться нулевым.

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

    public Test ()
    {
        Test test = new Test();
        if (test.ADate == null)
        {
            Console.WriteLine("What the?");
        }
    }
}
Джошуа Белден
источник
9
Вы тоже можете писать if (1 == 2). Анализ пути кода - не задача компилятора; для этого нужны инструменты статического анализа и модульные тесты.
Aaronaught
Почему предупреждение исчезло, см. Мой ответ; и нет - это не может быть нулем.
Марк Грейвелл
1
Согласился на (1 == 2), меня больше интересовала ситуация (1 == null)
Джошуа Белден
Спасибо всем, кто ответил. Теперь все имеет смысл.
Джошуа Белден,
Относительно предупреждения или отсутствия предупреждения: если рассматриваемая структура является так называемым «простым типом», например int, компилятор генерирует приятные предупреждения. Для простых типов ==оператор определяется спецификацией языка C #. Для других структур (не простого типа) компилятор забывает выдать предупреждение. Дополнительные сведения см. В разделе « Неправильное предупреждение компилятора при сравнении struct с null» . Для структур, которые не являются простыми типами, ==оператор должен быть перегружен opeartor ==методом, который является членом структуры (в противном случае ==разрешено «нет»).
Йеппе Стиг Нильсен

Ответы:

119

Это законно, потому что решение о перегрузке оператора позволяет выбрать лучшего оператора. Существует оператор ==, который принимает два целых числа, допускающих значение NULL. Int local можно преобразовать в обнуляемое int. Литерал null можно преобразовать в int, допускающий значение null. Следовательно, это законное использование оператора == и всегда приводит к ложному результату.

Точно так же мы также разрешаем вам сказать «if (x == 12.6)», что также всегда будет ложным. Int local можно преобразовать в double, литерал можно преобразовать в double, и, очевидно, они никогда не будут равны.

Эрик Липперт
источник
4
Повторите свой комментарий: connect.microsoft.com/VisualStudio/feedback/…
Марк Гравелл
5
@James: (я отозваю свой предыдущий ошибочный комментарий, который я удалил.) Пользовательские типы значений, для которых определен оператор равенства, определенный пользователем, также по умолчанию имеют сгенерированный для них расширенный оператор равенства, определенный пользователем . Поднятый пользовательский оператор равенства применим по указанной вами причине: все типы значений неявно преобразуются в соответствующий им тип, допускающий значение NULL, как и литерал NULL. Это не тот случай, когда определяемый пользователем тип значения, в котором отсутствует определяемый пользователем оператор сравнения, сопоставим с нулевым литералом.
Эрик Липперт
3
@James: Конечно, вы можете реализовать свои собственные operator == и operator! =, Которые принимают структуры, допускающие значение NULL. Если они существуют, то компилятор будет их использовать, а не генерировать автоматически. (И, кстати, я сожалею, что предупреждение для бессмысленного оператора с лифтом для операндов, не допускающих значения NULL, не вызывает предупреждения; это ошибка компилятора, которую мы не удосужились исправить.)
Эрик Липперт
2
Нам нужно наше предупреждение! Мы это заслужили.
Йеппе Стиг Нильсен
3
@JamesDunne: А как насчет определения и добавления static bool operator == (SomeID a, String b)тегов Obsolete? Если второй операнд является нетипизированным литералом null, это будет лучше, чем любая форма, требующая использования поднятых операторов, но если это a, SomeID?который оказывается равным null, победит поднятый оператор.
supercat
17

Это не ошибка, так как есть int?преобразование ( ); он генерирует предупреждение в приведенном примере:

Результатом выражения всегда будет false, поскольку значение типа int никогда не равно null типа int?

Если вы проверите IL, вы увидите, что он полностью удаляет недоступную ветку - ее нет в сборке выпуска.

Однако обратите внимание, что это предупреждение не генерируется для пользовательских структур с операторами равенства. Так было в 2.0, но не в компиляторе 3.0. Код все еще удаляется (поэтому он знает, что код недоступен), но предупреждения не генерируется:

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

С помощью IL (для Main) - обратите внимание, все, кроме MyValue(1)(которые могут иметь побочные эффекты), было удалено:

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

это в основном:

private static void Main()
{
    MyValue v = new MyValue(1);
}
Марк Гравелл
источник
1
Кто-то недавно сообщил мне об этом изнутри. Я не знаю, почему мы перестали выпускать это предупреждение. Мы ввели это как ошибку.
Эрик Липперт,
1
Вот и все: connect.microsoft.com/VisualStudio/feedback/…
Марк Гравелл
5

Тот факт, что сравнение никогда не может быть истинным, не означает, что оно незаконно. Тем не менее, нет, тип значения может когда-либо быть null.

Адам Робинсон
источник
1
Но тип значения может быть равен к null. Подумайте int?, что является синтаксическим сахаром для Nullable<Int32>, что является типом значения. Переменная типа int?определенно может быть равна null.
Грег
1
@Greg: Да, он может быть равен нулю, при условии, что «равное», о котором вы говорите, является результатом ==оператора. Однако важно отметить, что экземпляр на самом деле не является нулевым.
Адам Робинсон,
1

Тип значения не может быть null, хотя он может быть равен null(рассмотрим Nullable<>). В вашем случае intпеременная и nullнеявно приводятся Nullable<Int32>и сравниваются.

Грег
источник
0

Я подозреваю, что ваш конкретный тест просто оптимизируется компилятором, когда он генерирует IL, поскольку тест никогда не будет ложным.

Боковое примечание: возможно ли использовать Int32 для nullable Int32? x вместо этого.

GrayWizardx
источник
0

Я предполагаю, что это потому, что "==" - это синтаксический сахар, который фактически представляет вызов System.Object.Equalsметода, который принимает System.Objectпараметр. Null по спецификации ECMA - это особый тип, который, конечно же, является производным от System.Object.

Вот почему есть только предупреждение.

Виталий
источник
Это неверно по двум причинам. Во-первых, == не имеет той же семантики, что и Object.Equals, если один из его аргументов является ссылочным типом. Во-вторых, null - это не тип. См. Раздел 7.9.6 спецификации, если вы хотите понять, как работает оператор равенства ссылок.
Эрик Липперт,
"Литерал NULL (§9.4.4.6) оценивается как значение NULL, которое используется для обозначения ссылки, не указывающей на какой-либо объект или массив, или отсутствия значения. Тип NULL имеет единственное значение, которое является нулевым значение. Следовательно, выражение, тип которого является нулевым, может оценивать только нулевое значение. Нет способа явно записать нулевой тип и, следовательно, нет способа использовать его в объявленном типе ". - это цитата из ECMA. О чем ты говоришь? Также какую версию ECMA вы используете? В моем не вижу 7.9.6.
Виталий
0

[EDITED: превратили предупреждения в ошибки и сделали операторы явными для определения значения NULL, а не для взлома строки.]

Согласно умному предложению @supercat в комментарии выше, следующие перегрузки операторов позволяют генерировать ошибку при сравнении вашего настраиваемого типа значения с null.

Реализуя операторы, которые сравниваются с версиями вашего типа, допускающими значение NULL, использование NULL в сравнении соответствует версии оператора, допускающей значение NULL, что позволяет генерировать ошибку с помощью атрибута Obsolete.

Пока Microsoft не вернет нам предупреждение компилятора, я буду использовать этот обходной путь, спасибо @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;
    }
}
Йоу йоу
источник
Если я чего-то не упускаю, ваш подход вызовет крик компилятора Foo a; Foo? b; ... if (a == b)..., даже если такое сравнение должно быть совершенно законным. Причина, по которой я предложил "взлом строки", заключается в том, что он позволяет проводить вышеупомянутое сравнение, но не отвечает if (a == null). Вместо использования stringможно заменить любой ссылочный тип, отличный от Objectили ValueType; при желании можно определить фиктивный класс с частным конструктором, который никогда не может быть вызван и присвоить ему право ReferenceThatCanOnlyBeNull.
supercat
Вы абсолютно правы. Я должен был пояснить, что мое предложение нарушает использование значений NULL ... которые в кодовой базе, над которой я работаю, в любом случае считаются греховными (нежелательный бокс и т. Д.). ;)
yoyo
0

Я думаю, что лучший ответ на вопрос, почему компилятор принимает это, касается общих классов. Рассмотрим следующий класс ...

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

Если компилятор не принимал сравнения nullдля типов значений, он по существу нарушил бы этот класс, имея неявное ограничение, прикрепленное к его параметру типа (т.е. он будет работать только с типами, не основанными на значениях).

Ли Дж. Бакстер
источник
0

Компилятор позволит вам сравнить любую структуру, реализующую == значение null. Он даже позволяет вам сравнивать int с null (хотя вы получите предупреждение).

Но если вы дизассемблируете код, вы увидите, что сравнение решается при компиляции кода. Так, например, этот код (где Foo- реализация структуры ==):

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

Генерирует этот 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

Как вы видете:

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

Переведено на:

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

В то время как:

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

Переводится как false:

IL_001e:  ldc.i4.0
жестко запрограммированный
источник