(Toto je první část článku o hodnotových typech v jazyce C#, druhá část je zde, třetí část je zde.)
V jazyku C# máme referenční a hodnotové datové typy. Zatímco u referenčních typů se lokální proměnná nebo field pouze odkazuje na nějaký objekt (nebo má hodnotu null), u hodnotových typů je v proměnné nebo fieldu přímo vlastní hodnota. Při přiřazení hodnotového typu se hodnota kopíruje, u referenčních typů se kopíruje pouze odkaz, více proměnných se pak může například odkazovat na stejný objekt. Kromě hodnotových typů, které jsou obsaženy přímo v .NET Frameworku (bool, int, double, decimal, DateTime, …), můžeme psát i hodnotové typy vlastní (struktury a enumy). S psaním vlastních struktur to ale není tak jednoduché, proto se podíváme postupně na nějaké záludnosti.
Dnes začněme tím, že mnoho materiálů (a díky tomu i vývojářů) uvádí chybný výrok jako: Hodnotové typy se alokují na zásobníku, kdežto referenční typy na heapu (a v důsledku toho jsou hodnotové typy rychlejší).
Jak to tedy ve skutečnosti je? Pokud se chceme bavit o tom, kde je co alokováno, vše můžeme zjednodušit použitím termínu storage (uložiště tj. místo, kde jsou data umístěna - zůstanu ale u anglického výrazu).
U referenčních typů storage, který odpovídá lokální proměnné nebo fieldu, obsahuje odkaz na nějaký objekt tzv. referenci (nebo má reference hodnotu null). Objekt samotný je vždy alokován na heapu (garbage collected pool). Velikost reference přitom nezávisí na konkrétním datovém typu, ale je vždy stejně velká (*).
U hodnotových typů obsahuje odpovídající storage přímo vlastní hodnotu konkrétního datového typu a velikost storage je dána daným typem.
U lokální proměnné se, je-li to možné (**), storage nachází na zásobníku (nebo dokonce v registru), u fieldu je součástí dat objektu (u třídy) tj. na heapu. Storage odpovídající fieldu hodnotového typu (u struktury) je součástí dat instance celé struktury tj. je uvnitř jejího “vnějšího” storage, která je buď na zásobníku nebo na heapu.
Vidíme, že takto vše dává mnohem větší smysl. Výše uvedené má dále mnoho důsledků, například:
- U referenčních typů je samotná reference konstantně veliká, daný datový typ ale již určuje potřebnou velikost datového bloku (not-null) objektu, který se musí na heapu alokovat.
- To že jsou lokální proměnné (vždy pokud to lze) ukládány na zásobník je sice opravdu z důvodu rychlosti, jedná se ale o implementační detail, který je dán pouze dobou životnosti daného storage. Nicméně nutno dále říci, že rychlosti alokace volného místa o určité velikosti je přesto u zásobníku i heapu v průměrném případě stejná, co se hlavně liší je rychlosti dealokace tj. u hodnotových typů odpadá veškerá režie garbage collectoru (GC). A to platí obecně tj. bez ohledu na to, zda se hodnotový typ zrovna nachází na zásobníku nebo na heapu.
- Reference a instance hodnotových typů jsou z hlediska jejich storage (až na velikost) úplně stejné, někdy jsou na zásobníku nebo v registru, někdy na heapu v závislosti na životnosti daného storage.
- Správný výrok, odpovídající nepravdivému výroku uvedenému na začátku, by byl: Hodnotové typy mohou být někdy alokovány na zásobníku, zatímco referenční typy jsou vždy alokovány na heapu. Tato skutečnost ale není zpravidla u managed prostředí až tak důležitá. Navíc výrok je také neúplný, například neříká nic o referencích samotných, když je ale tak důležité co je na zásobníku a co na heapu, proč je ignorovat? A co skutečnost, že hodnotový typ může být za určitých okolností i třeba v registru, proč ignorovat toto?
Další vlastností hodnotových typů je to, že v .NET Frameworku a jazyce C# se v případě potřeby lze i k hodnotovým typům chovat jako k objektům tj. je možné vytvořit referenci, která se odkazuje na jakýsi “wrapper” hodnoty daného hodnotového typu (a ten je také vždy alokován na heapu). Tomuto procesu se říká boxing a procesu opačnému unboxing.
Nyní rozhodnout jak je to například s hodnotami 1 až 10 z následujícího příkladu bude již asi poměrně jednoduché:
class C<T>
{
readonly T x;
public C(T x) { this.x = x; }
}
struct S<T>
{
readonly T x;
public S(T x) { this.x = x; }
}
class Test
{
static void Main()
{
int i = 1;
C<int> ci = new C<int>(2);
S<int> si = new S<int>(3);
int[] ai = new int[] { 4 };
object o = 5;
C<object> co = new C<object>(6);
S<object> so = new S<object>(7);
object[] ao = new object[] { 8 };
C<S<int>> csi = new C<S<int>>(new S<int>(9));
S<S<int>> ssi = new S<S<int>>(new S<int>(10));
}
}
- Přímo hodnota na zásobníku (nebo v registru): 1 (proměnná i)
- Hodnota na zásobníku (nebo v registru) jako součást instance struktury: 3 (součást S<int>, proměnná si), 10 (součást S<int>, které je součástí S<S<int>>, proměnná ssi)
- Na heap přímo součástí dat objektu: 2 (objekt C<int> ), 4 (objekt int[]), 9 (součást struktury S<int>, která je součást dat objektu C<S<int>>)
- Na heap jako samostatný objekt (box): 5 (reference na zásobníku), 6 (reference součástí objektu C<objekt> na heap, 7 (reference součástí struktury S<object>, proměnná so na zásobníku), 8 (reference součástí objektu object[] na heap)
Všimněte si dále, že třída C<T> má jen jedinou datovou položku, a proto funguje pro T hodnotového typu jako “generický box” tj. chová se velmi obdobně jako klasický boxing, ale na rozdíl od typu object lze použít jen pro konkrétní datový typ (konkrétní T). Máme tedy také statickou typovou kontrolu. Použití takové třídy může být někdy docela užitečná technika.
class Boxed<T>
{
public T Value { get; private set; }
public Boxed(T value)
{
this.Value = value;
}
}
Závěrem: Nezapomínejme ale, že mnohem důležitější než to, kde se hodnotové typy nachází nebo i to, že jsou runtimem optimálněji dealokovány, je fakt, že hodnotové typy mají sémantiku kopírování hodnotou.
Dnes diskutovaným tématem se podrobněji zabývají články zde, případně zde a zde.
Příště se podíváme na to, jaké jsou základní rozdíly při psaní vlastních struktur oproti psaní tříd.
(Toto je první část článku o hodnotových typech v jazyce C#, druhá část je zde, třetí část je zde.)
(*) Jazyk C# garantuje, že čtení nebo zápis referencí je atomická operace, proto je velikost reference odvozená od velikosti fyzického ukazatele u dané procesorové architektury. Zpravidla tedy 32 nebo 64 bitů.
(**) Přesněji storage se nachází na zásobníku nebo v registru pouze pokud lze bezpečně v době kompilace určit, že jeho životnost je menší než tzv. aktivační perioda metody, která storage vlastní. To odpovídá lokální proměnné, pokud se nejedná např. o proměnnou v iterátor bloku, v lamda výrazu nebo closure přes danou proměnnou.