Hodnotové typy, část 3

Tomáš Holan       11. 7. 2011       C#       6026 zobrazení

(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:

  1. 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).
  2. 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.

 

hodnocení článku

0       Hodnotit mohou jen registrované uživatelé.

 

Nový příspěvek

 

Využití

Mutabilní struktury vyloženě špatné nejsou, to by pak v C# nebyly. Z hlediska rychlosti by se daly třeba použít ve velkých polích. Ovšem je pravda, že ve většině případů nejsou nejlepší možností a mohou způsobit mnohé komplikace. Ten poslední příklad mě zpočátku hodně překvapil, ale vlastně to dává smysl, když v paměti se prakticky od vytvoření struktura S vyskytuje jen jednou (metodě Bogus se jakoby předá ukazatel na její strukturu, který ale směřuje do T, takže se její obsah při přepsání T přepíše taky, což je mnohdy překvapující, nicméně logické chování). Atribut readonly k poli T.s to samozřejmě opraví, neboť se podle výše zmíněné specifikace struktura S při získání zduplikuje.

nahlásit spamnahlásit spam 0 odpovědětodpovědět
                       
Nadpis:
Antispam: Komu se občas házejí perly?
Příspěvek bude publikován pod identitou   anonym.

Nyní zakládáte pod článkem nové diskusní vlákno.
Pokud chcete reagovat na jiný příspěvek, klikněte na tlačítko "Odpovědět" u některého diskusního příspěvku.

Nyní odpovídáte na příspěvek pod článkem. Nebo chcete raději založit nové vlákno?

 

  • Administrátoři si vyhrazují právo komentáře upravovat či mazat bez udání důvodu.
    Mazány budou zejména komentáře obsahující vulgarity nebo porušující pravidla publikování.
  • Pokud nejste zaregistrováni, Vaše IP adresa bude zveřejněna. Pokud s tímto nesouhlasíte, příspěvek neodesílejte.

přihlásit pomocí externího účtu

přihlásit pomocí jména a hesla

Uživatel:  
Heslo:  

zapomenuté heslo

 

založit nový uživatelský účet

zaregistrujte se

 
zavřít

Nahlásit spam

Opravdu chcete tento příspěvek nahlásit pro porušování pravidel fóra?

Nahlásit Zrušit

Chyba

zavřít

feedback