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.