C# "dynamic," Part VI

[Update March 31, 2010: MUCH OF THIS HAS CHANGED SINCE PRE-RELEASE VERSION OF DYNAMIC. SEE THIS POST]

We left off last time with this piece of code

 public class C
{
  public static void M(int i) { }
  public static void M(string s) { }
}
...
dynamic d = GetSomeD();
C.M(d);

...together with some question about what the compiler could possibly do on the last line, when M is called. The answer is, of course, the compiler can't do much. If it knew what type was really behind d, then it could insert an assignment conversion and call the correct M. But the point of "dynamic" is that the compiler has no idea whatsoever what type is back there. The best it can do is defer its work until it does know what types are there--wait until runtime!

Overload resolution deferred

Overload resolution in C# is a bit complicated. I tend to think of C# method calls as more or less just depending on the target object and the name. But that's the naive view; since the language allows overloading methods on their names, we actually get into quite a mess trying to figure out which one to call. By the time you throw in generic methods with type inference, extension methods, params arrays, and now named and optional parameters, there's actually quite a bit in the language specification that says how you're supposed to sort all this out.

And since we can't decide which M to call in the example above, it just got more complicated. How do we know when to defer until runtime?

First, we don't defer if we don't need to. If dynamic is not involved at all, then of course we do what we've always done and nothing changes. Another case where we don't need to do anything is if dynamic is involved, but the old overload resolution rules give you an answer. This can happen, for instance, if you have dynamic as a formal parameter of one of the overloads, such as:

 public class C
{
  public static void M(int i) { }
  public static void M(dynamic d) { }
}
...
dynamic d = GetSomeD();
C.M(d);

In this case, the compiler will resolve to the second M, since the conversions work out. The trick is that there is an implicit conversion from dynamic to itself.

If none of the overloads that we look at actually work, then we need to try something else. It goes like this: while you are looking for methods to test, if ever you see one that fails only because there was a dynamic argument in one position and no conversion, then just "pretend" that there exists a method that takes dynamic everywhere and returns dynamic. So in this case, the pretend method would look like:

 public static dynamic M(dynamic d) { } // not real

But be careful--don't add the pretend method if there is already an existing real method that matches its signature (as in the second example). Also, ref and out parameters are a little special, so if you see them you need to alter the shape of your pretend method a little, but that's not too interesting.

So then what? Well, at the end of overload resolution, when you have all your candidate methods in hand, you compare them to see which is better as usual (this is another complicated part of the spec that involves testing arity and conversions and such), and if it turns out that you pick the "pretend" method, then that means you need to defer overload resolution until runtime.

And the way the compiler indicates this deferral is it makes a CallSite to ask the runtime binder to do its thing, almost as if you had called any old method on a dynamic target. The target is not dynamic, though, if there even is one! For instance, notice that in the examples above, the methods are static. So what we've done is produced a dynamic call site for a static method. I think that's weird, but there's a few legit examples that come up all the time, such as Math.Sign, or Math.Abs that each have a variety of overloads for different numeric types.

Incidentally, while we were designing this feature, we called the pretend method "the phantom method." I am not sure if that name will stick in the spec.

Some more conversions

So the deferred overload resolution as well as the assignment conversions are two different strategies that arose in an effort to get "dynamic" to work just as you expect it. But there are some other changes that make the dynamic type a little weird.

A couple posts ago, I talked about how dynamic doesn't really exist in the framework, that it's just a compiler construct (although from the perspective of the compiler it is a real type). Well, if that's the case, then whenever I have, say, a List<dynamic>, I should be able to store it in a space reserved for a List<object>, and vice-versa, right?

 List<dynamic> ld = GetAList();
List<object> lo = ld;

Of course I should! How do I do that? Well, we added more conversions. First, I need to define a mapping of types. For a given T, let red(T) ("T reduced") be the type T except with all the occurrences of dynamic replaced by object. So red(Dictionary<dynamic, object>) == Dictionary<object,object>. We added the following conversions.

  1. If there is an implicit reference conversion from red(S) to red(T), then there is also an implicit reference conversion from S to T, except in the case where S is dynamic and T is object.
  2. If there is an implicit reference conversion from S to T and from T to U, and one of them is a conversion added by rule 1, then there is an implicit conversion from S to U.
  3. If there is an explicit reference conversion from red(S) to red(T), then there is also an explicit reference conversion from S to T.

I call these conversions "structural conversions," since they exist only because they preserve the runtime structure of the objects being converted. This is probably not the language that you will see in the final C# 4 specification since this area is not 100% baked yet.

There's more weirdness for next time, too! Can you implement IEnumerable<dynamic> or derive from Base<dynamic>? Should you be able to? What does each of those things mean?

Previous posts in this series: Part V, Part IV, Part III, Part II, Part I