Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why are generic and non-generic structs treated differently when building expression that lifts operator == to nullable?

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

  1. The first expression that takes only MyStruct (not nullable) args, simply calls op_Equality (our implementation of operator ==)

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

  3. 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:

  1. Why is there a difference between generic and non-generic structs?
  2. What is the meaning of this exception?
  3. Is this a bug in C#/.NET?

The full code for reproducing this is available on this gist.

like image 936
sinelaw Avatar asked May 28 '13 17:05

sinelaw


2 Answers

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

like image 154
Eric Lippert Avatar answered Nov 10 '22 09:11

Eric Lippert


Yes, this bug is gone in Roslyn (the compiler under development). We'll see about the existing product.

like image 31
Neal Gafter Avatar answered Nov 10 '22 10:11

Neal Gafter