Poznámka: články jsou již mnoho let staré, doba se posunula, mnoho věcí v nich doporučovaných je již dnes překonané - berte s rezervou!


Operátory == a !=, metody Object.Equals a Object.GetHashCode (Zajímavé konstrukce jazyka C# I.)

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 StackOverflowEx­ception. 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(ob­ject o) Customer defines operator == or operator != but does not override Object.GetHashCo­de()

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




21
 
23
 
7
 
9
 
0
 
 
Vložit komentář:
 

 



 

 

Nepoužívejte žádné html ani texy značky, odřádkování se zachová. Pokud uvádíte zdrojový kód, můžete ho vložit mezi značky
<syntax jazyk="PHP">...</syntax>,
bude potom zformátován. Jako atribut můžete uvést PHP, C#, HTML, CSS a mnoho dalších.


opiste cislo Opište číslo: