From Wikipedia:
In computer science, dynamic dispatch is the process of selecting which implementation of a polymorphic operation (method or function) to call at runtime.
There are many ways to acheive dynamic dispatch in C#. I will cover a few ways to acheive single dispatch based on the argument into an overloaded method.
Say you have a class called ObjectProcessors
that has a overloaded methods named Process(xxx value)
that take a single argument like so:
public class ObjectProcessors
{
public void Process(object value)
{
LogMessage("object");
}
public void Process(string value)
{
LogMessage("string");
}
public void Process(MyClass value)
{
LogMessage("MyClass");
}
public void ProcessUnknownType()
{
LogMessage("NULL");
}
//More overloads as necessary
private void LogMessage(string typeName)
{
Console.WriteLine("Processed a value of type {0}.", typeName);
}
}
For all the below examples, you have a list of objects of varying types:
var objects = new List<object>
{
"A string",
new object(),
new MyClass(),
null, //null has no type
new NonOverloadedClass() //No overload specified
}
Direct Type Matching
To acheive dynamic dispatch you could just match on the type:
public void Dispatch(List<objects> objects)
{
var processors = new ObjectProcessors();
foreach (var obj in objects)
{
if(obj == null)
{
processors.ProcessUnknownType();
continue;
}
var objType = obj.GetType();
if(objType == typeof(string))
{
processors.Process((string)obj);
continue;
}
if(objType == typeof(MyClass))
{
processors.Process((MyClass)obj);
}
processors.Process(obj);
}
}
Advantages
- The fastest method described here (by a wide margin).
- No magic string for the overloaded method.
Disadvantages
- Lots of brittle code.
- You have to add a new conditional for EVERY overload.
Reflection
You could also use reflection:
public void Dispatch(List<objects> objects)
{
var processors = new ObjectProcessors();
var processorsType = processors.GetType();
foreach (var obj in objects)
{
if (obj == null)
{
processors.ProcessUnknownType();
}
else
{
var mi = processorsType.GetMethod("Process", new[] {obj.GetType()});
mi.Invoke(processors, new[] {obj});
}
}
}
Advantages
- No additional code needed for new overloads.
- Much less code than direct type matching.
Disadvantages
- Slowest method shown here.
- Magic string name for process method.
ImpromptuInterface
Next, you could use ImpromptuInterface in a similar fashion:
public void Dispatch(List<objects> objects)
{
var processors = new ObjectProcessors();
var ci = new CacheableInvocation(InvocationKind.InvokeMemberAction, "Process", 1);
foreach (var obj in objects)
{
if (obj == null)
{
processors.ProcessUnknownType();
}
else
{
ci.Invoke(processors, obj);
}
}
}
Advantages
- Very little code and no additional code needed for new overloads.
- Almost twice as fast as reflection.
Disadvantages
- Direct type matching is still almost 10x faster.
- Requires a third party library.
Dynamic
Finally, we can just let the dynamic language runtime do all the work:
public void Dispatch(List<objects> objects)
{
foreach (var obj in objects)
{
if (obj == null)
{
processors.ProcessUnknownType();
}
else
{
processors.Process((dynamic) obj);
}
}
}
Advantages
- Built into .Net 4.0 and later, no library needed.
- About 10-20% faster than ImpromptuInterface.
- No magic string name for method.
- No additional code needed for overloads.
Disadvantages
- There are a few "gotchas" when using dynamic (see below).
- Direct type matching is still significantly faster.
Dynamic Gotchas
In addition to those mentioned by Jon Skeet, here are some I have found:
public void MayNotBeObvious()
{
int? aNullableNumber = 3;
Process((dynamic)aNullableNumber);
int nonNullableNumber = 42;
Process((dynamic)nonNullableNumber)
}
public void Process(int? value)
{
//neither will match this one
}
public void Process(int value)
{
//both match this one
}
Explict casting in the dynamic example is much faster (2X) than using dynamic
in the foreach.
//This is faster
public void Dispatch(List<objects> objects)
{
foreach (var obj in objects)
{
if (obj == null)
{
processors.ProcessUnknownType();
}
else
{
processors.Process((dynamic) obj);
}
}
}
//This is much slower
public void Dispatch(List<objects> objects)
{
foreach (dynamic obj in objects)
{
if (obj == null)
{
processors.ProcessUnknownType();
}
else
{
processors.Process(obj);
}
}
}
Benchmarks
Using a simple console app I created (with a slighlty more complex example), here are some simple StopWatch
measured benchmarks for 200,000 iterations:
- Type Matching:
.42s
- Dynamic:
3.06s
- ImpromptuInterface:
3.63s
- Reflection:
6.48s
Conclusion
When using the reflection, ImpromptuInterface, or dynamic approach, the overload that accepts the type object
is the "default" method. That is to say, if no other suitable overload is found, it will use the one that takes object
. There is a lot of interesting behavior with respect to structs in addition to what I listed above. Take special care when using structs with these approaches, especially core structs like byte
, int
, double
, float
, etc.
In all cases, null
is a special case that must be handled explictly since it lacks type information and will lead to an exception in all expect a few narrow cases (none of which will match actual type).