C# "dynamic," Part V

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

Let's look at this:

 dynamic d = null;
object o = d; // not an implicit conversion

Last time, I said that the conversion on the second line is not an implicit conversion. If you know C# already, that sounds preposterous. Just look at it; of course it's an implicit conversion! It's not though. It's called an "assignment conversion."

Assignment Conversions

Assignment conversions are a new third kind of conversion, between explicit and implicit conversions. Every assignment conversion is also an explicit conversion, and every implicit conversion is also an assignment conversion.

ConversionsVenn

Why would we have introduced such a thing? Let's talk about the path that got us here. Initially, we considered having implicit conversions from dynamic to everything. That way you could always assign a dynamic value to any variable, or pass it to any method, and it would just work out. That's good. We want that. Why? Well, dynamic lets you work without type information, which means you that you don't need to insert casts everywhere, and if we required casts just to get back to the typed world (i.e., assign back to some typed local), we wouldn't have saved you too much typing or made your code look very much better.

There are better reasons not to have implicit casts from dynamic to everything, though. Recall that we do have implicit conversions from everything to dynamic. Maybe you can see what would have gone wrong with this scheme: imagine that we also wanted these simple implicit conversions to be transitive, and all the sudden there would have been implicit conversions from any type to any other type, "through" dynamic. It would be mayhem.

And even without transitivity, we'd face a problem. If dynamic were implicitly convertible to any type, then what happens during overload resolution?

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

When calling M on the last line, or any time you are converting a dynamic to some parameter type, what should you do? If there is an implicit conversion from dynamic to int and also an implicit conversion from dynamic to string, then what we have here is an ambiguity error at compile time. Even if there were some cases where we could say that one is better than the other (perhaps dynamic to object is better than dynamic to int), there are plenty of cases where we clearly couldn't. And if all these cases are ambiguities, then again, we've forced you to write a conversion to effectively pick one of these overloads by hand, which is not cool.

So what is an assignment conversion then?

  1. There is an assignment reference conversion from dynamic to any other reference type.
  2. There is an assignment unboxing conversion from dynamic to any value type.

As a matter of fact, these are exactly the explicit conversions I mentioned from dynamic last time. So forget that I said they were explicit conversions. They're actually assignment conversions! And in fact, these conversions together with the implicit conversions are all of the assignment conversions.

When?

We've seen that assignment is one place where the compiler will insert an assignment conversion:

 dynamic d = GetSomeD();
Worksheet ws = d; // assignment conversion

But that's not all! The C# 4 spec changes in such a way that quite a few "assignment-like things" now utilize assignment conversions. Returns, and yield returns do:

 return d; // assign. conversion to return type
yield return d; // assign. conversion to iterator type

Property and indexers:

 foo.Prop = d; // assign. conversion to Prop's type
foo[1] = d; // assign. conversion to indexer's type

And a few other less frequent cases, like array initializers (among some that I am surely forgetting right now):

 bool[] ba = new bool[] { true, d }; // assign. conversion

There are also some kind of one-off special cases where a statement form uses an assignment conversion, such as foreach

 foreach (var x in d) {} // assign. conversion to IEnumerable

or

 using (d) {} // assign. conversion to IDisposable

Wait, what about overload resolution?

One of constructs that does not use assignment conversions is overload resolution, for the reason cited above. If arguments to method calls were to be converted using assignment conversions, then we'd be in the mess where pretty much every time you passed a dynamic to some method you'd have an ambiguity.

However, now it would seem that we have the opposite problem. This snippet:

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

won't compile for entirely different reasons! There's no conversion from d, the argument in the final call, to any type that is an actual parameter on an M in C. Obviously, we need this to compile. You can see, in the CTP, that it does. In order to accomplish that, we changed the rules for overload resolution.

Before I tell you how those rules changed (next time), try a little thought experiment: what is everything we know about M at the call site in the last line, and what are the different strategies for emitting code that calls one of the overloads? What does the programmer expect?

And one last thing. This compiles in the CTP:

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

What kind of conversion is that? I'll give you a hint. It's not covariant.

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