“Happy Boxing Day”. Jistě budete souhlasit, že dnes je ten správný čas zabývat se operaci boxing.
Dejme tomu, že máme metodu s jedním generickým argumentem T (nebo metodu v generické třídě) a uvnitř této metody chceme provést přetypování nějaké hodnoty na typ T. Z důvodu obecnosti předpokládejme dále, že T může být i hodnotový typ tedy například int (pro něj, budeme celou věc zkoušet) a vybereme ještě nějaký jiný typ, pro který existuje konverze na int třeba short:
class Foo
{
public static void Bar<T>()
{
short s = 123;
int i = s;
T value1 = (T)i; //<--compile-time error "Cannot convert type 'int' to 'T'"
T value2 = (T)s; //<--compile-time error "Cannot convert type 'short' to 'T'"
T value3 = (T)(int)s; //stejné jako případ 1
T value4 = (T)(object)i; //boxing, unboxing
T value5 = (T)(object)s; //<--runtime error "Invalid cast exception"
T value6 = (T)(object)(int)s; //stejné jako případ 4
}
public static void Main()
{
Foo.Bar<int>();
}
}
Protože generický argument T ale může obecně představovat libovolný typ, jde nám v našem experimentu o takové chování, že přetypování hodnoty typu int na T projde jen ve volání Foo.Bar<int>() a v ostatních případech bude v runtime vyhozena výjimka. (Můžeme si představit, že kódu předchází switch nebo if test, kterým si zajistíme volání dané části kódu jen pro očekávané T.)
Co se tedy v uvedeném kódu děje? Máme hodnoty typu int a short a snažíme se je přetypovat (*) na T postupně několika způsoby (číslování dle názvu cílové proměnné):
- Hodnota i je známá v době kompilace jako typu int. Kompilátor ale obecně neví jakou konverzi na tomto místě vložit, protože ta je závislá na konkrétním T, které v době kompilace není známo, a proto toto jednoduše nedovolí.
- Naprosto totožný případ, to že máme proměnnou typu short místo proměnné typu int na výsledku nic nemění.
- Hodnotu typu short můžeme nejprve zkonvertovat na int (pro jednoduchost opět předpokládejme, že víme, že chceme int), ale tím máme opět případ č. 1.
- Pokud hodnotu typu int nejprve zkonvertujeme na object tj. provedeme boxed konverzi, kompilátor zde vygeneruje kód pro runtime check zda je object “naplněn” hodnotou typu T a kód pro provedení unboxing konverze v případě že T je hodnotový typ (náš případ). Výsledku tedy sice docílíme, ale důsledkem je nadbytečný boxing, unboxing naší hodnoty.
- U boxed short hodnoty takto jednoduché přetypování fungovat nebude a vede na InvalidCastException, vysvětlení viz tento článek.
- Aby jsme chybu (předchozí případ č. 5) obešli musíme provést nejprve konverzi short hodnoty na int, pak teprve na object a pak konečně na T. Důsledek je stejný jako u případu č. 4.
Zatím se zdá, že požadovaného docílíme pouze konverzi přes objekt. Otázkou je však, zda se lze zbavit nadbytečného provádění boxing, unboxing.
Pojďme se nejprve podívat jak je to v případě, kdy se vzdáme obecnosti u generického argumentu T.
Pokud bude T pouze referenční typ (where T: class), všechny uvažované konverze budou pouze tzv. representation-preserving a diskutovaný problém vůbec nevznikne.
Pokud bude T pouze hodnotový typ (where T: struct), bude se sice výše uvedený kód chovat stejně, ale máme k dispozici další možnosti:
public static void Bar<T>() where T : struct
{
short s = 123;
int i = s;
T value1 = (i as T?).Value;
T value2 = ((int)s as T?).Value;
}
V tomto případě lze výše uvedený problém odstranit použitím operátoru as (který pak pravděpodobně rovnou využijeme pro test, zda je konvertovaný výraz opravdu typu T). Samozřejmě u hodnoty typu short musíme provést nejprve konverzi na int, protože výraz s as T? by měl při T=int hodnotu null.
Pro obecný generický argument to není vůbec jednoduché. Jedno řešení sice existuje, ale má tu nevýhodu, že jeho efektivita bude záviset na množství prováděných konverzí tj. na tom, zda v konkrétním scénáři bude eliminace boxing/unboxing operací opravdu výhodnější oproti času potřebnému k inicializaci daného řešení. Toto řešení si ukážeme ale až příště.
(*) Operátor cast slouží pro přetypování i pro konverzi. Chování tohoto operátoru u boxing/unboxing konverzí i rozdíly mezi representation-preserving a representation-changing konverzemi jsou velmi dobře vysvětleny v tomto článku.