Pokud se snažíte v C# psát aplikace modulárně a genericky, tak můžete narazit na to, co je naznačeno v nadpisu – vytváření instancí tříd, jejichž typ není v době kompilace znám. Jak na to si ukážeme v tomto článku, včetně porovnání výkonu. Konkrétně se zaměřím na vytváření instancí pomocí bezparametrického konstruktoru a příliš nebudu řešit to, kolik zabere příprava té které metody instanciace – jde mi o vytváření velkého množství instancí malého počtu tříd.

Dejme tomu, že jsou dva způsoby, jak dostat informaci o typu třídy, jejíž instanci chceme vytvořit.

Generický parametr

Začněme tím víc typovým způsobem – máme za úkol vytvořit instanci typu, jenž máme předán jako parametr generiky. Na generické parametry můžeme klást různé požadavky (za klíčovým slovem where) a jedním z nich je existence bezparametrického konstruktoru (new()). Konstrukce instance třídy předané jako generický parametr vypadá takto:

class ClassFactory<T> where T : new()
{
    public static T Create()
    {
        return new T();
    }
} 

To je z hlediska designu docela pěkný, silně typový, přístup s kontrolou existence bezparametrického konstruktoru. Podívejme se teď, jaký CIL kód nám z této metody vypadl:

CIL - new T()

Jak vidno, volá se statická generická metoda System.Activator.CreateInstance<T>. Tato metoda je úplně normální a můžeme ji tudíž použít na vytvoření instance kdykoliv, když máme k dispozici generický typ. Můžeme ji použít ale i tehdy, když nemáme na generickém parametru specikováno omezení new(), což může vyústit ve výjimku za běhu. Proto doporučuji navrhnout aplikaci tak, abychom se přímémo volání metody CreateInstance<T> vyhnuli (tzn. nějak zajistit omezení generického parametru). Ve stejném duchu se vyjadřuje i dokumentace k této metodě.

System.Type

Druhým způsobem, jak se k nám může dostat informace o třídě, jejíž instanci máme vytvořit, je pomocí třídy System.Type. Tuto třídu můžeme získat různými způsoby:

Type t1 = typeof(TestClass); // přímo z typu
Type t2 = someObject.GetType(); // z kterékoliv instance lze získat aktuální typ
Type t3 = System.Reflection.Assembly.GetEntryAssembly().GetType("Test.TestClass"); // ze stringu!
Type t4 = typeof(ClassFactory<>).MakeGenericType(t1); // získání typu generiky s konkrétním generickým typem 

A jak vytvořit instanci takové typu, o němž máme informaci v podobě instance třídy System.Type? Takto :)

// jednoduše
var i1 = Activator.CreateInstance(t1);
// složitěji - získáme konstruktor a zavoláme ho
var i2 = t1.GetConstructor(Type.EmptyTypes).Invoke(null); 

Výhodou těchto dvou řešení je to, že ho můžeme použít i pro konstruktory s parametry. Nevýhodou je ukrutná pomalost. Proto uvedu ještě jedno řešení, které spočívá v tom, že si dynamicky vygenerujeme CIL kód, který vytváří instanci třídy:

// vytvoříme dynamickou metodu
var dynamicMethod = new DynamicMethod("blabla", t1, Type.EmptyTypes);
var ilGenerator = dynamicMethod.GetILGenerator();
// vygenerujeme do ní kód, který zavolá bezparametrický konstruktor
ilGenerator.Emit(OpCodes.Newobj, t1.GetConstructor(Type.EmptyTypes));
// a vrátí jeho výsledek
ilGenerator.Emit(OpCodes.Ret);
// vycucneme si delegáta na dynamickou metodu
var creator = (Func<object>)dynamicMethod.CreateDelegate(typeof(Func<object>));
// a voláme delegáta, který nám vrací instance
var instance = creator(); 

vlko mi v komentáři připomněl ještě jednu možnost, jak vytvářet instance (díky!). Je to podobné jako předchozí generování CILu, ale je to trošku abstrahovanější, modernější. Vygenerujeme takovou lambda expression, která bude vracet novou instanci, a tuto lambda expression zkompilujeme na delegáta. Kdybych udělali ale toto, tak dostaneme výjimku za běhu:

var lambdaCreator = (Func<object>)System.Linq.Expressions.Expression.Lambda(System.Linq.Expressions.Expression.New(typeof(TestClass))).Compile(); 

Konkrétně tuto výjimku: Objekt typu System.Func`1[CreateInstanceTest.TestClass] nelze přetypovat na typ System.Func`1[System.Object].. Problém je v tom, že jazyk C# 3.0 nepodporuje kontravarianci generických typů, tudíž nelze přetypovat Func<TestClass> na Func<object>. V C# 4.0 už toto bude bez problémů možné.
Aby vytváření instancí pomocí zkompilované lambda expression fungovalo i v C# 3.0, musíme vygenerovat takovou lambda expression, která bude přímo vracet Func<object> – tudíž musíme nově vytvořenou instanci ještě v lambda expression přetypovat na object:

var lambdaCreator = (Func<object>)System.Linq.Expressions.Expression.Lambda(System.Linq.Expressions.Expression.Convert(System.Linq.Expressions.Expression.New(typeof(TestClass)), typeof(object))).Compile();
var instance = lambdaCreator(); 

Místo metody Convert můžeme použít také ConvertChecked nebo TypeAs, ale Convert je podle mých jednoduchých měření nejefektivnější.

Výkon

Na závěr uvedu jednoduché výkonové srovnání jednotlivých metod, včetně zpomalení oproti běžnému volání new. Měření není nikterak přesné a má sloužit je k orientaci v problematice. Vždy jsem vytvářel 1024 * 1024 instancí jednoduché public třídy:

Způsob Čas v ms Zpomalení
new TestClass(); 20,55 1,0
Activator.CreateInstance(); 1980 96
ClassFactory.Create(); 1998 97
Activator.CreateInstance(typeof(TestClass)) 289 14
ctor.Invoke(null) 1641 80
creator() 27,64 1,3
lambdaCreator() 30,09 1,5

Jak vidno, Activator.CreateInstance<T>() a new T() je stejně rychlé, což jsme si již vysvětlili. Negenerická metoda Activator.CreateInstance() stejně jako přímé volání konstruktoru o dost pomalejší než klasické vytvoření pomocí new. Tomu se přibližuje jen dynamicky vyemitovaný CIL kód, resp. zkompilovaná lambda expression.

Jak jsem psal, toto měření proběhlo pro vytváření public třídy. Pokud ale změníme viditelnost třídy na internal, dostáváme jiná čísla:

Způsob Čas v ms Zpomalení
new TestClass(); 20,49 1,0
Activator.CreateInstance(); 1965 96
ClassFactory.Create(); 1966 96
Activator.CreateInstance(typeof(TestClass)) 4071 199
ctor.Invoke(null) 4877 238
creator() 26,83 1,3
lambdaCreator() 32,09 1,6

Co k tomu říci? Snad jen – zajímavé ;-)
Pro úplnost přikládám testovací kód.

Závěr

Pokud máme vytvářet instanci typu, jehož přesný typ není v době kompilace znám, je z hlediska výkonu (nepočítaje start-up) nejvýhodnější využít možnosti generování CIL kódu. Pokud chceme takto použít konstruktor s parametry, je to také možné, ale vytváření dynamické metody již bude složitější.
Upřednostnění metody s generováním CILu platí i pro situaci, kdy máme typ k dispozici ve formě generického parametru s omezením new(). Bude to rychlejší než volání new T(). Omezení na generický typ bych ale určitě nechal, abychom se nepřipravili o kontrolu během kompilace.
No a pokud nechceme toto vůbec řešit, můžeme využít třeba služeb nějakého IoC/DI kontejneru ;-)

  • Facebook
  • TwitThis
  • LinkedIn
  • Live
  • Google Bookmarks
  • email