Friday, May 22, 2009

The Dynamic Keyword in C# 4.0

The new dynamic keyword in C# 4.0 is made possible by including the Dynamic Language Runtime in the .NET framework. The DLR was originally part of the first implementation of IronPython and was abstracted out in order to create a framework for implementing dynamic languages on .NET.

Further than that, the DLR has been used to introduce new features into C# and VB.NET, the static languages traditionally used to program .NET. This is seen as a major part of what is new in .NET 4.0.

When Jim Hugunin went to work at Microsoft he wasn't employed to work specifically on IronPython; he joined the CLR (Common Language Runtime) architecture team with the brief of helping to make .NET a better runtime for dynamic languages. What follows is largely as the result of his influence and work.

The full semantics of the dynamic keyword are laid out in the C# 4.0 language specification document.
Note that this document is for the beta version of .NET 4.0. Although the details are not likely to change much it will inevitably be revised and edited for the final release.
19. Introduction to C# 4.0
The major theme for C# 4.0 is dynamic programming. Increasingly, objects are “dynamic” in the sense that their structure and behavior is not captured by a static type, or at least not one that the compiler knows about when compiling your program. Some examples include
  • Objects from dynamic programming languages, such as Python or Ruby
  • COM objects accessed through IDispatch
  • Ordinary .NET types accessed through reflection
  • Objects with changing structure, such as HTML DOM objects
While C# remains a statically typed language, we aim to vastly improve the interaction with such objects.

Dynamic lookup allows you to write method, operator and indexer calls, property and field accesses, and even object invocations which bypass the normal static binding of C# and instead gets resolved dynamically.
20. Dynamic Binding
Dynamic binding provides a unified approach to selecting operations dynamically. With dynamic binding developer does not need to worry about whether a given object comes from e.g. COM, IronPython, the HTML DOM or reflection; operations can uniformly be applied to it and the runtime will determine what those operations mean for that particular object.

This affords enormous flexibility, and can greatly simplify the source code, but it does come with a significant drawback: Static typing is not maintained for these operations. A dynamic object is assumed at compile time to support any operation, and only at runtime will an error occur if it was not so.

C# 4.0 introduces a new static type called dynamic. When you have an object of type dynamic you can “do things to it” that are resolved only at runtime:
dynamic d = GetDynamicObject(…);
C# allows you to call a method with any name and any arguments on d because it is of type dynamic. At runtime the actual object that d refers to will be examined to determine what it means to “call M with an int” on it.

The type dynamic can be thought of as a special version of the type object, which signals that the object can be used dynamically. It is easy to opt in or out of dynamic behavior: any object can be implicitly converted to dynamic, “suspending belief” until runtime. Conversely, the compiler allows implicit conversion of dynamic expressions to any type:
dynamic d = 7; // statically bound implicit conversion
int i = d; // dynamically bound implicit conversion
Not only method calls, but also field and property accesses, indexer and operator calls and even delegate invocations and constructors can be dispatched dynamically:
dynamic d = GetDynamicObject(…);
d.M(7); // calling methods
d.M(x: “Hello”); // passing arguments by name
d.f = d.P; // getting and setting fields and properties
d[“one”] = d[“two”]; // getting and setting through indexers
int i = d + 3; // calling operators
string s = d(5,7); // invoking as a delegate
C c = new C(d); // selecting constructors
The role of the C# compiler is simply to package up the necessary information about “what is being done to d”, so that the runtime can pick it up and determine what the exact meaning of it is, given an actual object referenced by d. Think of it as deferring part of the compiler’s job to runtime.

The result of most dynamic operations is itself of type dynamic. The exceptions are conversions and constructor invocations, both of which have a natural static type.

At runtime a dynamic operation is resolved according to the nature of its target object d: If d implements the special interface IDynamicObject, d itself is asked to perform the operation. Thus by implementing IDynamicObject a type can completely redefine the meaning of dynamic operations. This is used intensively by dynamic programming languages such as IronPython and IronRuby to implement their own dynamic object models. It can also be used by APIs, e.g. to allow direct access to the object’s dynamic properties using property syntax.

Otherwise d is treated a standard .NET object, and the operation will be resolved using reflection on its type and a C# “runtime binder” component which implements C#’s lookup and overload resolution semantics at runtime. The runtime binder is essentially a part of the C# compiler running as a runtime component to “finish the work” on dynamic operations that was left for it by the static compiler.

Assume the following code:
dynamic d1 = new Foo();
dynamic d2 = new Bar();
string s;
d1.M(s, d2, 3, null);
Because the receiver of the call to M is dynamic, the C# compiler does not try to resolve the meaning of the call. Instead it stashes away information for the runtime about the call. This information (often referred to as the “payload”) is essentially equivalent to:
“Perform an instance method call of M with the following arguments:
  • a string
  • a dynamic
  • a literal int 3
  • a literal object null”
At runtime, assume that the actual type Foo of d1 does not implement IDynamicObject. In this case the C# runtime binder picks up to finish the overload resolution job based on runtime type information, proceeding as follows:
  • Reflection is used to obtain the actual runtime types of the two objects, d1 and d2, that did not have a static type (or rather had the static type dynamic). The result is Foo for d1 and Bar for d2.
  • Method lookup and overload resolution is performed on the type Foo with the call M(string,Bar,3,null) using ordinary C# semantics.
  • If the method is found it is invoked; otherwise a runtime exception is thrown.
20.1 The dynamic type
The grammar is extended with the following type expression:
The types dynamic and object are considered the same, and are semantically equivalent in every way except the following two cases:
  • Expressions of type dynamic can cause dynamic binding to occur in specific situations
  • Type inference algorithms as described in §7.4 will prefer dynamic over object if both are candidates
This deep equivalence means for instance that:
  • There is an implicit identity conversion between object and dynamic
  • There is an implicit identity conversion between constructed types that differ only by dynamic versus object
  • Method signatures that differ only by dynamic versus object are considered the same
  • Like with object, there is an implicit conversion from every type (other than pointer types) to dynamic and an explicit conversion from dynamic to every such type.
The type dynamic does not have a separate runtime representation from object – for instance the expression
typeof(dynamic) == typeof(object)
is true.

An expression of the type dynamic is referred to as a dynamic expression.

20.2 Dynamic binding
The purpose of the dynamic type is to affect the way operations are selected by the compiler. The process of selecting which operation to apply based on the types of constituent expressions is referred to as binding.

The following operations in C# are selected based on some form of binding:
  • Member access: e.M
  • Method invocation: e.M(e1,…,en)
  • Delegate invocaton: e(e1,…,en)
  • Element access: e[e1,…,en]
  • Constructor calls: new C(e1,…,en)
  • Overloaded unary operators: +, -, !, ~, ++, --, true, false
  • Overloaded binary operators: +, -, *, /, %, &, &&, |, ||, ??, ^, <<, >>, ==,!=, >, <, >=, <=
  • Compound assignment operators: +=, -=, etc.
  • Implicit and explicit conversions
When dynamic expressions are not involved, C# always defaults to static binding, which means that the compile-time types of constituent expressions are used in the selection process. However, when one of the constituent expressions in the above listed operations is a dynamic expression, the operation is instead dynamically bound.

20.3 Compile time semantics of dynamic binding
An anonymous function cannot be used as a constituent value of a dynamically bound operation.

A method group can only be a constituent value of a dynamically bound operation if it is immediately invoked.

Other than that, a dynamically bound operation always succeeds at compile time, unless otherwise specified in the following.

The static result type of most dynamically bound operations is dynamic. The only exceptions are:
  • Conversions, which has the static type that is being converted to
  • Constructor invocations which have the static type that is being constructed
Most dynamically bound operations are classified as a value. The only exceptions are member accesses and element accesses, which are classified as variables. However, they can not be used as arguments to ref or out parameters.

20.3.1 Static binding with dynamic arguments
In certain cases if enough is known statically, the above operations will not lead to dynamic binding. This is the case for:
  • Element accesses where the static type of the receiver is an array type
  • Delegate invocations where the static type of the delegate is a delegate type
In these cases the operation is resolved statically, instead implicitly converting dynamic arguments to their required type. Thus, the result type of such operations is statically known.

20.3.2 Dynamic binding with a statically known candidate set
For most dynamically bound operations the set of possible candidates for resolution is unknown at compile time. In certain cases, however the candidate set is known:
  • Static method calls with dynamic arguments
  • Instance method calls where the static type of the receiver is not dynamic
  • Indexer calls where the static type of the receiver is not dynamic
  • Constructor calls
In these cases a limited compile time check is performed for each candidate to see if any of them could possibly apply at runtime. This check includes:
  • Checking that the candidate has the right name and arity
  • Performing a partial type inference to check that inferences do exist where not depending on arguments with the static type dynamic
  • Checking that any arguments not statically typed as dynamic match parameter types that are known at compile time
If no candidate passes this test, a compile time error occurs.
Extension methods are not considered candidates for instance method calls, because they are not available during runtime binding.

20.3.3 Conversion to interface types
In C# user defined conversions to interface types are not allowed. For performance purposes conversions of dynamic expressions to interface types are therefore statically rather than dynamically bound. This does have a slight semantic effect, because a dynamic object (i.e. an object whose runtime type implements IDynamicObject) could have given other meaning to the conversion, had it been dynamically bound.

In foreach and using statements (§20.3.4 and §20.3.5), expansions may do dynamic casts to interfaces even though that cannot be done directly from source code.

20.3.4 Dynamic collections in foreach statements
If the collection expression of a foreach statement (§8.8.4) has the static type dynamic, then the collection type is System.IEnumerable, the enumerator type is the interface System.Collections.IEnumerator, and the element type is object.

Furthermore the cast that is part of the foreach expansion is bound dynamically, not statically as is otherwise the case for interface types (§20.3.3). This enables dynamic objects to participate in a foreach loop even if they do not implement the IEnumerable interface directly.

20.3.5 Dynamic resources in using statements
A using statement (§8.13) is allowed to have a dynamic expression as the resource acquisition. More specifically, if the form of resource-acquisition is expression and the type of the expression is dynamic, or if the form of resource-acquisition is local-variable-declaration and the type of the local-variable-declaration is dynamic, then the using statement is allowed.

In this case, the conversion of the expression or local variables to IDisposible occurs before the body of the using statement is executed, to ensure that the conversion does in fact succeed. Furthermore the conversion is bound dynamically, not statically as is otherwise the case for conversion of dynamic to interface types (§20.3.3). This enables dynamic objects to participate in a using statement even if they do not implement the IDisposable interface directly.

A using statement of the form
using (expression) statement
where the type of expression is dynamic, is expanded as:
{ IDisposable __d = (IDisposable)expression // as a dynamic cast
try {
finally {
if (__d != null) __d.Dispose();
A using statement of the form
using (dynamic resource = expression) statement
is expanded as:
dynamic resource = expression;
IDisposable __d = (IDisposable)resource // as a dynamic cast
try {
finally {
if (__d != null) __d.Dispose();
In either expansion, the resource variable is read-only and the __d variable is invisible in the embedded statement. Also, as already mentioned, the cast to IDisposable is bound dynamically.

20.3.6 Compound operators
Compound operators x binop= y are bound as if expanded to the form x = x binop y, but both the binop and assignment operations, if bound dynamically, are specially marked as coming from a compound assignment.

At runtime if all the following are true:
  • x is of the form d.X where d is of type dynamic
  • the runtime type of d declares X to have type T or T? where T is a primitive type
  • the result of x binop y has the runtime type S where S is a primitive type
  • S and T are either both integral types (sbyte, byte, short, ushort, int, uint, long or ulong) or both floating point types (float or double)
  • S is implicitly convertible to T
Then the result of x binop y is explicitly converted to T before being assigned. This is to mimic the corresponding behavior of statically bound compound assignments, which will explicitly convert the result of a primitive binop to the type of the left hand side variable (§7.16.2).
For dynamically bound += and -= the compiler will emit a call to check dynamically whether the left hand side of the += or -= operator is an event. If so, the dynamic operation will be resolved as an event subscription or unsubscription instead of through the expansion above.

20.4 Runtime semantics of dynamic binding
Unless specified otherwise in the following, dynamic binding is performed at runtime and generally proceeds as follows:
  • If the receiver is a dynamic object – i.e., implements an implementation-specific interface that we shall refer to as IDynamicObject – the object itself programmatically defines the resolution of the operations performed on it.
  • Otherwise the operation gets resolved at runtime in the same way as it would have at compile time, using the runtime type of any constituent value statically typed as dynamic and the compile time type of any other constituent value.
    • If a constituent value derives from a literal, the dynamic binding is able to take that into account. For instance, some conversions are available only on literals.
    • If a constituent value of static type dynamic has the runtime value null, it will be treated as if the literal null was used.
    • Extension method invocations will not be considered – the set of available extension methods at the site of the call is not preserved for the runtime binding to use.
  • If the runtime binding of the operation succeeds, the operation is immediately performed,otherwise a runtime error occurs.
In reality the runtime binder will make heavy use of caching techniques in order to avoid the performance overhead of binding on each call. However, the observed behavior is the same as described here.

The rest of the document covers the other features in C# 4.0: Named and Optional Arguments, COM Interoperability, and Co- and Contravariance. Particularly COM Interoperability also makes use of dynamic:
COM methods are often designed for a language environment that is more dynamic than C#. This means that they will often return weakly typed results, and rely on the calling language to dynamically look up further operations on those results.

No comments:

Post a Comment

Note: only a member of this blog may post a comment.