Gieno Dynamic in C# 4.0, Part 1 |
||
The other day I was playing around with some office code, and I found myself writing a lot of code much like the following sample that Anders used at his PDC talk: static void Main(string[] args) { var xl = new Excel.Application(); ((Excel.Range)xl.Cells[1, 1]).Value2 = "Process Name"; ((Excel.Range)xl.Cells[1, 2]).Value2 = "Memory Usage"; } As you can imagine, it very quickly became tiresome assigning the results of each call to a local variable, debugging and finding out what type it returns for my scenarios, making the cast to the strong type so that I can call certain methods on it, rinse, and repeat. This pattern is common in dynamic APIs, and cause a lot of excess code to be written that essentially is used just to make the type system happy. Wouldn't it be nice if instead, we could write something like this? static void Main(string[] args) { var xl = new Excel.Application(); xl.Cells[1, 1].Value2 = "Process Name"; xl.Cells[1, 2].Value2 = "Memory Usage"; } Well, in C# 4.0, we now allow you to write exactly that. One of the main features that we're working on in C# 4.0 is the dynamic late binding feature. This feature allows you to tell the compiler that the thing that I'm returning really ought to be treated like a dynamic type, and that any dispatch on it should be done dynamically. The runtime will then do the binding for you based on the runtime type of the object instead of the static compile time return type, and if the binding succeeds, then the code will succeed. This gives us exactly what we want. So how does this feature work? Firstly, we've introduced a dynamic type into the type system. This type indicates to the compiler that all operations based on the type should be bound dynamically and not at compile time. Secondly, we've created a C# runtime binder which does the late binding for you. Lastly, we've baked in the usage of the DLR and are making full use of their caching and dynamic dispatch capabilities, so that you can interop with dynamic objects (objects created from Iron Python for instance). The dynamic typeIn order to start using the dynamic binding, we've got to have some way to signify to the compiler that we want our object or expression to be bound dynamically. Enter the dynamic type. The dynamic type is just a regular type that you can use in your code to denote local variables, fields, method return values etc. It tells the compiler that everything to do with that object or expression should be done dynamically. Consider the following example: static void Main(string[] args) { dynamic d = SomeInitializingStatement; d.Foo(1, 2, 3); // (1) d.Prop = 10; // (2) var x = d + 10; // (3) int y = d; // (4) string y = (string)d; // (5) Console.WriteLine(d); // (6) } In this example, each of the statements has some element of the dynamic type flowing through it, and is therefore dispatched dynamically. Lets consider each of them.
There are several other scenarios that dynamic flows out to, but I've listed these to give you a general idea of what the dynamic type's implications are. Now, we should note that the dynamic type is really just syntactic sugar to signify to the compiler that it should treat bindings dynamically. In metadata, dynamic is just object with an attribute signifying its dynamicity (if that's even a word... I don't think it is though!). What happens at compile time?For each dynamic operation, the compiler generates calls into the DLR, and takes advantage of its call sites. The DLR call site takes a set of standard actions which indicate what type of dynamic action we want to take. The C# compiler emits a subclass of these standard actions, annotated with some C# specific details, and emits invocations of the call sites in place of the call that the user makes. For instance, this code sample gets translated into something like the following pseudocode: // This code... static void Main(string[] args) { dynamic d = SomeInitializingStatement; d.Foo(1, 2, d); } // transforms into this code. static void Main(string[] args) { dynamic d = SomeInitializingStatement; _csharpCallAction = new CSharpCallAction("Foo"); _dlrSite<T> = new Site<T>(_csharpCallAction); // Create the site. _dlrSite.Target(1, 2, d); // Invoke the delegate. } Note that the site creation pseudocode specifies a generic argument, T. This argument is a delegate type that represents the signature of the call. So in our example, our call takes 2 integer arguments and a dynamic argument, and has a dynamic receiver. T would then be a delegate that represents that. Invoking that delegate invokes the C# runtime binder, which binds the expression based on the runtime types of the arguments and the receiver. What happens at runtime?When the DLR delegate gets invoked, it does a couple of cool things that I'll describe briefly.
The C# runtime binderThe C# runtime binder uses Reflection to populate its internal symbol table to determine what to bind to. Each of the C# specific actions encodes the type of the binding, along with extra information that allows us to determine how to bind the action. For example, if the argument is known at compile time to have a static type, then that type will be marked in the C# action, and will be used as the type of the argument during runtime binding. If it is known at compile time to be typed dynamic (ie it is a variable of type dynamic, or is an expression that returns dynamic), then the runtime binder will use reflection to determine its runtime type and use that type as the type of the argument. The runtime binder populates its symbol table as needed. For instance, in our example, we were calling the method Foo. The runtime binder will load all members named Foo on the type of the receiver into the symbol table. It then populates the necessary conversions for each of the argument types. Since we may need to coerce the arguments to types that match the method calls (using user-defined conversions as necessary), the binder loads those conversions into the symbol table as well. It then performs overload resolution exactly like the static compiler does. That means that we get the exact same semantics as the static compiler. It also means that we get the same error semantics and messages - a failed binding at runtime results in an exception being thrown, which encapsulates the error message that you would have gotten at compile time. It then takes the result of overload resolution and generates an expression tree that represents the result, and returns that back to the DLR. A summarySo that's a brief summary of what the dynamic pipeline looks like. Of course, I've glossed over a lot of the details, but I'll be covering those details in my future posts. Until next time, some questions to ponder: What happens when the receiver is known statically but the arguments are dynamic? What happens if the methods we're trying to bind against are private? What about operators - how does resolution work on them? As always, happy coding! |
||
![](https://img2024.cnblogs.com/blog/35695/202407/35695-20240713070336838-1837943664.jpg)