Struct Equality

Ondřej Janáček       19.04.2014       C#, Optimalizace       10712 zobrazení

Většina z Vás už jistě slyšela o tom, že když je potřeba šetřit pamětí, tak lze některé třídy nahradit strukturami (struct), které na rozdíl od tříd mohou být uloženy na zásobníku namísto heapu a nemají paměťový overhead. Já se v tomto článku se zaměřím výhradně na porovnávání instancí structů, protože jsem zjistil, že mnoho vývojářů stále neví, jak to dělat správně, což v důsledku na snížení paměťových nároků sice vede, ale přináší to nechtěný výkonnostní overhead.

Začneme tedy vytvořením structu, který nás bude provázet celým článkem.

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;
    }
}

Ačkoliv není cílem tohoto článku ukázat Vám, jak se structy správně vytvářejí, nýbrž jak se správně porovnávají, udělal jsem jej přesto neměnný (immutable), protože to je naprostý základ hodnotových typů. Teď si vytvoříme dvě instance našeho bodu

var point1 = new Point(10, 5);
var point2 = new Point(12, 5);

a porovnáme s tím, co mám k dispozici.

point1.Equals(point2);

Co se doopravdy děje na předchozím řádku? Přesně toto.

IL_001A:  box         Point
IL_001F:  constrained. Point
IL_0025:  callvirt    System.Object.Equals

Nemusíte znát MSIL skrz na skrz, aby Vás napadlo, co tyto tři řádky znamenají. V našem structu Point metodu Equals explicitně uvedenou nemáme, takže se volá na rodiči, což je Object, kde vypadá následovně.

public virtual bool Equals(object obj)
{
   // vnitřek nás začne zajímat v následujících řádcích
}

Je tedy nutné point2 zaboxovat (zabalit do objektu), specifikovat, že se volání metody provede na typu Point a poté zavolat virtuální metodu na rodiči (zdá se, že volání skončí na typu Object, což není tak docela pravda, viz dále). Tohle přesně nám předchozí IL prozradilo. To ale není vše. Jaká je vlastně výchozí implementace Equals?

Podle MSDN se pro structy rovnost dvou instancí definuje metodou Equals v typu ValueType a porovnávají se hodnoty všech instančních proměnných (instance fields) a to dvěma způsoby:

  • byte-by-byte, tj. postupně po bytech
  • field-by-field pomocí reflexe

Reflexe se používá pouze pro proměnné referenčního typu. Není to tedy případ našeho structu, nicméně vyplatí se mít to na paměti. Každopádně, je moudré přepsat Equals, abychom neztráceli výkon na takové jednoduché operaci, jako je porovnání.

public override bool Equals(object obj)
{
    if (obj is Point)
    {
        var p = (Point)obj;
        return p.X == X && p.Y == Y;
    }
    return base.Equals(obj);
}

Pro lepší čitelnost jsem vynechal explicitní kontrolu na null a porovnání run-time typů obou porovnávaných objektů, protože base.Equals(obj) to udělá za mě, v případě potřeby. Já jsem ale přesvědčen, že to nutné nebude, protože porovnávám Point jenom s Point a také vím, že v C# nemohu ze structu dědit a na vstup mi tedy určitě přijde zaboxovaný Point a ne na příklad potencionální potomek Point3D.

Zatím jsme si tedy moc nepomohli. IL pro stejné porovnání pomocí Equals zůstává stejný (stále se volá virtuální metoda na rodiči, která teď alespoň skončí v našem typu). Než se dostanu k "triku", ke kterému mířím už od začátku, je nutné vědět, že pokud přepisujete Equals, je také nutné přepsat GetHashCode, aby se zachovalo správné chování při ukládání do hash tabulek, a také operátory == a !=, aby všechny různé kontroly rovnosti dvou instancí structu vraceli stejné výsledky.

Nakonec, implementujeme rozhraní IEquatable<T>, které obsahuje jedinou metodu, kterou je bool Equals<T>(T other). A právě toto rozhraní je tak málo známé a ještě méně používané.

struct Point : IEquatable<Point>
{
    …
    …
       
    public bool Equals(Point point)
    {
        return point.X == X && point.Y == Y;
    }
}

Teď, když znova porovnáme naše body jako na začátku, dostaneme následující IL.

IL_001A:  call        Point.Equals

Žádné boxování, žádné virtuální volání (single dispatching), ani kontrola typu, pouze prosté zavolání metody a porovnání dvou čísel. Důležité je zmínit, že pokud typ implementuje toto rozhraní, pak má přednost před virtuálním Equals z typu Object (proč tomu tak je se můžete dočíst v mém starším článku o přetěžování metod) a je použito při porovnávání v metodách generických kolekcí jako je Contains. Kompletní seznam těchto metod je k dispozici na MSDN.

 

hodnocení článku

0       Hodnotit mohou jen registrované uživatelé.

 

Nový příspěvek

 

Příspěvky zaslané pod tento článek se neobjeví hned, ale až po schválení administrátorem.

                       
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říspěvky zaslané pod tento článek se neobjeví hned, ale až po schválení administrátorem.

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