10

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?

Léo Natan
  • 56,823
  • 9
  • 150
  • 195
  • 2
    Does `HistoryMessage` implement the equality operators? – Lasse V. Karlsen Jun 01 '16 at 08:05
  • @LasseV.Karlsen No, no overriding. Forgot to write in the question that there is no operator overriding here. – Léo Natan Jun 01 '16 at 08:06
  • 1
    Not even on any of the base classes for `HistoryMessage`? – Lasse V. Karlsen Jun 01 '16 at 08:07
  • 2
    In any case, can you show us `HistoryMessage`? – Lasse V. Karlsen Jun 01 '16 at 08:08
  • 1
    If you decompile the project you can spot if there is actually operator overloading in play. Try [dotPeek](https://www.jetbrains.com/decompiler/) for a free decompiler. If there is a basic reference check the comparison should be implemented by the `ceq` instruction, if there is operator overloading in play it would look like `call SomeClass.op_Equality`. You can also spot this in [LINQPad](http://linqpad.net) which is where I tried this. – Lasse V. Karlsen Jun 01 '16 at 08:13
  • @LasseV.Karlsen Good idea, will test. Thanks – Léo Natan Jun 01 '16 at 08:15
  • 3
    Another reason for this behavior could be a racing condition or a bug in `GetObject` that makes consecutive calls return different results. Cache `context.GetObject(id)` to a local variable and *then* check for `null` equality and see if the issue persists. – InBetween Jun 01 '16 at 08:25
  • I suspect `Connection.Get()` is behaving wrong, I also agree to @InBetween, sometimes caching logic or any other logic may act weird that even few microsecond in execution will make a difference. Can you post code for `Connection.Get()` ? – Akash Kava Jun 01 '16 at 08:28
  • They are returning the same result. Assigning to a local variable results in the same behavior. – Léo Natan Jun 01 '16 at 08:32
  • @AkashKava Why is then the internal `if (rv != null)` comparison evaluated correctly? – Léo Natan Jun 01 '16 at 08:33
  • @LeoNatan how do you know the internal comparison evaluated correctly? If `rv` is `null`, it will simply return `null`, for first time, there is no distinguishing factor here. Try to put log in there, to prove that it is behaving strange as well. – Akash Kava Jun 01 '16 at 08:37
  • @LeoNatan: does any multi-threading take place? – Dennis Jun 01 '16 at 08:53
  • @AkashKava The internal comparison evaluates correctly, because otherwise there would be a null pointer exception. Adding debug log proves that the internal method behaves correctly. – Léo Natan Jun 01 '16 at 10:23
  • @Dennis Right now there isn't any multithreading. – Léo Natan Jun 01 '16 at 10:23
  • @LasseV.Karlsen Hmm, after looking CIL I saw there was operator overloading, which I later found. But in that case, what I don't understand is why in the internal, generic method `GetObject`, the overloaded `!=` operator was not called for `rv != null`? – Léo Natan Jun 01 '16 at 14:36
  • Where was the operator declared? In which class? – Lasse V. Karlsen Jun 01 '16 at 16:49
  • In any case, it is impossible to answer why a method behaves the way it does without seeing the method, so if you want to know why the operator didn't fail in the GetObject method, you're going to have to post the operator overload method. – Lasse V. Karlsen Jun 01 '16 at 17:10
  • It is defined as a `partial` of the superclass of `HistoryMessage`. Get object has `HistoryMessage` as its generic type. I will post tomorrow the classes. – Léo Natan Jun 01 '16 at 17:12
  • @LasseV.Karlsen Please see edit. I've added my classes. – Léo Natan Jun 02 '16 at 13:56

2 Answers2

3
  • 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.
Ladi
  • 1,274
  • 9
  • 17
  • 1
    Thanks for the explanation. I'm guessing operators are not virtual because they are static. Any idea why operators are implemented as static? Is it because of the null edge case? – Léo Natan Jun 03 '16 at 09:00
  • See this: http://stackoverflow.com/questions/2018108/why-must-c-sharp-operator-overloads-be-static – Ladi Jun 03 '16 at 09:04
  • BTW, if you write your own generic method and want to benefit of the virtual aspect of comparison you can do so by overwriting the virtual `Equals` and calling that. – Ladi Jun 03 '16 at 09:12
  • Actually, I do have it overwritten. That's why I initially thought I had no operator overloads (not a big fan). It seems here I find more reasons not to like them. – Léo Natan Jun 03 '16 at 09:13
1

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).

Jeppe Stig Nielsen
  • 60,409
  • 11
  • 110
  • 181