6

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.

svick
  • 236,525
  • 50
  • 385
  • 514
Tamschi
  • 1,089
  • 7
  • 23
  • 1
    What the hack are you trying to do with that bizarre code? – gdoron Jan 06 '13 at 19:45
  • @gdoron It's for some matrix operations that don't have to run fast. I'm pretty sure this is the easiest way to implement these for sparse matrices without overloading everything for simple types and constraining the values to an interface. – Tamschi Jan 06 '13 at 19:53
  • Using `(array[row][column] != null) && (array[row][column].Equals(default(T)) == false)` is likely better than the first statement, I still wonder why they resolve differently though. – Tamschi Jan 06 '13 at 20:20
  • Generics and arithmetic operators are a fickle mix. – Hans Passant Jan 06 '13 at 20:40
  • I'm having a lot of difficulty figuring out what is going on here with all this matrix indexing code. Can you provide a small, simple, self-contained repro that demonstrates the problem? – Eric Lippert Jan 06 '13 at 21:17
  • @EricLippert The error disappeared when I tried to recreate it in a new solution. I'll try to cut down the original projects to a simple example but it could take a while. – Tamschi Jan 06 '13 at 22:16
  • @EricLippert [Repository](https://bitbucket.org/Tamschi/dynamic-operator-call/), [Zip](https://bitbucket.org/Tamschi/dynamic-operator-call/get/default.zip) It works normally if I move the generic method to the main assembly, so maybe it's a bug. – Tamschi Jan 06 '13 at 23:59

1 Answers1

13

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?

Eric Lippert
  • 647,829
  • 179
  • 1,238
  • 2,067
  • Yes, mostly. What I don't completely understand is why I can write `bool equal = /*T*/a == (default(T) as dynamic);` and it calls the operator for T, not object. Does a dynamic value that is `null` match all nullable parameter types? (I updated the repository with an example.) – Tamschi Jan 07 '13 at 08:49
  • I think what confused me in the first place was that I had thought of the type represented by a generic type parameter as accessible, independently of its usual scope. – Tamschi Jan 07 '13 at 10:55
  • 2
    It's interesting that if the compile-time type of one of a dynamic expression's constituent expressions is an open type, such as a type parameter, the run-time analysis uses the run-time closed type instead of the original compile-time open type, even if the closed type is inaccessible. That is, the static type of left-hand side of the multiplication expression is `T`, but the dynamic binding is using `MyClass`. The original code would work as the poster expected if it was somehow a dynamic multiplication expression where both arguments were statically typed as `T`. – Quartermeister Jan 07 '13 at 19:09