dynamic operator resolution

2019-03-20 14:08发布

问题:

I have a generic method that calls operators by casting one of the operands to dynamic. There are two different calls:

//array is T[][]
//T is MyClass
array[row][column] != default(T) as dynamic

This works and calls static bool operator !=(MyClass a, MyClass b) (even if both sides are null).

What surprised me is the behaviour of the following line:

//array, a and b are T[][]
//T is MyClass
array[row][column] += a[line][i] * (b[i][column] as dynamic);

This calls
public static MyClass operator *(MyClass a, object b) and
public static MyClass operator +(MyClass a, object b)

and not
public static MyClass operator *(MyClass a, MyClass b) and
public static MyClass operator +(MyClass a, MyClass b).

Removing the (MyClass, object) operators causes

Microsoft.CSharp.RuntimeBinder.RuntimeBinderException wurde nicht behandelt.
  HResult=-2146233088
  Message=Der *-Operator kann nicht auf Operanden vom Typ "[...].MyClass" und "object" angewendet werden.
  Source=Anonymously Hosted DynamicMethods Assembly
  StackTrace:
       bei CallSite.Target(Closure , CallSite , MyClass , Object )
       bei System.Dynamic.UpdateDelegates.UpdateAndExecute2[T0,T1,TRet](CallSite site, T0 arg0, T1 arg1)
       bei [...].MatrixMultiply[T](T[][] a, T[][] b) in 
       [...]
  InnerException: 

(ellipses mine).

Why?
Can I call the right operator without explicitly calling a T Operators.Add<T>(T a, T b) method instead of the operator?

Update

public static T TestMethod<T>(this T a, T b)
    {
        return (T)(a * (b as dynamic));
    }

This method in a separate assembly calls (or tries to call) operator *(T, object), if the same method is in the main assembly it correctly calls operator *(T, T).

The class I use as type parameter is internal and the problem disappears when I change it to public, so it seems to depend on the class' visibility towards the method.

operator *(T, object) is called successfully even if the class isn't visible.

回答1:

It sounds like you have stumbled upon an interesting design decision -- not a bug, this was deliberate -- of the dynamic feature. I've been meaning to blog about this one for some time.

First off, let's take a step back. The fundamental idea of the dynamic feature is that an expression containing an operand of dynamic type has its type analysis deferred until runtime. At runtime, the type analysis is done fresh by spinning up a new version of the compiler and re-do the analysis, this time treating the dynamic expression as though it were an expression of its actual runtime type.

So if you have an addition expression which at compile time has a left hand compile-time type of object, and a right-hand compile-time type of dynamic, and at runtime the dynamic expression is in fact a string, then the analysis is re-done with the left hand side being object and the right hand side being string. Notice that the runtime type of the left hand side is not considered. It's compile time type was object, not dynamic. Only expressions of dynamic type have the property that their runtime types are used in the runtime analysis.

Just to make sure that's clear: if you have:

void M(Giraffe g, Apple a) {...}
void M(Animal a, Fruit f) { ... }
...
Animal x = new Giraffe();
dynamic y = new Apple();
M(x, y);

then at runtime, the second override is called. The fact that at runtime x is Giraffe is ignored, because it wasn't dynamic. It was Animal at compile time, and so at runtime it continues to be analyzed as an expression of type Animal. That is, the analysis is done as though you had said:

M(x, (Apple)y);

and that obviously picks the second overload.

I hope that is clear.

Now we come to the meat of the issue. What happens when the runtime type would not have been accessible? Let's actually work up an example:

public class Fruit {}
public class Apple : Fruit 
{
  public void M(Animal a) {}
  private class MagicApple : Apple 
  {
    public void M(Giraffe g) {}
  }
  public static Apple MakeMagicApple() { return new MagicApple(); }
}
...
dynamic d1 = Apple.MakeMagicApple();
dynamic d2 = new Giraffe();
d1.M(d2);

OK, what happens? We have two dynamic expressions, so according to my earlier statement, at runtime we do the analysis again but pretend that you said

((Apple.MagicApple)d1).M((Giraffe)d2));

And so you would think that overload resolution would choose the method Apple.MagicApple.M that exactly matches that. But it does not! We cannot pretend that the code above is what you said because that code accesses a private nested type outside its accessibility domain! That code would fail to compile entirely. But equally obviously we cannot allow this code to fail, because this is a common scenario.

So I must emend my previous statement. What the runtime analysis engine actually does is pretend that you inserted casts that you could legally have inserted. In this case, it realizes that the user could have inserted:

((Apple)d1).M((Giraffe)d2));

And overload resolution would have chosen Apple.M.

Moreover: the pretend casts are always to class types. It is possible that there is an interface type or a type parameter type cast that could have been inserted that would cause overload resolution to succeed, but by using "dynamic" you indicated that you wanted the runtime type to be used, and the runtime type of an object is never an interface or type parameter type.

It sounds like you are in the same boat. If the dynamic expression's runtime type would not have been accessible at the call site then it is treated as being of its closest accessible base type for the purposes of runtime analysis. In your case, the closest accessible base type might be object.

Is that all clear?