Похоже, что проблема в том, что когда MS ввел нулевые типы, они сделали так, что каждая структура неявно преобразуется в свой нулевой тип (foo?
), так что код
if( f == null)
эквивалентен
if ( (Nullable<foo>)f == (Nullable<foo>)null)
Поскольку в MSDN сказано, что "любые пользовательские операторы, существующие для типов значений, могут также использоваться нулевыми типами", когда вы переопределяете оператор ==
, вы позволяете неявное приведение компилировать, так как теперь у вас есть пользовательский == -- дающий вам нулевую перегрузку бесплатно.
В стороне:
Похоже, что в вашем примере есть некоторая оптимизация компилятора. Единственное, что выдает компилятор, который даже намекает, что был тест, это IL:
ldc.i4.0
ldc.i4.0
ceq
stloc.1 //where there is an unused boolean local
Обратите внимание, что если поменять main на
Foo f = new Foo();
object b = null;
if (f == b) { Console.WriteLine("?"); }
, то он больше не будет компилироваться. Но если вы установите флажок struct:
Foo f = new Foo();
object b = null;
if ((object)f == b) { Console.WriteLine("?"); }
если компилирует, то испускает IL, и запускается, как и ожидалось (структура никогда не бывает нулевой);
.struct не определяет перегрузки "==" или "!=", поэтому вы получили оригинальную ошибку. После добавления перегрузок в Вашу структуру сравнение было легальным (с точки зрения компилятора). Как создатель перегрузки оператора, Вы отвечаете за обработку этой логики (очевидно, Microsoft пропустила это в данном случае).
В зависимости от реализации Вашей структуры (и того, что она собой представляет) сравнение с нулем может быть вполне корректным, вот почему это возможно.
.рекомендую вам заглянуть на эти страницы:
http://www.albahari.com/valuevsreftypes.aspx
Все, что я могу подумать, это то, что ваша перегрузка оператора == дает компилятору выбор между:
public static bool operator ==(object o1, object o2)
и
public static bool operator ==(Foo f1, Foo f2)
и что с помощью обоих вариантов он может привести левую к объекту и использовать первую. Конечно, если вы попытаетесь запустить что-то, основанное на вашем коде, это не приведет к перегрузке оператора. Не имея выбора между операторами, компилятор явно проводит некоторую дополнительную проверку.
Я полагаю, что когда вы перегружаете оператора, вы явно подписываетесь на то, что будете обрабатывать всю необходимую логику с конкретным оператором. Следовательно, Вы отвечаете за обработку нуля в методе перегрузки оператора, если он когда-нибудь получит удар. В данном случае, как я уверен, Вы наверняка заметили, перегруженные методы никогда не получат удара, если сравнивать с нулем.
Что действительно интересно, так это то, что после ответа Хенкс здесь , я проверил в рефлекторе следующий код.
Foo f1 = new Foo();
if(f1 == null)
{
Console.WriteLine("impossible");
}
Console.ReadKey();
Это то, что показал рефлектор.
Foo f1 = new Foo();
Console.ReadKey();
Компилятор его очистил, и поэтому перегруженные методы операторов даже не вызывались.
Это не имеет ничего общего с сериализацией или COM - так что стоит убрать это из уравнения. Например, вот короткая, но полная программа, которая демонстрирует проблему:
using System;
public struct Foo
{
// These change the calling code's correctness
public static bool operator ==(Foo f1, Foo f2) { return false; }
public static bool operator !=(Foo f1, Foo f2) { return false; }
// These aren't relevant, but the compiler will issue an
// unrelated warning if they're missing
public override bool Equals(object x) { return false; }
public override int GetHashCode() { return 0; }
}
public class Test
{
static void Main()
{
Foo f = new Foo();
Console.WriteLine(f == null);
}
}
Я считаю, что это компилируется, потому что существует неявное преобразование из нулевого литерала в Nullable
, и вы можете сделайте это законно:
Foo f = new Foo();
Foo? g = null;
Console.WriteLine(f == g);
Интересно, что это происходит только тогда, когда == перегружен - Марк Гравелл заметил это раньше. Я не знаю, действительно ли это ошибка компилятора или что-то очень тонкое в способе разрешения преобразований, перегрузок и т. Д.
В некоторых случаях (например, int
, decimal
) компилятор предупредит вас о неявном преобразовании, но в других случаях (например, Guid
) нет.