Simple typeswitch in C# 3.0, Part 2: The Solutions
September 20th, 2007
Some days ago I wrote an overview of typeswitch problem.
Now it’s time to give some solutions.
Overview
Let’s imagine a document inheritance hierarchy:
XsltDocument : XmlDocument : Document TextDocument : Document
Let’s assume I want to get strings “Xml”, “Xslt”, “Not Xml and not Xslt” based on the document runtime type.
This is primitive indeed, but it does demonstrate a concept.
I call the most useful soultion fluent switch:
string result = Switch.Type(document). Case( (XsltDocument d) => "Xslt" ). Case( (XmlDocument d) => "Xml" ). Otherwise( d => "Not Xml and not Xslt" ). Result;
It does contain a lot of visual clutter, but it scales quite well in comparison to if/return approach.
The code behind is simple — each Case checks type and returns itself.
Case is a generic method whose parameters are inferred from the lambda.
For this task, case bodies do not depend on actual object contents.
So they can be expressed cleaner:
string result = Switch.Type(document).To<string>(). Case<XsltDocument>("Xslt"). Case<XmlDocument>("Xml"). Otherwise("Not Xml and not Xslt"). Result;
This time I have to specify the result type explicitly.
The fluent switch syntax is quite powerful — you can even add cases dynamically.
This is a nice difference from conventional language constructs.
Alternatives
I have tried a number of alternatives, but no one of them did better.
- Many overloads switch
string result = Switch.Type( document, (XsltDocument d) => "Xslt", (XmlDocument d) => "Xml", d => "Not Xml and not Xslt" );
This syntax is quite concise and understandable.
But it requires an additional overload for each additional case.
So it is quite impractical. - Object initializer switch
string result = new TypeSwitch<Document, string>(document) { (XsltDocument d) => "Xslt", (XmlDocument d) => "Xml", d => "Not Xml and not Xslt" };
Also more concise than my original solution, but much more cryptic.
Constructor and generic parameters also add a degree of confusion.The most interesting thing about this syntax was that it actually worked.
It seems object initializers have some nice fluent power.
Compilation
After running some benchmarks, I found that fluent switch is about 200 times slower than hardcoded ifs.
It may be perfectly acceptable, of course.
However, I have found a way to precompile the switch using expression trees.
From the usage perspective, precompiled switch is just a Func<T, TResult> (it does not support Actions right now).
So you can cache it in
private static readonly Func<Document, string> CompiledFluentLambdaSwitch = Switch.Type<Document>().To<string>(). Case( (XsltDocument d) => "Xslt" ). Case( (XmlDocument d) => "Xml" ). Otherwise( d => "Not Xml and not Xslt" ). Compile();
which is extremely similar to the first code sample.
The differences are that you do not specify what you are switching on (it would be a function parameter).
But you do explicitly specify from/to types.
The compilation process was fun to write, since it was the first time I dug into expressions trees.
Statements are not supported in trees, so I had to use embedded ConditionalExpressions for cases.
The resulting tree is something like
d => (d is XsltDocument) ? ((cast => "Xslt")(d as XsltDocument)) : ((d is XmlDocument) ? ((...
I have not found a way to cache cast and null-check it, so I cast/typecheck it two times.
Benchmarks
The best thing about compilation is performance:
Benchmark: 1000000 iterations, two switch calls per iteration. Benchmark overhead: 40.1ms 30.0ms 30.0ms 30.0ms | 32.5ms Direct cast: 80.1ms 60.1ms 80.1ms 60.1ms | 70.1ms Fluent switch on lambdas: 1512.2ms 1502.2ms 1602.3ms 1482.1ms | 1524.7ms on lambdas (compiled): 90.1ms 110.2ms 80.1ms 80.1ms | 90.1ms on constants: 1281.8ms 1271.8ms 1311.9ms 1271.8ms | 1284.3ms on constants (compiled): 80.1ms 90.1ms 90.1ms 70.1ms | 82.6ms Many overloads switch: 440.6ms 390.6ms 430.6ms 420.6ms | 420.6ms Object initializer switch: 751.1ms 681.0ms 741.1ms 751.1ms | 731.1ms
As you can see, precompiled switch is nearly as performant as hardcoded one (direct cast).
I am quite impressed by simplicity/power ratio of the expression trees.
Code
I uploaded AshMind.Constructs to Google Code.
I see it as a learning/research project, but you can put it to any practical use.