(Toto je třetí část článku o hodnotových typech v jazyce C#, první část je zde, druhá část je zde.)
S hodnotovými datovými typy je často spojována věta: “Mutable value types are evil.”, o co jde? Ukážeme si to hned na příkladu, předpokládejme následující “mutable” strukturu (struktura, která umožňuje změnit své vnitřní data) a kód:
struct Point
{
private int x;
private int y;
public int X { get { return x; } }
public int Y { get { return y; } }
public Point(int x, int y)
{
this.x = x;
this.y = y;
}
public void Add(int dx, int dy) //<--Metoda mění hodnoty x a y!!!
{
this.x += dx;
this.y += dy;
}
public override string ToString()
{
return string.Format("X = {0}, Y = {1}", this.X, this.Y);
}
}
static class Test
{
static readonly Point Position = new Point(1, 1); //<--Readonly field
static void Main()
{
Position.Add(1, 1);
Console.WriteLine(Position);
Position.Add(1, 1);
Console.WriteLine(Position);
Position.Add(1, 1);
Console.WriteLine(Position);
}
}
Struktura obsahuje metodu Add(), která mění hodnoty fieldů x a y. Program se pak třikrát po sobě pokouší voláním této metody změnit data instance struktury v readonly fieldu Position, možná ale někoho překvapí, jaký je výstup tohoto programu:
X = 1, Y = 1
X = 1, Y = 1
X = 1, Y = 1
Co se stalo? Změna dat struktury totiž ve skutečnosti proběhne na kopii (resp. třech různých kopiích) původní hodnoty a vlastní hodnota fieldu Position zůstane netknutá. Specifikace jazyka C# (sekce 7.5.4 a 7.4.4) nám to vysvětluje ve stručnosti takto:
- Výsledkem odkazu na readonly field mimo konstruktor dané instance, ve které je field deklarován, je hodnota (value) (nikoliv proměnná (variable) – pozn. autora).
- Pokud není cíl volání metody klasifikován jako proměnná (variable), je vytvořena dočasná lokální proměnná, do které je obsah cíle přiřazen a volání metody proběhne nad touto lokální proměnnou.
Podrobnější vysvětlení naleznete v článku zde.
Toto je jedna ukázka, proč jsou “mutable” struktury vyloženě špatné. Hodnotové typy sémanticky vyjadřují hodnoty, proto se vždy snažte struktury navrhovat jako immutable tj. tak, že po konstrukci instance struktury se její vnitřní hodnota již nikdy nezmění.
Problémů s mutable strukturami je samozřejmě více, např. třeba již to, že mutable strukturu nelze použít jako klíč v Dictionary nebo pro HashSet apod.
Immutable verze naší struktury bude vypadat takto:
struct Point
{
private readonly int x;
private readonly int y;
public int X { get { return x; } }
public int Y { get { return y; } }
public Point(int x, int y)
{
this.x = x;
this.y = y;
}
public Point Add(int dx, int dy)
{
return new Point(this.X + dx, this.Y + dy);
}
public override string ToString()
{
return string.Format("X = {0}, Y = {1}", this.X, this.Y);
}
}
static class Test
{
static readonly Point Position = new Point(1, 1);
static void Main()
{
Console.WriteLine(Position.Add(1, 1).Add(1, 1).Add(1, 1));
}
}
X = 4, Y = 4
Nyní metoda Add() vytváří a vrací novou instanci struktury tj. novou hodnotu a fieldy x a y mohou být označeny jako readonly.
To nás přivádí k problému druhému a tím jsou právě ony readonly fieldy.
Označení fieldu klíčovým slovem readonly ve struktuře pouze zajistí kontrolu, že daný field není možné ve vnitřní implementaci nastavit mimo konstruktor (což je samo osobě samozřejmě dobré). To o co nám ale hlavně jde je, aby byla celá struktura immutable zvenčí (*), což označení všech jejích fieldů tímto klíčovým slovem vždy zajistit nemusí. Potvrzuje to například následující ukázka:
struct S
{
private readonly int x;
public S(int x) { this.x = x; }
public Bogus(Action action)
{
Console.WriteLine(this.x);
action();
Console.WriteLine(this.x); //different!
}
}
struct T { public S s; ... }
T t = new T(new S(123), ... );
t.s.Bogus(() => { t = default(T); });
Vidíte kde je problém? Problém je v tom, že u struktury není instance vlastníkem storage, ve kterém se data fieldu nacházejí (zatímco u referenčních typů tomu tak vždy je) a tudíž readonly u fieldu nemůže zajistit, že tento storage nebude změněn někým jiným. V ukázce takto akce nepřímo ovlivní i strukturu S.
Z tohoto důvodu klíčové slovo readonly u fieldu struktury vyjadřuje spíše autorův záměr nebo přání nikoliv pevnou skutečnost a na to pozor. (Tímto tématem se více zabývá článek zde a komentáře u článku zde.)
Doufám, že se mi v této sérii podařilo ukázat, že psaní vlastních struktur vyžaduje od autora, aby alespoň trochu věděl co a proč dělá, a že například napsat struct místo class s úmyslem ničím nepodloženého pokusu o nějakou optimalizaci nebude úplně správný přístup.
(*) Z tohoto pohledu je naprosto přípustné, aby případně struktura vnitřně obsahovala i mutable fieldy, např. z důvodu lazy inicializace apod.