Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Comparison to null evaluates to true for both expr == null and expr != null

Tags:

c#

.net

null

I am seeing something very strange, which I cannot explain. I am guessing some edge case of C# which I am not familiar with, or a bug in the runtime / emitter?

I have the following method:

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

While testing my app, I see it is misbehaving - it is returning true for objects I know do not exist in my db. So I stopped at the method and in Immediate, I ran the following:

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

GetObject is defined like so:

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

Interestingly, when casting the expression to object, the comparison is evaluated correctly:

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

There is no equality operator overriding.

Edit: It turns out there is an operator overload, which was incorrect. But then why would the equality evaluate correctly in the internal method generic GetObject, where rv is of type HistoryMessage in this case.

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

    //...
}

What is going on here?

like image 561
Léo Natan Avatar asked Jun 01 '16 08:06

Léo Natan


2 Answers

  • As you already clarified, the == operator failed for your type because you had an overload that was incorrect.
  • When casting to object, the == operator worked correctly since it was object's implementation of == that was used and not EquatableIdentifiableObject's.
  • In the method GetObject the operator evaluates correctly because it is not EquatableIdentifiableObject's implementation of == that is being used. In C# generics are resolved at run-time (at least in the sense that is relevant here) and not at compile time. Note that == is static and not virtual. So the type T is resolved at run-time but the call to == has to be resolved at compile time. At compile time when the compiler resolves == it will not know to use EquatableIdentifiableObject's implementation of ==. Since the type T has this constraint: where T : DBObject, new(), DBObject's implementation (if any) will be used. If DBObject does not define == then the implementaion of the first base class that does so (up to object) will be used.

A few more comments about EquatableIdentifiableObject's implementation of ==:

  • You could replace this part:
if (ReferenceEquals(self, null))
{
     return ReferenceEquals(other, null);
}

with:

// If both are null, or both are the same instance, return true.
if (object.ReferenceEquals(h1, h2))
{
    return true;
}
  • It would be more robust to replace
public static bool operator !=(EquatableIdentifiableObject self, EquatableIdentifiableObject other)
{
    ...
}

with:

public static bool operator !=(EquatableIdentifiableObject self, EquatableIdentifiableObject other)
{
    return !(self == other);
}
  • The way you define the signature for == is slightly misleading. The first parameter is named self and the second is named other. That would be ok if == was an instance method. Since it is a static method, the name self is a bit misleading. Better names would be o1 and o2 or something along this line so that the two operands are treated on a more equal footing.
like image 110
Ladi Avatar answered Oct 15 '22 07:10

Ladi


There can be several overloads of operator ==(...) as you now know. Some of them can be C# built-in overloads, and others can be user-defined operators.

If you hold the mouse over the != or == symbol in Visual Studio, it will show you what overload is chosen by overload resolution (up till VS2013 it would only show it if the chosen overload was actually a user-defined one, in VS2015 it will show it in all cases I believe).

The binding of == (i.e. which overload to call) is fixed statically at compile-time. The is nothing dynamic or virtual about it. So if you have:

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

  if (rv != null)
  {

then which overload of != to use will be fixed, at compile-time, with usual overload resolution (including a few special rules for ==). The rv has type T which is known to be a reference type eqaul to or deriving from SomeBaseClass. So the best overload is chosen based on that. That might be the operator !=(object, object) overload (built-in) if SomeBaseClass does not define (or "inherit") an appropriate overload.

At run-time, then, even if the actual substitution for T happens to be a more specific type SomeEqualityOverloadingClass (that satisfies the constraint of course), that does not mean a new overload resolution will happen at run-time!

This is different from the virtual method .Equals(object).

In C#, generics do not work like templates, and they are not like dynamic.

If you really want dynamic overload resolution (binding at run-time instead of at compile-time), it is allowed to say if ((dynamic)rv != null).

like image 24
Jeppe Stig Nielsen Avatar answered Oct 15 '22 08:10

Jeppe Stig Nielsen