What are the precise rules for overload resolution with ==
between two expressions of delegate type?
Consider the following code (where using System;
is needed):
static class ProgramA
{
static void TargetMethod(object obj)
{
}
static void Main()
{
Action<object> instance1 = TargetMethod;
Action<object> instance2 = TargetMethod;
Action<string> a1 = instance1;
Action<Uri> a2 = instance2;
Console.WriteLine((object)a1 == (object)a2);
Console.WriteLine((Delegate)a1 == (Delegate)a2);
Console.WriteLine((Action<object>)a1 == (Action<object>)a2);
Console.WriteLine(a1 == a2); // warning CS0253: Possible unintended reference comparison; to get a value comparison, cast the right hand side to type 'System.Action<string>'
}
}
Explanation:
instance1
and instance2
are two separate instances of the same run-time type, the generic Action<in T>
which is contravariant in T
. Those instances are distinct but Equals
since they the have same targets.
a1
and a2
are the same as instance1
and instance2
, but because of the contravariance of Action<in T>
there exist implicit reference conversions from Action<object>
to each of Action<string>
and Action<System.Uri>
.
Now, the C# Language Specification has (among other overloads) these operator ==
:
bool operator ==(object x, object y); // §7.10.6
bool operator ==(System.Delegate x, System.Delegate y); // §7.10.8
The current Visual C# compiler realizes the first one by simply checking if the references are the same (the IL does not actually call a mscorlib method like object.ReferenceEquals
, but that would give the same result), while it realizes the second one by calling Delegate.op_Equality
method which looks like a "user-defined" operator inside that assembly even when it is defined by the C# Language Spec, so is maybe not "user-defined" in the sense of the spec(?).
Note that §7.10.8 is a little confusing because it says "Every delegate type implicitly provides the following predefined comparison operator[s]" and then gives the operator with the (System.Delegate, System.Delegate)
signature. That is just one operator, not one for "every" delegate type? This seems important for my question.
It is not surprising that the three first WriteLine
write False
, True
and True
, respectively, given what I said above.
Question: But why does the fourth WriteLine
lead to the (object, object)
overload being used?
There does exist an implicit reference conversion from Action<>
(or any other delegate type) to System.Delegate
, so why can't that be used here? Overload resolution should prefer that over the (object, object)
option.
Of course, there are no implicit conversions between Action<string>
and Action<Uri>
, but why is that relevant? If I create my own class MyBaseClass
containing a user-defined operator ==(MyBaseClass x, MyBaseClass y)
and I create two unrelated deriving classes, then my ==
operator will still be used (left and right operand not convertible to each other but both convertible to MyBaseClass
).
Just for completeness, here is the analogous example with covariance (Func<out TResult>
) instead of contravariance:
static class ProgramF
{
static string TargetMethod()
{
return "dummy";
}
static void Main()
{
Func<string> instance1 = TargetMethod;
Func<string> instance2 = TargetMethod;
Func<ICloneable> f1 = instance1;
Func<IConvertible> f2 = instance2;
Console.WriteLine((object)f1 == (object)f2);
Console.WriteLine((Delegate)f1 == (Delegate)f2);
Console.WriteLine((Func<string>)f1 == (Func<string>)f2);
Console.WriteLine(f1 == f2); // warning CS0253: Possible unintended reference comparison; to get a value comparison, cast the right hand side to type 'System.Func<System.ICloneable>'
}
}
A question related to my question above is, where in the C# Language Specification does it say that this shall be illegal:
Func<string> g1 = ...;
Func<Uri> g2 = ...;
Console.WriteLine(g1 == g2); // error CS0019: Operator '==' cannot be applied to operands of type 'System.Func<string>' and 'System.Func<System.Uri>'
I can see that the compiler figured out that no type can ever inherit from both string
and Uri
(unlike the pair ICloneable
and IConvertible
), and so this (if it were legal) could only become true
if both variables were null
, but where does it say that I am not allowed to do this? In this case it would not matter if the compiler chose operator ==(object, object)
or operator ==(Delegate, Delegate)
since, as I said, it comes down to checking if both are null references, and both overloads do that in the same way.