Page 49 - MSDN Magazine, September 2019
P. 49

have a method that builds the columns based on subexpressions in an array literal (using Visual Basic):
grid.SetColumns(Function(x As Person) {x.LastName, x.FirstName, x.DateOfBirth})
Or from the subexpressions in an anonymous type (in C#):
grid.SetColumns((Person x) => new {x.LastName, x.FirstName, DOB = x.DateOfBirth});
Using Expression Trees II:
Compiling Invocable Code at Runtime
The second major use case for expression trees is to generate executable code at runtime. Remember the body variable from before? You can wrap it in a LambdaExpression, compile it into a delegate, and invoke the delegate, all at runtime. The code would
look like this in Visual Basic:
Dim lambdaExpression = Lambda(Of Action)(body) Dim compiled = lambdaExpression.Compile compiled.Invoke
' prints either "Hello" or "Good night"
and like this in C#:
var lambdaExpression = Lambda<Action>(body); var compiled = lambdaExpression.Compile(); compiled.Invoke();
// Prints either "Hello" or "Good night"
Compiling an expression tree into executable code is very useful for implementing other languages on top of the CLR, as it’s much easier to work with expression trees over direct IL manipulation. But if you’re programming in C# or Visual Basic, and you know the program’s logic at compile time, why not embed that logic in your existing method or assembly at compile time, instead of compiling at runtime?
However, runtime compilation really shines when you don’t know the best path or the right algorithm at design time. Using expression trees and runtime compilation, it’s relatively easy to iteratively rewrite and refine your program’s logic at runtime in response to actual conditions or data from the field.
Self-Rewriting Code: Call-Site Caching in Dynamically Typed Languages As an example, consider call-site caching in the Dynamic Language Runtime (DLR), which enables dynamic typing in CLR-targeting language implementations. It uses expression trees to provide a powerful optimization, by iteratively rewriting the delegate assigned to a specific call site when needed.
Both C# and Visual Basic are (for the most part) statically typed languages—for every expression in the language we can resolve a fixed type that doesn’t change over the lifetime of the program. In other words, if the variables x and y have been declared as an Integer (or an int in C#) and the program contains a line of code x + y, the resolution of the value of that expression will always use the “add” instruction for two Integers.
However, dynamically typed languages have no such guarantee. Generally, x and y have no inherent type, so the evaluation of x + y must take into account that x and y could be of any type, say String, and resolving x + y in that case would mean using String.Concat. On the other hand, if the first time the program hits the expression x and y are Integers, it’s highly likely that successive hits will also have the same types for x and y. The DLR takes advantage of this fact with call-site caching, which uses expression trees to rewrite the delegate of the call site each time a new type is encountered.
Each call site gets assigned a CallSite(Of T) (or in C# a Call- Site<T> ) instance with a Target property that points to a compiled
msdnmagazine.com
delegate. Each site also gets a set of tests, along with the actions that should be taken when each test succeeds. Initially, the Target delegate only has code to update itself, like so:
‘ Visual Basic code representation of Target delegate Return site.Update(site, x, y)
At the first iteration, the Update method will retrieve an applicable test and action from the language implementation (for example, “if both arguments are Integers, use the ‘add’ instruction”). It will then generate an expression tree that performs the action only if the test succeeds. A code-equivalent of the resulting expression tree might look something like this:
‘ Visual Basic code representation of expression tree
If TypeOf x Is Integer AndAlso TypeOf y Is Integer Then Return CInt(x) + CInt(y) Return site.Update(site, x, y)
The expression tree will then be compiled into a new delegate, and stored at the Target property, while the test and action will be stored in the call site object.
In later iterations, the call site will use the new delegate to resolve x + y. Within the new delegate, if the test passes, the resolved CLR operation will be used. Only if the tests fail (in this case, if either x or y isn’t an Integer) will the Update method have to turn again to the language implementation. But when the Update method is called it will add the new test and action, and recompile the Target delegate to account for them. At that point, the Target delegate will contain tests for all the previously encountered type pairs, and the value resolution strategy for each type, as shown in the following code:
If TypeOf x Is Integer AndAlso TypeOf y Is Integer Then Return CInt(x) + CInt(y) If TypeOf x Is String Then Return String.Concat(x, y)
Return site.Update(site, x, y)
This runtime code rewriting in response to facts on the ground would be very difficult—if not impossible—without the ability to compile code from an expression tree at runtime.
Dynamic Compilation with Expression Trees vs. with Roslyn
You can also dynamically compile code from simple strings rather than from an expression tree with relative ease using Roslyn. In fact, this approach is actually preferred if you’re starting off with Visual Basic or C# syntax, or if it’s important to preserve the Visual Basic or C# syntax you’ve generated. As noted earlier, Roslyn syntax trees model syntax, whereas expression trees only represent code operations without regard for syntax.
Also, if you try to construct an invalid expression tree, you’ll get back only a single exception. When parsing and compiling strings to runnable code using Roslyn, you can get multiple pieces of diagnostic information on different parts of the compilation, just as you would when writing C# or Visual Basic in Visual Studio.
On the other hand, Roslyn is a large and complicated depen- dency to add to your project. You may already have a set of code operations that came from a source other than Visual Basic or C# source code; rewriting into the Roslyn semantic model may be unnecessary. Also, keep in mind that Roslyn requires multithread- ing, and cannot be used if new threads aren’t allowed (such as within a Visual Studio debugging visualizer).
Rewriting Expression Trees:
Implementing Visual Basic’s Like Operator
I mentioned that expression trees are immutable; but you can create a new expression tree that reuses parts of the original. Let’s imagine
September 2019 41


































































































   47   48   49   50   51