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:
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 😉
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íTo se mi líbí
To je pravda 🙂 O tom měla tak trochu být i ta poslední věta 🙂 Vědět to člověk nemusí, ale když ho zajímá, co se děje na pozadí…
To se mi líbíTo se mi líbí
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íTo se mi líbí
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íTo se mi líbí
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íTo se mi líbí
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íTo se mi líbí
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íTo se mi líbí
btw len tak pre zaujimavost ako sa este daju vytvarat objekty, tak windsor ma fastcreate [link] pouziva nieco taketo:
co je pomalsie ako activator, ale nie nutne, treba mysliet tiez na to, ze pouzivame bezparametricke konstruktory
To se mi líbíTo se mi líbí
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íTo se mi líbí
no uz som otrava, ale neda mi este jedno genericke threadsafe riesenie s lambda a cachingom:)
To se mi líbíTo se mi líbí
Jen bych přidal ještě omezení new() na generický parametr, aby byla zaručena existence bezparametrického konstruktoru 😉
To se mi líbíTo se mi líbí