In my previous article, I spoke about DynamicMethod. Today I would like to talk briefly about dynamic objects. Despite the similarity in name, these are completely different topics.
It is a simple keyword really – “dynamic”. So innocent. So effortless to type. But this new keyword hides some powerful mojo. If you happen to be working with code that integrates with the DLR (Dynamic Language Runtime), then you have likely already encountered this keyword in C#. And even if you aren’t working with the DLR you might find some good use for this new and fascinating language feature.
Among other things, C# 4.0 introduced the “dynamic” type declaration. On the surface it resembles the “var” keyword, and from the standpoint of compiler magic that is a good comparison (both var and dynamic are language features, and use of the keywords cause the compiler to do special things). However from the CLR platform perspective they are very different. To illustrate, let’s explore a simple example and inspect the resulting MSIL that gets compiled:
- static void Main(string[] args)
- {
- object ObjVariable = “Hello, World!”;
- var VarVariable = “Hello, World!”;
- dynamic DynVariable = “Hello, World!”;
- }
- .method private hidebysig static void Main(string[] args) cil managed
- {
- .entrypoint
- // Code size 20 (0x14)
- .maxstack 1
- .locals init ([0] object ObjVariable,
- [1] string VarVariable,
- [2] object DynVariable)
- IL_0000: nop
- IL_0001: ldstr “Hello, World!”
- IL_0006: stloc.0
- IL_0007: ldstr “Hello, World!”
- IL_000c: stloc.1
- IL_000d: ldstr “Hello, World!”
- IL_0012: stloc.2
- IL_0013: ret
- } // end of method Program::Main
Here we can see that for the “var” declaration, the C# compiler figured out that VarVariable would always be a string based on the fact that we assigned it a string literal. So when compiling to MSIL, the C# compiler simply replaced “var VarVariable” with “string VarVariable”. That is about all there is to the compiler magic behind the “var” keyword.
However when we look at the “dynamic” declaration, we can clearly see that something mysterious is afoot. The compiler treated our “dynamic” as “object”, even though we assigned it a literal string value. So far, this hasn’t really provided any benefit. We could have declared it as “object DynVariable” and achieved the same result. But clearly there is a difference, because otherwise the language would not have introduced a new (longer) keyword if there were not some purpose behind it.
Let us begin by exploring what we can do with a “dynamic” object versus a plain-vanilla “object”. Revisiting our code from above, let’s see if DynVariable offers any additional methods or properties. In the code editor we type “DynVariable.” and expect to see an Intelliprompt session displayed with a listing of the legitimate members that we can invoke. Instead, we see this:
Making sure Visual Studio isn’t broken, we take a look at the Intelliprompt session list for ObjVariable, and sure enough Intelliprompt is working just fine:
The reason for this is that when you declare something as “dynamic”, the compiler will no longer make any claims about the type of the actual object instance that is being referred to. It no longer “knows” the target type. And if it doesn’t know the type of an object, it certainly can’t help you by displaying a listing of that type’s members.
But just because the compiler doesn’t know the type or the members exposed by that type, doesn’t mean that we don’t either. In fact, we just assigned a string to DynVariable, so we know for a fact that we can, for example, use it in place of a normal string. And pretty much the simplest thing we can do with a normal string is assign it to a string variable:
- string StrVariable = DynVariable;
Innocent enough. We need to take a peek at the MSIL for this though. The compiler makes no claims about the type of DynVariable, yet it just allowed us to assign it to a string local variable. On the surface this seems to defy the rules of “strong typing” that the CLR in general, and C# in particular, are known for. So how did it pull this off?
- .method private hidebysig static void Main(string[] args) cil managed
- {
- .entrypoint
- // Code size 87 (0x57)
- .maxstack 3
- .locals init ([0] object ObjVariable,
- [1] string VarVariable,
- [2] object DynVariable,
- [3] string StrVariable)
- IL_0000: nop
- IL_0001: ldstr “Hello, World!”
- IL_0006: stloc.0
- IL_0007: ldstr “Hello, World!”
- IL_000c: stloc.1
- IL_000d: ldstr “Hello, World!”
- IL_0012: stloc.2
- IL_0013: ldsfld class [System.Core]System.Runtime.CompilerServices.CallSite`1<class [mscorlib]System.Func`3<class [System.Core]System.Runtime.CompilerServices.CallSite,object,string>> ConsoleApplication2.Program/'<Main>o__SiteContainer0′::'<>p__Site1′
- IL_0018: brtrue.s IL_0040
- IL_001a: ldc.i4.0
- IL_001b: ldtoken [mscorlib]System.String
- IL_0020: call class [mscorlib]System.Type [mscorlib]System.Type::GetTypeFromHandle(valuetype [mscorlib]System.RuntimeTypeHandle)
- IL_0025: ldtoken ConsoleApplication2.Program
- IL_002a: call class [mscorlib]System.Type [mscorlib]System.Type::GetTypeFromHandle(valuetype [mscorlib]System.RuntimeTypeHandle)
- IL_002f: call class [System.Core]System.Runtime.CompilerServices.CallSiteBinder [Microsoft.CSharp]Microsoft.CSharp.RuntimeBinder.Binder::Convert(valuetype [Microsoft.CSharp]Microsoft.CSharp.RuntimeBinder.CSharpBinderFlags,
- class [mscorlib]System.Type,
- class [mscorlib]System.Type)
- IL_0034: call class [System.Core]System.Runtime.CompilerServices.CallSite`1<!0> class [System.Core]System.Runtime.CompilerServices.CallSite`1<class [mscorlib]System.Func`3<class [System.Core]System.Runtime.CompilerServices.CallSite,object,string>>::Create(class [System.Core]System.Runtime.CompilerServices.CallSiteBinder)
- IL_0039: stsfld class [System.Core]System.Runtime.CompilerServices.CallSite`1<class [mscorlib]System.Func`3<class [System.Core]System.Runtime.CompilerServices.CallSite,object,string>> ConsoleApplication2.Program/'<Main>o__SiteContainer0′::'<>p__Site1′
- IL_003e: br.s IL_0040
- IL_0040: ldsfld class [System.Core]System.Runtime.CompilerServices.CallSite`1<class [mscorlib]System.Func`3<class [System.Core]System.Runtime.CompilerServices.CallSite,object,string>> ConsoleApplication2.Program/'<Main>o__SiteContainer0′::'<>p__Site1′
- IL_0045: ldfld !0 class [System.Core]System.Runtime.CompilerServices.CallSite`1<class [mscorlib]System.Func`3<class [System.Core]System.Runtime.CompilerServices.CallSite,object,string>>::Target
- IL_004a: ldsfld class [System.Core]System.Runtime.CompilerServices.CallSite`1<class [mscorlib]System.Func`3<class [System.Core]System.Runtime.CompilerServices.CallSite,object,string>> ConsoleApplication2.Program/'<Main>o__SiteContainer0′::'<>p__Site1′
- IL_004f: ldloc.2
- IL_0050: callvirt instance !2 class [mscorlib]System.Func`3<class [System.Core]System.Runtime.CompilerServices.CallSite,object,string>::Invoke(!0,
- !1)
- IL_0055: stloc.3
- IL_0056: ret
- } // end of method Program::Main
</div> </div> <p>Oh dear, that greatly increased the amount of code being executed!</p> <p>So what is all of this CallSite funny business? Call Sites are the “compiler magic” that backs up the “dynamic” keyword in C#. The CallSite concept is not terribly difficult to explain, but it might be a little easier if we take a step back first and evolve our example just a little further. What if we add the following line of code to our example?</p>
- int IntVariable = DynVariable;
</div> </div> <p>Surely this cannot work. We have a string in DynVariable. And sure enough, while it in fact compiles, it throws a runtime exception (RuntimeBinderException) upon execution:</p> <p><a href=”https://training.atmosera.com/devcenter/media/Default/Blogs/Images/krome/image_77bd40d3.png”><img style=”border-right-width:0px;padding-left:0px;padding-right:0px;border-top-width:0px;border-bottom-width:0px;border-left-width:0px;padding-top:0px” title=”image” border=”0″ alt=”image” src=”https://training.atmosera.com/devcenter/media/Default/Blogs/Images/krome/image_thumb_7e041761.png” width=”554″ height=”224″></a></p> <p>This exception was thrown because our program <em>deferred</em> the type check on our assignment to IntVariable until runtime. In fact, we could have done this ourselves too by doing the following:</p>
- if (DynVariable == null || typeof(int).IsAssignableFrom(((object)DynVariable).GetType()) == false)
- throw new Microsoft.CSharp.RuntimeBinder.RuntimeBinderException(“Cannot implicitly convert type ‘int’ to ‘string’.”);
- int IntVariable = (int)DynVariable;
</div> </div> <p>Here we are first verifying that the actual object contained within DynVariable is compatible with the variable named IntVariable. Since IntVariable is an integer (value type), this means it cannot be null as well.</p> <p><em>Note that since we are also typecasting directly to an int in the last line of code, we no longer need to declare our variable as “dynamic”. We could easily change our code now to use “object” as the type.</em></p> <p>We can logically split this code into two parts. The first part is the “if” statement that verifies type compatibility. The second part is the actual value assignment and type casting. For discussion’s sake, we can call these the “test” and the “action”. If the “test” passes, then we perform the “action”.</p> <p>Now, what if our code is a slightly more complicated than just assigning a second variable. Suppose we want to do the following:</p>
- object ObjCombined = DynVariable + DynVariable;
</div> </div> <p>Now this is certainly an interesting bit of code. Since DynVariable is declared as dynamic, the compiler assumes nothing about the members it might support. And with this line of code, we are telling the compiler “trust me, there is a + operator that will work here”. What really makes it interesting though is that there is a + operator defined for both strings <em>and</em> integers (and other data types as well!).</p> <p>Going back to the notion of a separate “test” and “action”, we can codify correct behavior for all of these cases fairly easily. We can find all + operators available within the current scope, and for each of these write a “test” to check for type compatibility with the operands and also an “action” which passes the left and right operands to the appropriate operator function (in a strongly-typed fashion).</p> <p>Some of our test and action methods might look like this if we were to support both string and integer values:</p>
- static bool CheckOperandTypeCompatibility<TOperand>(object left, object right)
- {
- if (left == null || typeof(TOperand).IsAssignableFrom(left.GetType()) == false)
- return false;
- if (right == null || typeof(TOperand).IsAssignableFrom(right.GetType()) == false)
- return false;
- return true;
- }
- static string ApplyOperator(string left, string right)
- {
- return left + right;
- }
- static int ApplyOperator(int left, int right)
- {
- return left + right;
- }
</div> </div> <p>And we could execute the appropriate tests and corresponding actions accordingly:</p>
- if (CheckOperandTypeCompatibility<int>(DynVariable, DynVariable))
- ObjCombined = ApplyOperator((int)DynVariable, (int)DynVariable);
- if (CheckOperandTypeCompatibility<string>(DynVariable, DynVariable))
- ObjCombined = ApplyOperator((string)DynVariable, (string)DynVariable);
</div> </div> <p>But this would get out of hand rather quickly. Our next step would be to wrap these test/action pairs into a list, and then use that list to drive the test and action execution:</p>
- List<Tuple<Func<object, object, bool>, Action<object, object, object>>> OperatorTests
- = new List<Tuple<Func<object, object, bool>, Action<object, object, object>>>();
- // add our test/action pairs to OperatorTests for each operator we want to support
- foreach (var test in OperatorTests)
- if (test.Item1(DynVariable, DynVariable))
- {
- test.Item2(ObjCombined, DynVariable, DynVariable);
- break;
- }
</div> </div> <p>We would probably use lambda expressions when populating the list of tests/actions. But that would still get fairly unwieldy in a hurry. We just learned about DynamicMethod in my <a href=”https://training.atmosera.com/devcenter/krome/getting-to-know-dynamicmethod”>previous blog topic</a>, so it makes natural sense to generate these test/action delegate pairs using DynamicMethod objects that we create on the fly when we see a new set of types, and add that generated pair to our list. So DynamicMethod and the “dynamic” keyword are related to each other after all!</p> <p>So at this point you might be wondering who in their right mind would want to write this kind of code? And just to avoid having to know what type we are working with from the outset?</p> <p>And you would be right about that. <u>The good thing is that we don’t have to write this stuff.</u> In fact, through the above code we have more or less reinvented the core idea behind a CallSite. A real CallSite however is much smarter than ours. It can handle far more complex scenarios than simple binary operator functions. It will also create (and cache) the tests and actions on-demand, so if you pass a string one time and the runtime type binding fails, it will simply throw the exception right away if another string is passed, instead of trying to find the right member again (and failing).</p> <p>Furthermore, we don’t really have to even know about CallSites in most cases. When we use the “dynamic” keyword, the C# compiler will automatically create CallSites in the compiled MSIL whenever you attempt to invoke members or pass the value around your application as seen in our first example above. All of the dirty details are hidden away from view (unless your view happens to include using ILDASM). This might all seem like the dynamic keyword violates the laws of type-safety, but once you understand the mechanics of CallSites, you will realize that in fact it does not violate them at all.</p> <p>But it certainly pulls off a cool magic trick.
Always Here For You – 24x7x365
Atmosera has hired and developed an industry-leading team of skilled engineers, architects, technicians, systems specialists and project managers.
We offer a wealth of real world experience and proven best practices.
We are also committed to staying abreast of the latest technology vendors and offerings.
System Status and Outages
Date
All Green
Service Impacted
All Systems and Services are Fully Operational
Comments
Stay Connnected
Maintenance notices, newsletter, news flash, events, and email preferences.
Access Web Help Desk
Web Help Desk, or WHD for short, is a web-based application built on the SolarWinds Web Help Desk software. If you are an authorized WHD user, use your credentials to login.