Сравнение с нулевым значением равно true для expr == null и expr!= Null

Я вижу что-то очень странное, чего я не могу объяснить. Я угадываю какой-то крайний случай С#, с которым я не знаком, или ошибка в среде выполнения/эмиттера?

У меня есть следующий метод:

public static bool HistoryMessageExists(DBContext context, string id)
{
    return null != context.GetObject<HistoryMessage>(id);
}

Во время тестирования моего приложения я вижу, что это неверно работает - он возвращает true для объектов, которые, как я знаю, не существуют в моем db. Поэтому я остановился на методе и в Immediate, я запустил следующее:

context.GetObject<HistoryMessage>(id)
null
null == context.GetObject<HistoryMessage>(id)
true
null != context.GetObject<HistoryMessage>(id)
true

GetObject определяется следующим образом:

public T GetObject<T>(object pk) where T : DBObject, new()
{
    T rv = Connection.Get<T>(pk);

    if (rv != null)
    {
        rv.AttachToContext(this);
        rv.IsInserted = true;
    }

    return rv;
}

Интересно, что при выдаче выражения в object сравнение оценивается правильно:

null == (object)context.GetObject<HistoryMessage>(id)
true
null != (object)context.GetObject<HistoryMessage>(id)
false

Не существует переопределения оператора равенства.

Изменить: Оказывается, существует перегрузка оператора, которая была неправильной. Но тогда почему равенство правильно оценивалось во внутреннем методе generic GetObject, где rv имеет тип HistoryMessage в этом случае.

public class HistoryMessage : EquatableIdentifiableObject
{
    public static bool HistoryMessageExists(DBContext context, string id)
    {
        var rv = context.GetObject<HistoryMessage>(id);
        bool b = rv != null;
        return b;
    }

    public static void AddHistoryMessage(DBContext context, string id)
    {
        context.InsertObject(new HistoryMessage { Id = id });
    }
}

public abstract partial class EquatableIdentifiableObject : DBObject, IObservableObject
{
    public event PropertyChangedEventHandler PropertyChanged;

    [PrimaryKey]
    public string Id { get; set; }

    //...
}

public abstract partial class EquatableIdentifiableObject
{
    //...

    public static bool operator ==(EquatableIdentifiableObject self, EquatableIdentifiableObject other)
    {
        if (ReferenceEquals(self, null))
        {
            return ReferenceEquals(other, null);
        }

        return self.Equals(other);
    }

    public static bool operator !=(EquatableIdentifiableObject self, EquatableIdentifiableObject other)
    {
        if (ReferenceEquals(self, null))
        {
            return !ReferenceEquals(other, null);
        }

        return !self.Equals(other);
    }
}

public abstract class DBObject
{
    [Ignore]
    protected DBContext Context { get; set; }

    [Ignore]
    internal bool IsInserted { get; set; }

    //...
}

Что здесь происходит?

Ответ 1

  • Как вы уже выяснили, оператор == не смог выполнить ваш тип, потому что у вас была неверная перегрузка.
  • При нажатии на объект оператор == работал правильно, так как это была object's реализация ==, которая была использована, а не EquatableIdentifiableObject's.
  • В методе GetObject оператор правильно оценивает, потому что это не EquatableIdentifiableObject's реализация ==, которая используется. В С# обобщения разрешаются во время выполнения (по крайней мере, в том смысле, что здесь актуально), а не во время компиляции. Обратите внимание, что == является статическим, а не виртуальным. Таким образом, тип T разрешен во время выполнения, но вызов == должен быть разрешен во время компиляции. Во время компиляции, когда компилятор разрешает ==, он не будет знать, использовать EquatableIdentifiableObject's реализацию ==. Так как тип T имеет это ограничение: будет использоваться where T : DBObject, new(), DBObject's реализация (если таковая имеется). Если DBObject не определяет ==, тогда будет использоваться реализация первого базового класса, который делает это (до object).

Несколько комментариев о EquatableIdentifiableObject's реализации ==:

  • Вы можете заменить эту часть:
if (ReferenceEquals(self, null))
{
     return ReferenceEquals(other, null);
}

с:

// If both are null, or both are the same instance, return true.
if (object.ReferenceEquals(h1, h2))
{
    return true;
}
  • Было бы более удобно заменять
public static bool operator !=(EquatableIdentifiableObject self, EquatableIdentifiableObject other)
{
    ...
}

с:

public static bool operator !=(EquatableIdentifiableObject self, EquatableIdentifiableObject other)
{
    return !(self == other);
}
  • То, как вы определяете подпись для ==, немного вводит в заблуждение. Первый параметр имеет имя self, а второй - other. Это было бы нормально, если == был методом экземпляра. Поскольку это статический метод, имя self немного вводит в заблуждение. Лучшими именами были бы o1 и o2 или что-то в этом направлении, чтобы два операнда обрабатывались на более равномерной основе.

Ответ 2

Может быть несколько перегрузок operator ==(...), как вы теперь знаете. Некоторые из них могут быть встроенными перегрузками С#, а другие могут быть определяемыми пользователем операторами.

Если вы удерживаете мышь над символом != или == в Visual Studio, он покажет вам, какая перегрузка выбрана с помощью разрешения перегрузки (вплоть до VS2013 она будет показывать только это, если выбранная перегрузка была фактически пользователем -определенный, в VS2015 он покажет его во всех случаях, которые я считаю).

привязка == (т.е. перегрузка для вызова) фиксируется статически во время компиляции. В этом нет ничего динамичного или виртуального. Поэтому, если у вас есть:

public T SomeMethod<T>() where T : SomeBaseClass
{
  T rv = ...;

  if (rv != null)
  {

то какая перегрузка != для использования будет фиксирована во время компиляции с обычным разрешением перегрузки (включая несколько специальных правил для ==). rv имеет тип T, который известен как ссылочный тип eqaul или полученный из SomeBaseClass. Поэтому на этой основе выбирается лучшая перегрузка. Это может быть перегрузка operator !=(object, object) (встроенная), если SomeBaseClass не определяет (или "наследует" ) соответствующую перегрузку.

Во время выполнения, даже если фактическая подстановка для T оказывается более конкретным типом SomeEqualityOverloadingClass (который, конечно же, удовлетворяет ограничению), это не означает, что новое разрешение перегрузки произойдет при запуске -время!

Это отличается от метода virtual .Equals(object).

В С# дженерики не работают как шаблоны, и они не похожи на dynamic.

Если вы действительно хотите dynamic разрешение перегрузки (привязка во время выполнения, а не во время компиляции), разрешено говорить if ((dynamic)rv != null).