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?
==
operator failed for your type because you had an overload that was incorrect. ==
operator worked correctly since it was object's
implementation of ==
that was used and not EquatableIdentifiableObject's
. 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 ==
:
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;
}
public static bool operator !=(EquatableIdentifiableObject self, EquatableIdentifiableObject other)
{
...
}
with:
public static bool operator !=(EquatableIdentifiableObject self, EquatableIdentifiableObject other)
{
return !(self == other);
}
==
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. 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)
.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With