The introduction of dynamic objects8 simplified a host of programming scenarios and enabled several new ones previously not available. At its core, programming with dynamic objects enables developers to code operations using a dynamic dispatch mechanism that the runtime will resolve at execution time, rather than having the compiler verify and bind to it at compile time.
Why? Many times, objects are inherently not statically typed. Examples include loading data from an XML/CSV file, a database table, the Internet Explorer DOM, or COM’s IDispatch interface, or calling code in a dynamic language such as an IronPython object. Dynamic object support provides a common solution for talking to runtime environments that don’t necessarily have a compile-time–defined structure. In the initial implementation of dynamic objects, four binding methods are available:
Of these four approaches, we will delve into the first two. The principles underlying them translate seamlessly to the remaining cases—COM interoperability and dynamic language interoperability.
One of the key features of reflection is the ability to dynamically find and invoke a member on a type based on an execution-time identification of the member name or some other quality, such as an attribute (see Listing 18.3). However, dynamic objects provide a simpler way of invoking a member by reflection, assuming compile-time knowledge of the member signature. To reiterate, this restriction states that at compile time we need to know the member name along with the signature (the number of parameters and whether the specified parameters will be type-compatible with the signature). Listing 18.29 (with Output 18.10) provides an example.
In this example, there is no explicit code for determining the object type, finding a particular MemberInfo instance, and then invoking it. Instead, data is declared as type dynamic and methods are called against it directly. At compile time, there is no check of whether the members specified are available or even which type underlies the dynamic object. Hence, it is possible at compile time to make any call, so long as the syntax is valid. At compile time, it is irrelevant whether there is really a corresponding member.
However, type safety is not abandoned altogether. For standard CLR types (such as those used in Listing 18.29), the same type checker normally used at compile time for non-dynamic types is instead invoked at execution time for the dynamic type. Therefore, at execution time, if no such member is available, the call will result in a Microsoft.CSharp.RuntimeBinder.RuntimeBinderException.
Note that this capability is not nearly as flexible as the reflection described earlier in the chapter, although the API is undoubtedly simpler. The key difference when using a dynamic object is that it is necessary to identify the signature at compile time rather than determine things such as the member name at runtime (as we did when parsing the command-line arguments).
Listing 18.29 and the accompanying text reveal several characteristics of the dynamic data type:
The CIL disassembler reveals that within the CIL, the dynamic type is actually a System.Object. In fact, without any invocations, declaration of the dynamic type is indistinguishable from System.Object. However, the difference becomes apparent when invoking a member.
To invoke the member, the compiler declares a variable of type System.Runtime.CompilerServices.CallSite<T>. T varies on the basis of the member signature, but something simple, such as the invocation of ToString(), would require instantiation of the type CallSite<Func<CallSite, object, string>>, along with a method call with parameters of CallSite site, object dynamicTarget, and string result. site is the call site itself, dynamicTarget is the object on which the method call is invoked, and result is the underlying return value from the ToString() method call. Rather than instantiate CallSite<Func<CallSite _site, object dynamicTarget, string result>> directly, a Create() factory method is available for instantiating it; this method takes a parameter of type Microsoft.CSharp.RuntimeBinder.CSharpConvertBinder. Given an instance of the CallSite<T>, the final step involves a call to CallSite<T>.Target() to invoke the actual member.
Under the covers at execution time, the framework uses reflection to look up members and to verify that the signatures match. Next, the runtime builds an expression tree that represents the dynamic expression as defined by the call site. Once the expression tree is compiled, we have a CIL method body that is similar to what the compiler would have generated had the call not been dynamic. This CIL code is then cached in the call site, and the invocation occurs via a delegate. As the CIL is now cached at the call site, the next invocation doesn’t require all the reflection and compilation overhead again.
In addition to reflection, we can define custom types that we invoke dynamically. We might consider using dynamic invocation to retrieve the values of an XML element, for example. Rather than using the strongly typed syntax of Listing 18.30, using dynamic invocation we could call person.FirstName and person.LastName.
Although the code in Listing 18.30 is not overly complex, compare it to Listing 18.31—an alternative approach that uses a dynamically typed object.
The advantages are clear, but does that mean dynamic programming is preferable to static compilation?
In Listing 18.31, we have the same functionality as in Listing 18.30, albeit with one very important difference: Listing 18.30 is entirely statically typed. Thus, at compile time, all types and their member signatures are verified with this approach. Method names are required to match, and all parameters are checked for type compatibility. This is a key feature of C#—and something we have highlighted throughout the book.
In contrast, Listing 18.31 has virtually no statically typed code; the variable person is instead dynamic. As a result, there is no compile-time verification that person has a FirstName or LastName property—or any other members, for that matter. Furthermore, when coding within an IDE, no IntelliSense is available to identify the members on person.
The loss of typing would seem to result in a significant decrease in functionality. Why, then, is such a possibility even available in C#?10
To understand this apparent paradox, let’s reexamine Listing 18.30 . Notice the call to retrieve the "FirstName" element:
Element.Descendants("FirstName").FirstOrDefault().Value
The listing uses a string ("FirstName") to identify the element name, but no compile-time verification occurs to ensure that the string is correct. If the casing was inconsistent with the element name or if there was a space, the compile would still succeed, even though a NullReferenceException would occur with the call to the Value property. Furthermore, the compiler does not attempt to verify that the "LastName" element even exists; if it doesn’t, we would also get the NullReferenceException message. In other words, in spite of all the type-safety advantages, type safety doesn’t offer many benefits when you’re accessing the dynamic data stored within the XML element.
Listing 18.31 is no better than Listing 18.30 when it comes to compile-time verification of the element retrieval. If a case mismatch occurs or if the FirstName element doesn’t exist, an exception would still be thrown.11 However, compare the call to access the first name in Listing 18.31 (person.FirstName) with the corresponding call in Listing 18.30. The call in the latter listing is undoubtedly significantly simpler.
In summary, in some situations type safety doesn’t—and likely can’t—make certain checks. In those cases, code that makes a dynamic call that is verified only at runtime, rather than also being verified at compile time, is significantly more readable and succinct. Obviously, if compile-time verification is possible, statically typed programming is preferred because readable and succinct APIs can accompany it. However, in the cases where it isn’t effective, dynamic capabilities12 enable programmers to write simpler code rather than emphasizing the purity of type safety.
Listing 18.31 included a method call to DynamicXml.Parse(...) that was essentially a factory method call for DynamicXml—a custom type rather than one built into the CLR framework. However, DynamicXml doesn’t implement a FirstName or LastName property. To do so would break the dynamic support for retrieving data from the XML file at execution time rather than fostering compile-time–based implementation of the XML elements. In other words, DynamicXml does not use reflection for accessing its members but rather dynamically binds to the values based on the XML content.
The key to defining a custom dynamic type is implementation of the System.Dynamic.IDynamicMetaObjectProvider interface. Rather than implementing the interface from scratch, however, the preferred approach is to derive the custom dynamic type from System.Dynamic.DynamicObject. This provides default implementations for a host of members and allows you to override the ones that don’t fit. Listing 18.32 shows the full implementation.
The key dynamic implementation methods for this use case are TryGetMember() and TrySetMember() (assuming you want to assign the elements as well). Only these two method implementations are necessary to support the invocation of the dynamic getter and setter properties. Furthermore, the implementations are straightforward. First, they examine the contained XElement, looking for an element with the same name as the binder.Name—the name of the member invoked. If a corresponding XML element exists, the value is retrieved (or set). The return value is set to true if the element exists and false if it doesn’t. A return value of false will immediately cause the runtime to throw a Microsoft.CSharp.RuntimeBinder.RuntimeBinderException at the call site of the dynamic member invocation.
System.Dynamic.DynamicObject supports additional virtual methods if more dynamic invocations are required. Listing 18.33 produces a list of all overridable members.
As Listing 18.33 shows, there are member implementations for everything—from casts and various operations, to index invocations. In addition, there is a method for retrieving all the possible member names: GetDynamicMemberNames().
________________________________________