This looks like a bug in lifting to null of operands on generic structs.
Consider the following dummy struct, that overrides operator==
:
struct MyStruct
{
private readonly int _value;
public MyStruct(int val) { this._value = val; }
public override bool Equals(object obj) { return false; }
public override int GetHashCode() { return base.GetHashCode(); }
public static bool operator ==(MyStruct a, MyStruct b) { return false; }
public static bool operator !=(MyStruct a, MyStruct b) { return false; }
}
Now consider the following expressions:
Expression<Func<MyStruct, MyStruct, bool>> exprA =
(valueA, valueB) => valueA == valueB;
Expression<Func<MyStruct?, MyStruct?, bool>> exprB =
(nullableValueA, nullableValueB) => nullableValueA == nullableValueB;
Expression<Func<MyStruct?, MyStruct, bool>> exprC =
(nullableValueA, valueB) => nullableValueA == valueB;
All three compile and run as expected.
When they're compiled (using .Compile()
) they produce the following code (paraphrased to English from the IL):
The first expression that takes only MyStruct
(not nullable) args, simply calls op_Equality
(our implementation of operator ==
)
The second expression, when compiled, produces code that checks each argument to see if it HasValue
. If both don't (both equal null
), returns true
. If only one has a value, returns false
. Otherwise, calls op_Equality
on the two values.
The third expression checks the nullable argument to see if it has a value - if not, returns false. Otherwise, calls op_Equality
.
So far so good.
Next step: do the exact same thing with a generic type - change MyStruct
to MyStruct<T>
everywhere in the definition of the type, and change it to MyStruct<int>
in the expressions.
Now the third expression compiles but throws a runtime exception InvalidOperationException
with the following message:
The operands for operator 'Equal' do not match the parameters of method 'op_Equality'.
I would expect generic structs to behave exactly the same as non-generic ones, with all the nullable-lifting described above.
So my questions are:
The full code for reproducing this is available on this gist.
The short answer is: yes, that's a bug. I've put a minimal repro and a short analysis below.
My apologies. I wrote a lot of that code and so it was likely my bad.
I have sent a repro off to the Roslyn development, test and program management teams. I doubt this reproduces in Roslyn, but they'll verify that it does not and decide whether this makes the bar for a C# 5 service pack.
Feel free to enter an issue on connect.microsoft.com as well if you want it tracked there as well.
Minimal repro:
using System;
using System.Linq.Expressions;
struct S<T>
{
public static bool operator ==(S<T> a, S<T> b) { return false; }
public static bool operator !=(S<T> a, S<T> b) { return false; }
}
class Program
{
static void Main()
{
Expression<Func<S<int>?, S<int>, bool>> x = (a, b) => a == b;
}
}
The code that is generated in the minimal repro is equivalent to
ParameterExpression pa = Expression.Parameter(typeof(S<int>?), "a");
ParameterExpression pb = Expression.Parameter(typeof(S<int>), "b");
Expression.Lambda<Func<S<int>?, S<int>, bool>>(
Expression.Equal(pa, pb, false, infoof(S<int>.op_Equality)
new ParameterExpression[2] { pa, pb } );
Where infoof
is a fake operator that gets a MethodInfo
for the given method.
The correct code would be:
ParameterExpression pa = Expression.Parameter(typeof(S<int>?), "a");
ParameterExpression pb = Expression.Parameter(typeof(S<int>), "b");
Expression.Lambda<Func<S<int>?, S<int>, bool>>(
Expression.Equal(pa, Expression.Convert(pb, typeof(S<int>?), false, infoof(S<int>.op_Equality)
new ParameterExpression[2] { pa, pb } );
The Equal
method cannot deal with one nullable, one non-nullable operands. It requires that either both are nullable or neither is.
(Note that the false
is correct. This Boolean controls whether the result of a lifted equality is a lifted Boolean; in C# it is not, in VB it is.)
Yes, this bug is gone in Roslyn (the compiler under development). We'll see about the existing product.
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