Reference Equality – co i jak

Tym wpisem chciałbym rozpocząć serię o trzewiach c# i ILAssembly. Ostatnimi czasy chyba dwa razy słyszałem pytanie, czym się różni operator == od metody Equals. Czy zawsze porównują referencję, czy to jest to samo? W tym wpisie chciałbym pokazać metody porównywania typów referencyjnych, dostępnych w c#. Również spojrzeć troszkę niżej i zobaczyć jak takie metody są zaimplementowane i jaki kod IL generują.

 

Więc na jakie sposoby możemy porównać dwa obiekty. Poniżej przedstawiłem wszystkie znane mi metody:

Jako że klasa Foo nie dostarcza nam, żadnej implementacji operatora == oraz metody Equals, wywoływane są bazowe z obiektu bazowego typu Object.

Operator ==

 

Operator == zawsze będzie porównywać referencję przekazanych obiektów. Na dowód tego, poniżej zamieściłem kod z IlSpy jaki został wygenerowany właśnie dla tego operatora.

Najważniejszym elementem powyższego kodu jest instrukcja ceq. Jak z dokumentacji wynika, ceq porównuje dwie wartości ze stosu, jeżeli są te same zwraca 1, w przeciwnym wypadku zwraca 0. W naszym przypadku wartościami są referencję do instancji klas Foo i dla nich następuje porównanie.

Metoda Equals

Jeżeli metoda Equals nie jest przeciążona, wywoływana jest metoda bazowa z klasy Object, metoda zwróci true w momencie gdy referencje obiektów będą te same. Zobaczmy jak ona jest zaimplementowana:

No jak widać nie za wiele widać. Atrybut MethodImplOptions.InternalCall  informuje nas, że implementacja metody jest w samym CLR. Jeszcze się nie doszukałem tego w kodzie cpp. Jak ktoś znajdzie dajcie znać.

Jeszcze kod w IL:

A więc potwierdzenie, wywołana metoda wirtualna Equals.

W tym linku znajdziemy potwierdzenie tego co napisałem, plus parę wskazówek kiedy i co najlepiej stosować.

Metoda Object.ReferenceEquals

Metoda statyczna klasy Object. Porównuje zawsze referencje. Na dowód tego kodzik źródłowy:

Wywołanie operatora ==. Jako że jest to metoda statyczna więc nie ma możliwości nadpisania jej w żaden sposób. Wywołując tą metodę mamy pewność, że zawsze wykonamy porównanie referencji.

Metoda Object.Equals

W pierwszej kolejności porównywana jest referencja. Następnie sprawdzane jest czy obiekty nie są null. Na końcu wywoływane jest Equals na pierwszej instancji.

Na podstawie powyższych przykładów można wyciągnąć jeden bardzo istotny wniosek.

Dla typów referencyjnych, jeżeli nie dostarczymy jawnych implementacji operatora == lub metody Equals, powyższe wywołania zawsze porównują referencję obiektów.

Dla potwierdzenia powyższego zaimplementujmy np. Equals:

I jeszcze porównanie za pomocą statycznej metody Equals:

Wynikiem tego porównania będzie oczywiście True. Jako, że operator == zwrócił False, oraz żadne z obiektów nie jest null, wykonana została metoda Equals. Jest ona przeciążona w klasie Bar, więc ta implementacja została wywołana.

Podsumowanie

W powyższym wpisie opisałem różne metody porównywania obiektów referencyjnych. Chciałbym nadmienić, że rozpatrywałem wyłącznie nasze implementacje. I zasada którą przytoczyłem dotyczy wyłącznie obiektów nie implementujących operatorów i metod. Należy o tym pamiętać. Zwrócić należny uwagę na np.  klasę string która posiada takie implementacje i niekoniecznie musi się zachowywać w dokładnie taki sam sposób. Omówienie klasy string zachowam sobie na inny wpis.