C # гаразд із порівнянням типів значень із нулем


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 ==методом, який є членом структури (інакше не ==дозволяється).
Jeppe Stig Nielsen

Відповіді:


119

Це законно, оскільки оператору дозволу на перевантаження є унікальний найкращий оператор на вибір. Існує оператор ==, який приймає два обнулювані вставки. Локальний Int є конвертованим у int, що має нульовий статус. Нульовий літерал перетворюється на нульовий int. Тому це легальне використання оператора ==, і воно завжди призведе до помилки.

Подібним чином ми також дозволяємо вам говорити "if (x == 12,6)", що також завжди буде помилковим. Int локальний конвертований у подвійний, літерал - у подвійний, і очевидно, вони ніколи не будуть рівними.


4
До вашого коментаря: connect.microsoft.com/VisualStudio/feedback/…
Marc Gravell

5
@James: (Я відкликаю свій попередній помилковий коментар, який я видалив.) Визначені користувачем типи значень, які мають визначений користувачем оператор рівності, визначений також за замовчуванням, мають для них піднятий визначений користувачем оператор рівності . Піднятий визначений користувачем оператор рівності застосовується з тієї причини, яку ви заявляєте: усі типи значень неявно перетворюються у відповідний їм тип, що має значення NULL, як і нульовий літерал. Це не той випадок, що визначений користувачем тип значення, в якому відсутній визначений користувачем оператор порівняння, є порівнянним з нульовим літералом.
Ерік Ліпперт

3
@James: Звичайно, ти можеш реалізувати власний оператор == та оператор! =, Який приймає обнулювані структури. Якщо вони існують, то компілятор використовуватиме їх, а не генерує для вас автоматично. (І, до речі, я шкодую, що попередження для безглуздого піднятого оператора на недійсних операндах не видає попередження; це помилка в компіляторі, яку ми не дійшли до виправлення.)
Ерік Ліпперт,

2
Ми хочемо нашого попередження! Ми цього заслуговуємо.
Jeppe Stig Nielsen

3
@JamesDunne: А як щодо визначення static bool operator == (SomeID a, String b)та позначення його Obsolete? Якщо другий операнд є нетипізованим літералом null, це було б кращим збігом, ніж будь-яка форма, яка вимагала використання піднятих операторів, але якщо це таке, 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
    }
}

З ІЛ (для 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
Хтось внутрішньо також повідомив мені про це нещодавно. Я не знаю, чому ми перестали давати це попередження. Ми ввели це як помилку.
Eric Lippert


5

Той факт, що порівняння ніколи не може бути правдивим, не означає, що воно є незаконним. Тим не менше, ні, тип значення ніколи не може бути null.


1
Але тип значення може бути дорівнює до null. Поміркуйте int?, що таке синтаксичний цукор Nullable<Int32>, який тип цінності. Змінна типу int?може, безумовно, дорівнювати null.
Грег

1
@Greg: Так, це може бути рівно нулю, якщо припустити, що "рівний", на який ви посилаєтесь, є результатом ==оператора. Важливо зазначити, що екземпляр насправді не є нульовим.
Адам Робінсон


1

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


0

Я підозрюю, що ваш конкретний тест просто оптимізується компілятором, коли він генерує IL, оскільки тест ніколи не буде хибним.

Примітка збоку: Чи можна використовувати Int32 з використанням Int32, який можна опустити? x замість цього.


0

Я думаю, це тому, що "==" - це синтаксичний цукор, який насправді представляє виклик System.Object.Equalsметоду, який приймає System.Objectпараметр. Нуль за специфікацією ECMA - це особливий тип, який, звичайно, походить від System.Object.

Тому є лише попередження.


Це неправильно з двох причин. По-перше, == не має тієї самої семантики, що й Object.Equals, коли одним із аргументів є тип посилання. По-друге, null не є типом. Дивіться розділ 7.9.6 специфікації, якщо ви хочете зрозуміти, як працює оператор посилальної рівності.
Ерік Ліпперт,

"Нульовий літерал (§9.4.4.6) обчислює нульове значення, яке використовується для позначення посилання, яке не вказує на будь-який об'єкт чи масив, або відсутність значення. Тип нуля має єдине значення, яке є нулем Значення. Отже, вираз, типом якого є нульовий тип, може обчислювати лише нульове значення. Немає можливості явно записати нульовий тип і, отже, жодного способу використовувати його в оголошеному типі. " - це цитата з ECMA. Про що ти говориш? Також яку версію ECMA ви використовуєте? Я не бачу 7.9.6 у своєму.
Віталій

0

[ВИДАЛЕНО: зробив попередження про помилки та зробив операторам явні відомості про нульовий, а не про рядок рядок.]

Відповідно до розумної пропозиції @ supercat у коментарі вище, наведені нижче перевантаження оператора дозволяють згенерувати помилку щодо порівняння вашого власного типу значення з нулем.

Впроваджуючи оператори, які порівнюють з версіями вашого типу, що мають значення, що допускають нульове використання, використання null у порівнянні відповідає версії оператора, що допускає значення, що має значення NULL, що дозволяє генерувати помилку через застарілий атрибут.

Поки 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

Ви абсолютно праві. Я мав би пояснити, що моя пропозиція порушує використання недопустимих значень ... які в кодовій базі, яку я працюю, і так вважаються грішною (небажаний бокс тощо). ;)
йойо

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

Генерує цей ІЛ:

.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
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.