24

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.

sinelaw
  • 16,205
  • 3
  • 49
  • 80
  • 1
    Could you post your modified code as well? It looks like you might have missed a spot when copy-pasting `MyStruct` in place of `MyStruct`. – Sergey Kalinichenko May 28 '13 at 17:25
  • 1
    I've added a [gist](https://gist.github.com/anonymous/5664417) with the full code, also added a link at the end of the question. – sinelaw May 28 '13 at 17:26

2 Answers2

22

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

Eric Lippert
  • 647,829
  • 179
  • 1,238
  • 2,067
  • Once again, thanks Eric. I've opened a [bug on connect](https://connect.microsoft.com/VisualStudio/feedback/details/788793/expression-equal-with-one-nullable-and-one-plain-generic-struct-value-causes-invalidoperationexception#). – sinelaw May 28 '13 at 17:34
  • 10
    Allow me to also comment that without quick responses like these, our team in the past would wait months or even years for a proper clarification on Connect, during which we implemented workarounds that may or may not be correct. I hope that someone still at Microsoft will make fast responses an official policy. – sinelaw May 28 '13 at 17:38
  • @sinelaw: You're welcome. The Roslyn team confirms to me that this does repro in C# 5 but does not repro in Roslyn by the way. See Neal's answer. – Eric Lippert May 28 '13 at 22:32
  • You tease with the `infoof` operator, Eric. Any hope this is a portent of good things to come in C#? – MgSam May 31 '13 at 14:18
  • Interesting. This code also reproduces the bug: `using System; using System.Linq.Expressions; static class Program { static ArraySegment A; static ArraySegment? B; static void Main() { Expression> e = () => A == B; } } ` By the way, it can also happen when the struct is non-generic but is nested inside a generic class. – Jeppe Stig Nielsen Jun 10 '13 at 08:30
5

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

Neal Gafter
  • 3,293
  • 20
  • 16