C# "dynamic," Part IV
Today, let's geek out about the language design regarding the dynamic type.
Type in the language vs. Type in the runtime
One thing that lots of people already know is that when you say "dynamic" in code, you can think of it as "System.Object" in your assembly, because that's how the compiler emits it.
dynamic d = 2008; // A lot like "object d = 2008;" List<dynamic> x = new List<dynamic>(); // ...like "List<object>"
There is no "System.Dynamic" type. However, despite that there is no runtime type that corresponds to the thing you just typed, dynamic is an honest-to-goodness type as far as the language is concerned, although it behaves a little weirdly.
Note that this is different from "var." When you say "var" in C# source code in place of a real type, the compiler infers the type and declares the local variable with the appropriate type, which could be anything. It's a shorthand for local variable definitions. You cannot have List<var>s or var expressions. You cannot derive from something with a var in it, or have a var field. Var is a feature that makes local variable declaration statements better. Dynamic is not.
How do you think of types?
There are lots of relationships between types in C#, and the most important two that I can think of are the inheritance relationships and the convertibility relationships. And it is in those relationships where dynamic starts to diverge from the way you're used to thinking of types. I'm about to leave out a lot of detail and edge cases, so bear with me.
I think of the inheritance relationship like this: There is a big inheritance tree, and its root is System.Object. Pretty much all types live there, except for interface types. Interface types have their own inheritance relationships that form a lattice with no root. And furthermore, the tree of non-interface types is bound to the lattice of interface types in various locations whenever a type implements an interface. You can build families of types by parameterizing them (generics) but that doesn't screw up this model so just forget about it for now.
That's kind of abstract. Here's a picture. The blue diagram shows you the "real" object types, and the green diagram shows you the interfaces. Most things live in the blue diagram. Objects, arrays, delegates, enums, numeric types, all that.
The things at the heads of arrows are the base types, and at the tail they're the subtypes. I haven't drawn the arrows from blue things to green things.
Ok, what about convertibility (sometimes, "compatibility")? Well, convertibility is a separate relationship that is informed by the picture above, but it is not the same thing. I don't think of convertibility as a picture at all. I think of it as a list of rules. And actually, there are two kinds of conversions: explicit and implicit. All implicit conversions are also explicit conversions.
How do those rules look? Well, here's one of them: There exists an implicit conversion from S to T if T is a base type of S. In other words, the arrows above are also implicit conversions.
Base myBase = myDerived;
Here's another: There exists an explicit conversion from T to S it T is a base type of S.
Derived myDerived = (Base)myBase;
And another: There is an implicit conversion from int to double.
double myDouble = myInt;
And: There is an implicit conversion from S to T if the user defined a special static implicit conversion method in the definition of S or T.
string s = myThingWithUserDefinedConversionToString;
There are lots of these rules, and some of them are given special names because they have certain characteristics, such as "boxing," "numeric," "reference," "user-defined," etc. I said I don't think of these as a picture because if I had to draw a picture like the above, there would simply be too many arrows for it to be helpful.
The difference between the explicit conversion and implicit conversions at compile time is that you must specify a cast to invoke an explicit conversion. The difference at runtime is that an explicit conversion may fail, but an implicit conversions should not. This last distinguishing characteristic sounds like something that's going to have to be relaxed a bit for dynamic.
What about dynamic?
So none of this so far is new. But here's the question: if dynamic is really a type, then where does it sit in the above inheritance picture, and what rules got added to the (long) list of conversion rules?
It's a good question. Let's answer the first part with a picture:
That was easy. The type dynamic stands alone. It does not derive from anything, even object. It is a unique, isolated, stand-alone weirdo. And you cannot introduce types that derive from it, either. In that sense, it's not very much like object at all!
The conversions are, though. There is an implicit conversion from just about everything to object, and they are more than just the "implicit reference conversions" that come right out of the arrows in the hierarchy tree. There are also conversions from interfaces to object, and value types to object, and the degenerate identity conversion from object to object. We "copy" all of these conversions to the dynamic world:
- There is an implicit identity conversion from dynamic to dynamic.
- There is an implicit reference conversion from any other reference type to dynamic.
- There is an implicit boxing conversion from any value type to dynamic.
Great! So we got back all those conversions that we "lost" due to the fact that dynamic sits outside of the usual inheritance picture. What about explicit conversions?
- There is an explicit reference conversion from dynamic to any other reference type.
- There is an explicit unboxing conversion from dynamic to any value type.
Wow! That's everything, right? We more or less get dynamic to behave just like object with respect to its conversions by adding these new rules. Maybe that's what Anders means when he says that dynamic can be though of as a peer to object. Did we screw up anything? What's happening here that's unexpected? Well, This all seems pretty straight-forward, but it doesn't get us everything we want.
[Update March 31, 2010: THIS HAS CHANGED SINCE PRE-RELEASE VERSION OF DYNAMIC. SEE THIS POST]
The first weird thing: there is no implicit conversion from dynamic to object. Why not? Well, there are no implicit conversions mentioned above from dynamic to anything! Except the identity conversion of course. Why did we do that? Well, it'll come out in the wash as I explain more, but the short story is that if there were implicit conversions from object to dynamic and back again, it would be very hard to distinguish them from one another when we perform operations that expect one or the other. So we cut the chain. But wait a minute, in the CTP, you can type this:
dynamic d = null; object o = d;
And it compiles. That's because it's not an implicit conversion! More next time: what kind of conversion is that? What other new crazy conversions have we had to add? What about signature matching, and constraints? How does overload resolution work?