Operátory == a !=, metody Object.Equals a Object.GetHashCode (Zajímavé konstrukce jazyka C# I.)
- Vložil Trupík 7/30/2006 7:37:41 PM
-
Dalo by se říct, že kdo zná syntaxi jazyka C a C++, nemá v C#
žádný významnější problém, jazyk C# je ale v mnoha směrech
o něco bohatší – je vlajkovou lodí všech jazyků platformy .NET
a je tak velmi těsně svázán s principy této platformy. Díky tomu lze
s chytrým psaním objektů dosáhnout toho, že budoucí kód bude díky
zapouzdření elegantní a přehledný a přitom funkční.
První díl bude porovnávání a souvisejících metodách.
Operátory == a !=
K porovnávání slouží operátory == a !=. Jsou definovány na všech
hodnotových typech (int, DateTime atd., hodnotové typy ale nechám stranou
– u nich je totiž situace jednoduchá) i na třídě
System.Object. Díky tomu, že object tento operátor definuje a tomu, že
každá třída dědí od třídy object nebo nějakého jejího potomka
(i když to není přímo uvedeno v kódu), je možné porovnávat
operátorem == instance libovolných stejných tříd nebo třídy potomka a
předka, i když u těchto tříd operátor == předefinován není.
Lze tedy psát:
String s = "";
object o = new object();
if (s == o)
return;
Takovýto kód se sice bez problémů přeloží a provádí, Visual Studio
u něj ale vypíše Warning:
Possible unintended reference comparison; to get a value comparison, cast the
right hand side to type ‚string‘
Operátor == u třídy object, který dědí i třídy, které ho
nepředefinovávají, totiž funguje jako referenční rovnost (tedy ne
hodnotová rovnost). Vysvětlím na příkladě. Nadeklaruji ukázkovou třídu
User, jejíž instance budu chtít porovnávat.
public class User
{
public string login;
public string pwd;
public User(string login, string pwd)
{
this.login = login;
this.pwd = pwd;
}
}
Uvažujme tento kód:
User c1 = new User("Jakub", "balonek");
User c2 = new User("Jakub", "balonek");
//toto porovnání se vyhodnotí jako nepravda
if (c1 == c2)
return true;
c1 = c2;
//toto porovnání se vyhodnotí jako pravda
if (c1 == c2)
return true;
Referenční rovnost znamená, že porovnávané proměnné odkazují na
stejnou instanci třídy v paměti – na jeden jedinný objekt.
Takže ač se třídy c1 a c2 zdají „stejné“, porovnávání je
vyhodnotí jako rozdílné, protože jsou to dva oddělené objekty.
V druhém příkladě se c2 přiřadí do c1 (původní c1 je tedy
ztracen), takže obsahují reference na stejné objekty v paměti a proto
se porovnání vyhodnotí jako pravdivé.
Toto základní chování dává smysl v mnoha případech, někdy ale
můžeme chtít chování změnit na námi definovanou „rovnost“
objektů – dejme tomu, že dva objekty třídy User budeme brát za
totožné, pokud se shoduje jejich login. K tomu potřebujeme
předefinovat porovnávací operátor a to v C# jde. Rozšíříme tedy
definici třídy User takto:
public static bool operator ==(Customer customer1, Customer customer2)
{
if (customer1.login == customer2.login)
return true;
else return false;
}
Třída nepůjde přeložit, protože pokud definujeme operátor ==, je
nutné definovat také operátor !=. Ten se dá snadno napsat doslova jako opak
rovnosti (a většinou to tak vyhovuje, v některých případech ale
může být implementace != bez == efektivnější):
public static bool operator !=(Customer customer1, Customer customer2)
{
return !(customer1 == customer2);
}
Metoda operator == odpovídá naší definici rovnosti, Původní kód se
bude chovat dle očekávání (obě porovnání vrátí true), ale je tu jeden
problém. Může se stát, že jeden z porovnávaných objektů bude mít
hodnotu null a v tom případě přístup na jeho položku login vyhodí
výjimku NullReferenceException.
Rada 1: vždy počítejte s tím, že porovnávané objekty mohou mít
hodnotu null.
Upravíme tedy definici operátoru dle zavedeného standardu – pokud
jsou oba objekty null, == vrací true, pokud je pouze jeden z nich null,
== vrací false, nabízí se takováto implementace:
public static bool operator ==(Customer customer1, Customer customer2)
{
if (customer1 == null && customer2 == null)
return true;
if (customer1 == null || customer2 == null)
return false; //oba null uz urcite nejsou
if (customer1.login == customer2.login)
return true;
else return false;
}
kód se přeloží, ale při prvním zavolání se ukazuje další problém
– volání skončí vyjímkou StackOverflowException. Proč?
Uvnitř definice operátoru == se používá znovu operátor == (při
porovnání s null), takže se volá znovu celá porovnávací metoda
v nekonečné rekurzi, která skončí přeplněním zásobníku. Jak
z toho ven? Porovnat s null je přeci potřeba! Stačí přetypovat
na typ object (od kterého třída vždy dědí), ten má už definovaný
operátor == jako referenční hodnost a ta nám tady vyhovuje. Takže vzorová
implementace operatoru == může vypadat takto:
public static bool operator ==(Customer customer1, Customer customer2)
{
if ((object)customer1 == null && (object)customer2 == null)
return true;
if ((object)customer1 == null || (object)customer2 == null)
return false; //oba null uz urcite nejsou
if (customer1.login == customer2.login)
return true;
else return false;
}
Rada 2: uvnitř operatoru == přetypovat před porovnáním s null na
typ object (nebo na jiného předka).
Metody GetHashCode a Equals
Nyní už kód pracuje jak má. Kompilátor ale stále dává varování:
Customer defines operator == or operator != but does not override
Object.Equals(object o) Customer defines operator == or operator != but
does not override Object.GetHashCode()
Metody Equals a GetHashCode jsou virtuální metody třídy object a tak je
dědí i třída Customer. Je doporučeno je předefinovat u všech
tříd, které předefinovávají operátor ==. I když vám to může
připadat zbytečné, protože tyto metody nepotřebujete, můžete je volat a
ani o tom nemusíte vědět.
Libovolnou třídu lze totiž použít jako klíč v hashovacích
tabulkách, slovnícich a podobných třídách (třídy
Dictionary<key,value>, třídy s interface IDictionary atd.).
Do těchto tříd se pod klíč ukládá nějaká hodnota. Lze pak psát
například
System.Collections.Generic.Dictionary<Customer, string> d = new Dictionary<Customer, string>();
d[c1] = "popisek1";
d[c2] = "popisek2"; //přepíše "popisek1", pokud c1 je rovno c2.
Tyto „slovníkové“ třídy by měly fungovat tak, že pokud
ještě klíč ve slovníku není, tak se tam pří předešlém zápisu
přidá, pokud tam již je, tak se stávající klíč nezmění a změní se
jen přiřazená hodnota. Při standartním chování a za předpokladu, že c1
je rovno c2 měl skončit tak, že ve slovníku d bude jeden pár
klíč/hodnota, klíčem bude objekt typu Customer a přiřazenou hodnotou
„popisek2“;
Háček je v tom, že když třída zjišťuje, zda již neobsahuej
daný klíč, tak nevolá operátor ==, jak by někdo mohl předpokládat, ale
právě metody GetHashCode() a Equals(). Pokud nepředefinujete metodu
Equals(), bude fungovat opět jako referenční rovnost a tak bude v
některých případech vracet jiné výsledky, než již předefinovaný operátor
==! Přitom by měly vracet výsledky stejné.
Rada 3: Pokud předefinujete porovnávací operátory, pro jistotu
odpovídajícím způsobem předefinujte
také porovnávací metody Equals a GetHashCode. Pokud používáte slovníkové třídy,
je to nutnost!
K ověřování rovnosti jsou metody dvě z důvodu efektivity.
Porovnávání může být u složitějších tříd poměrně
komplikovanou a časově náročnou operací, a například při třídění a
vyhledávání ve větším množství objektů se porovnávání provádí
mnohokrát. Proto aby se zrychlilo, jsou metody dvě.
Nejprve se volá GetHashCode(), která by měla být rychlá. Vrací
nejjednodušší typ – typ int. Musí platit, že dvě dle naší
definice „sobě rovné“ třídy musí vracet stejné číslo při
volání GetHashCode(). Pokud stejné číslo vrátí i dvě různé
třídy, tak to nevadí. GetHashCode by měla být skutečně co nejrychlejší
a zároveň by měla co možná nejméně vracet stejné číslo u
„různých“ objektů. Ukázková třída bod se dvěma
souřadnicemi by mohla vypadat například takto:
class Bod
{
public int X, Y;
public override int GetHashCode()
{
return X * Y
}
}
Je vidět, že výraz X * Y bude vracet stejné hodnoty i pro některé
různé dvojice X a Y, ale můžeme se s takovou implementací spokojit.
U složitějších tříd (složených z dalších tříd apod.) se
obvykle GetHashCode implementuje tak, že se zavolá GetHashCode na některé
(nebo všechny) položky třídy a s nimi se provede nějaká aritmetická
operace.
Tedy jednoduše – GetHashCode rychle odbyde většinu porovnání, a ty
rozdíly, které nedokáže rozeznat, přenechá metodě Equals. Metoda Equals
řeší konflikty způsobené právě nedokonalostí GetHashCode. Její
signatura je
public override bool Equals(object obj)
Metoda musí mimo jiné pořešit to, že parametr (předaný jako obecný
typ object), musí být stejného typu jako objekt, k němuž metoda
Equals patří, takže implementace by mohla vypadat třeba takto (pokud již
máme operátor ==)
public override bool Equals(object obj)
{
if (obj == null || GetType() != obj.GetType()) return false;
return (this == obj);
}
Rada 4: nezapomeňte, že metoda Equals může dostat za parametr objekt
jiného typu, než je objekt, na kterém se volá.
Operátor == musí počítat s tím, že oba z operandů mohou být
null, ale v metodě Equals může být null pouze obj (this nikdy), takže
by se kód dal trochu vylepšit (ale nijak významně).
Ohodnoťte prosím užitečnost článku