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.