Dynamic in C# III: A slight twist

Last time we dealt with the basics of dynamic binding. This time, we'll add a small twist.

First, lets recall the example we were using last time:

 static void Main(string[] args)
{
    dynamic d = 10;
    C c = new C();

    // (1) Dynamic receivers.
    d.Foo(); // Call.
    d.PropOrField = 10; // Property.
    d[10] = 10; // Indexer.

    // (2) Statically typed receivers (or static methods)
    //     with dynamic arguments.
    c.Foo(d); // Instance method call.
    C.StaticMethod(d); // Static method call.
    c.PropOrField = d; // Property.
    c[d] = 10; // Indexer.
    d++; // Think of this as op_increment(d).
    var x = d + 10; // Think of this as op_add(d, 10).
    int x = d; // Think of this as op_implicit(d).
    int y = (int)d; // Think of this as op_explicit(d).
}

Last time we dealt with the first set of invocations - those with dynamic receivers. This time, we'll deal with the second set - those with either static receivers, or no real apparent receiver.

What do you expect?

Lets take the simplest of this set of invocations and expand it out a bit. Suppose we have something like the following:

 public class C
{
    public void Foo(decimal x) { ... }
    public void Foo(string x) { ... }
    static void Main(string[] args)
    {
        C c = new C();
        dynamic d = 10;
        c.Foo(d);
    }
}

First lets consider this from a purely intuitive standpoint. What would we expect to happen?

Since we know the type of our local variable 'c', intuitively, we know that one of the two overloads of Foo on C should be called. However, we also know that d is dynamically typed, so the compiler cannot determine the exact overload to be called until runtime. We would therefore expect the combination of these two to happen - the compiler will determine the candidate set at compile time, and determine which the call should resolve to at runtime. In this case, since d has the value 10 at the time of the call, we would expect the overload of Foo that takes a decimal to be called, since the value 10 is not convertible to type string.

What don't you expect?

Let's be a little more specific here, and expand our example to illustrate what we would NOT expect to have happen:

 public class C
{
    public void Foo(decimal x) { ... }
    public void Foo(string x) { ... }
    static void Main(string[] args)
    {
        C c = new D();
        dynamic d = 10;
        c.Foo(d);
    }
}

public class D : C
{
    public void Foo(int x) { ... }
}

First of all, lets notice the subtle change in our source code, highlighted in yellow. We now creating an instance of the derived class D. This means that at runtime, the local variable c will be an instance of type D instead of C as in our previous example. Note also that D contains an overload of Foo that is a better match than all of the overloads on C - the value 10 is intrinsically typed int and so D.Foo is the best match.

However, note that although our example instantiates the local variable c within our code, it is very easy to imagine a method taking a parameter of type C and being given some other derived class at runtime. We do not expect this to change our candidate set used for overload resolution! Specifically (in compiler terminology), since the call to c.Foo can be bound statically to a method group, we expect the statically determined method group to be the one that is used. The dynamic argument should only serve to influence the resolution of the method group, not to influence the creation of the group itself.

What actually happens?

As I mentioned before, one of the design tenets that we've been trying to maintain is that dynamic binding behaves exactly as it would at static compile time, with the exception that the type used in place of the dynamic objects (arguments or receiver) is the runtime determined type instead of the compile time determined one. This means that for all arguments not statically typed dynamic, the compile time types will be used, regardless of their runtime types.

Applying this rule to our example means that at runtime, we should bind as if the type of the receiver c is C, and the type of the argument d is int. Using these types for overload resolution will yield C.Foo(decimal) as the result.

A slightly more complex example to (hopefully) drill home the point:

 public class C
{
    public void Foo(object x, C c) { ... }
    static void Main(string[] args)
    {
        C c = new D();
        dynamic d = 10;
        c.Foo(d, c);
    }
}

public class D : C
{
    public void Foo(int x, D d) { ... }
}

Notice in this example that at runtime, c contains an instance of type D, and d contains the value 10. If we were to use the runtime types for everything involved in the binding at runtime, then the receiver would be type D, with the argument types being int and D respectively. This would yield D.foo(int, D) as the best result, but that's not at all what we would expect.

Because the only statically-known dynamically typed argument is the first argument d, it is the only one that has its runtime type used. The remainder of the arguments to the call (the receiver c, and the second argument c) have their static types used. As such, the only method considered is C.Foo(object, C), which is the method we'd expect to have resolved.

What's next?

Next time we'll look deeper at exactly what the compiler does in order to determine that we need a runtime dispatch for the given expression. After that, we'll apply the same principles we've discussed to other invocation types in our example (such as property-accessor-looking things and operators). By the end of it, we'll have turned this whole dynamic thing inside out, so stay tuned!

kick it on DotNetKicks.com