Vytváření instancí předem neznámých tříd v C#

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 😉

11 thoughts on “Vytváření instancí předem neznámých tříd v C#

  1. zajímavé, ale nenapadá mě jaksi konkrétní příklad použití, většinou volám neznámé třídy pomocí Factory pattern a tam takové psí kusy není potřeba dělat 🙂

    To se mi líbí

  2. caf, zabudol si este na jeden sposob, vyuzitie Expression:
    var lambda = Expression.Lambda<Func>(
    Expression.New(typeof (TestClass)),
    null
    ).Compile();
    for (int i = 0; i < instances; i++)
    {
    var t = lambda();
    }
    sw.Stop();
    report("lamda", sw.Elapsed);

    vyzera to viac sexy ako emit, nie?:)

    To se mi líbí

  3. Díky moc 🙂 Doplnil jsem článek i o tuhle možnost vytváření instancí. Je to určitě pěknější zápis než generováni CILu, jen škoda, že je (aspoň u mě) o chlup pomalejší…

    To se mi líbí

  4. to augi: na mojom pc ten rozdiel nebol az taky vyrazny a mimochodom emitovanie kodu podlieha ReflectedPermission, co by teoreticky (nemam odskusane) lambda nemala. Na to treba mysliet najma pri nasadeni aplikacie napr na hosting server.

    To se mi líbí

  5. expression.Compile() by tohle omezení mít nemělo – používá se např. v ASP.NET MVC, který se běžně nasazuje na hostinzích. Docela mi to dává smysl – pomocí emitování člověk může vygenerovat pěkný blbosti, ale při sestavování lambda expression má dost svázané ruce, takže nemůže napáchat tolik škody 🙂

    Co se týče rychlosti, tak pokud se neliší řádově, tak bych se jí spíš neřídil a vybral si to, co mi víc vyhovuje. Taky těžko říct, co jsme vlastně změřili 😉

    To se mi líbí

  6. no urcite si zmeral to, ze Activator je archaicke riesenie, ktore sa by som urcite neodporucal pouzivat a skor isiel bud cestou CIL, co je to najrychlejsie riesenie, alebo cestou lambda (tu treba mysliet na to, ze bez cachovania scompilovanej funkcie ide o najpomalsie riesenie).

    Teraz som sa tak zamyslel, tak ake vysledky by asi test daval, ak by si necachoval konstrutor, lambdu pripaden dynamic metodu, ale volal vzdy cely kod?

    A k ctor.Invoke(null) a creator() testu by som mal poznamku, pretoze vidim, ze pouzivas var, tak si si vedomy, ze vracias object a pre typovu bezpecnost je potrebne pretypovanie, co uz z nich take vykonne riesenie nerobi:)

    To se mi líbí

  7. btw len tak pre zaujimavost ako sa este daju vytvarat objekty, tak windsor ma fastcreate [link] pouziva nieco taketo:

    sw.Reset();
    sw.Start();
    
    ConstructorInfo cinfo = typeof(TestClass).GetConstructor(Type.EmptyTypes);
                
    for (int i = 0; i < instances; i++)
    {
      var t = (TestClass)System.Runtime.Serialization.FormatterServices.GetUninitializedObject(typeof(TestClass));
      cinfo.Invoke(t, null);
    }
    sw.Stop();
    report("windsor", sw.Elapsed);
    

    co je pomalsie ako activator, ale nie nutne, treba mysliet tiez na to, ze pouzivame bezparametricke konstruktory

    To se mi líbí

  8. Jo. A dost možná ještě pár dalších věcí, které jsem tím mým jednoduchým testem (ne)změřil 🙂 Nicméně když jsem zkusil přidat přetypování, tak jsem se u sebe signifikantní změny nedočkal.

    Ve článku jsem chtěl hlavně ukázat, jaké existují možnosti a jaké jsou fakt výkonově špatné – což mě třeba u new T() (kde T je generický parametr) dost překvapilo.

    Taky je asi zřejmý, že výkon není třeba řešit třeba v případě, kdy jde např. o vytvoření instance pluginu (jen jednotky instanciací během programu).

    Inicializaci jednotlivých způsobů vytváření instancí jsem schválně neřešil – podnětem pro vznik článku bylo měření, které jsem si dělal kvůli jedné specifické situaci, kterou jsem musel řešit, což bylo vytváření hodně instancí několika málo tříd.

    To se mi líbí

  9. no uz som otrava, ale neda mi este jedno genericke threadsafe riesenie s lambda a cachingom:)

    public class LambdaFactory where T : class
    {
       private static readonly Func<T> LambdaConstruction = Expression.Lambda<Func<T>>(
          Expression.New(typeof(T)), null).Compile();
    
       public static T Create()
       {
          return LambdaConstruction();
       }
    }

    To se mi líbí

Napsat komentář

Tento web používá Akismet na redukci spamu. Zjistěte více o tom, jak jsou data z komentářů zpracovávána.