.NET
is a platform which integrates a huge number of features experienced in
the last 20 years of software development. There are many developers,
which find all this new and exciting. For example, object oriented
development. An experienced VB-developer does not need much time to
learn many of important object oriented rules and practices. It is very
easy to start to develop in object oriented manner.
However, to start to feel and think this way, it is required much more time than many of us think.
In
last 6 years of .NET development, there was lot of exiting examples,
which used object oriented patterns just wrong way. One of them is
described in this article.
Generics
are for sure one of most important new features of .NET2.0. At this
moment it is very important to know where generics come from and what
they are. C++ developers know that they are a resurrection of
templates, died in the time of .NET1.0.
Generics
in C# are instantiated at the runtime and templates in C++ are
instantiated during compile or link time. Additionally, C# generics are
type safe and guarantee that all operations on type will succeed.
By
using of generics I will analyze the factory pattern, which is often
used the wrong way. Assume there is a very simple method, which creates
the object of some specific type described by the input parameter like
shown in this code snippet:
public static object CreateInstance(string type)
{
if (type == "type1")
return new Type1();
else if (type == "type@")
return new Type2();
else
throw new Exception("Bad type");
}
This
code is very simple and works fine. However thanks to generics there is
also a new way to do the same thing as shown bellow:
public static T CreateInstance<T>() where T:new()
{
T t = new T();
}
Even more there are also different ways to prove the type of ‘T’:
T t = new T();
if (t is int)
...;
else if (typeof(T).IsAssignableFrom(typeof(long)))
...;
else if (typeof(T).IsInstanceOfType(typeof(uint)))
...;
else
throw new Exception("Bad type");
All this seems to be modern and simply cool way to implement a factory. However it is mostly very bad pattern.
Why?
To
understand what here happens, it is important to understand how
generics works. Because generics are instantiated on the runtime, the
used binding is late-binding (slow-binding). To prove this I
disassembled the method CreateInstance<T>:
// Code size 38 (0x26)
.maxstack 2
.locals init ([0] !!T CS$1$0000,
[1] !!T CS$0$0001)
IL_0000: nop
IL_0001: ldloca.s CS$0$0001
IL_0003: initobj !!T
IL_0009: ldloc.1
IL_000a: box !!T
IL_000f: brfalse.s IL_001c
IL_0011: ldloca.s CS$0$0001
IL_0013: initobj !!T
IL_0019: ldloc.1
IL_001a: br.s IL_0021
IL_001c: call !!0 [mscorlib]System.Activator::CreateInstance<!!0>()
IL_0021: stloc.0
IL_0022: br.s IL_0024
IL_0024: ldloc.0
IL_0025: ret
It
is obvious, that dynamic instantiating (late-binding) is used in the
generic method.This kind of invocation is known as slow one. Now, let’s
take a look on the usual factory pattern shown in the first example.
.locals init ([0] object CS$1$0000)
IL_0000: nop
IL_0001: newobj instance void ConsoleApplication3.Type1::.ctor()
IL_0006: stloc.0
IL_0007: br.s IL_0009
IL_0009: ldloc.0
IL_000a: ret
In this case the early-binding is used. Because the type is known at the compile time the compiler generates the exact type.
To
prove the real performance of both methods I implemented following
example. Methods t1 and t2 implement generic and none-generic factory.
Methods c1 and c2 wraps up calls to t1 and t2 respectively, so I can be
sure that required boxing in c2 is also calculated during performance
measure.
static void Main(string[] args)
{
for (int n = 0; n < 3000; n++)
{
c1();
c2();
}
}
public static void c1()
{
p = t1<Program>();
}
public static void c2()
{
p = (Program)t2();
}
public static T t1<T>()
where T : new()
{
return new T();
}
public static object t2()
{
return new Program();
}
Here are results of instrumentation:
Method calls exclusive inclusive-time
------------------------------------------------------------
c1() 3000 0.934185 8.431655
t1() 3000 0.332074 7.497470
c2() 3000 0.444161 1.249673
t2() 3000 0.375057 0.805512
Conclusion :
If
described pattern is required note that generics under the hub use
late-binding by instantiating of the generic type. This is a reason why
generics are slower in such cases than using new object() and corresponded cast.
However,
there is a case where CreateInstance<T> can and should be used.
That is when your implementation of the method requires late binding:
Activator.CreateInstance().
Damir Dobric
www.daenet.de
Posted
Mar 14 2006, 12:40 AM
by
Damir Dobric