C# хорошо со сравнением типов значений к нулю
Я столкнулся с этим сегодня и понятия не имею, почему компилятор 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?");
}
}
}
11 ответов
Это законно, потому что у оператора разрешения перегрузки есть единственный лучший оператор на выбор. Существует оператор ==, который принимает два обнуляемых числа. Int local является конвертируемым в обнуляемое int. Нулевой литерал конвертируется в обнуляемый тип int. Поэтому это законное использование оператора == и всегда приводит к ложному.
Точно так же мы также разрешаем вам сказать "if (x == 12.6)", что также всегда будет ложным. Int local конвертируем в double, литерал конвертируется в double, и, очевидно, они никогда не будут равны.
Это не ошибка, так как есть (int?
) конверсия; он генерирует предупреждение в приведенном примере:
Результатом выражения всегда является "ложь", поскольку значение типа "int" никогда не равно "нулю" типа "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);
}
Тот факт, что сравнение никогда не может быть правдой, не означает, что это незаконно. Тем не менее, нет, тип значения может быть null
,
Нет, Int32 x
никогда не станет null
,
Если вы сравниваете int с нулем, тогда применяется оператор сравнения, который принимает два целых числа?
"Почему сравнение типа значения с нулевым является предупреждением?" статья поможет вам
Тип значения не может быть null
хотя это может быть равно null
(рассматривать Nullable<>
). В вашем случае int
переменная и null
неявно приводятся к Nullable<Int32>
и сравнил.
Компилятор позволит вам сравнить любую структуру, реализующую ==
к нулю. Он даже позволяет сравнить int с нулем (хотя вы получите предупреждение).
Но если вы разберете код, вы увидите, что сравнение решается при компиляции кода. Так, например, этот код (где 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);
Переводится в ложь:
IL_001e: ldc.i4.0
Я думаю, что лучший ответ относительно того, почему компилятор принимает это, для универсальных классов. Рассмотрим следующий класс...
public class NullTester<T>
{
public bool IsNull(T value)
{
return (value == null);
}
}
Если компилятор не принимал сравнения с null
для типов значений он по существу нарушает этот класс, имея неявное ограничение, связанное с его параметром типа (то есть он будет работать только с типами, не основанными на значениях).
Пишу потому, что проверка вывода компилятора дает несколько иную картину, чем принятый ответ, говорящий о перегрузке оператора. Чтобы увидеть это, рассмотрим этот вариант кода из вопроса:
int x = 1;
int? y = null;
if (x == y)
{
System.Console.WriteLine("What the?");
}
Например, как видно из https://sharplab.io/ , это соответствует следующему низкоуровневому C#:
int num = 1;
Nullable<int> num2 = null;
if ((num == num2.GetValueOrDefault()) & num2.HasValue)
{
Console.WriteLine("What the?");
}
Здесь нет перегрузки оператора, по крайней мере, во время выполнения не используется оператор базовой библиотеки, который принимает два значения NULL. Скорее, nullable превращается в простое целое числоGetValueOrDefault
. В частном случае, когдаy
равен нулю, общий результат из-заHasValue
.
(Причина, по которой я не взял точный код из вопроса, заключается в том, что выражение сравнения будет заменено наfalse
во время компиляции - то есть интеллект компилятора запутал бы проблему.)
Я подозреваю, что ваш конкретный тест просто оптимизируется компилятором, когда он генерирует IL, поскольку тест никогда не будет ложным.
Примечание: возможно ли использование Int32 с недопустимым значением Int32? х вместо.
[Отредактировано: сделал предупреждения в ошибках, и сделал операторы явными о Nullable, а не взломать строки.]
Согласно умному предложению @supercat в приведенном выше комментарии, следующие перегрузки операторов позволяют генерировать ошибку о сравнении вашего пользовательского типа значения со значением 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;
}
}
Я думаю, это потому, что "==" является синтаксическим сахаром, который на самом деле представляет собой вызов System.Object.Equals
метод, который принимает System.Object
параметр. Нулевой согласно спецификации ECMA - это особый тип, который, конечно, получен из System.Object
,
Вот почему есть только предупреждение.