Dlaczego C # i Java używają równości odniesienia jako wartości domyślnej dla „==”?

32

Zastanawiałem się przez jakiś czas, dlaczego Java i C # (i jestem pewien, że inne języki) domyślnie odnoszą się do równości ==.

W programowaniu, które wykonuję (co z pewnością jest tylko niewielkim podzbiorem problemów programistycznych), prawie zawsze chcę logicznej równości podczas porównywania obiektów zamiast równości odniesienia. Próbowałem wymyślić, dlaczego oba te języki poszły tą drogą, zamiast odwracać ją i ==być logiczną równością i używać .ReferenceEquals()do odniesienia równości.

Oczywiście stosowanie równości referencyjnej jest bardzo proste do wdrożenia i zapewnia bardzo spójne zachowanie, ale nie wydaje się, aby dobrze pasowało do większości praktyk programistycznych, które widzę dzisiaj.

Nie chcę wydawać się nieświadomym problemów z próbą wprowadzenia logicznego porównania i że musi ono zostać zaimplementowane w każdej klasie. Zdaję sobie również sprawę, że te języki zostały zaprojektowane dawno temu, ale ogólne pytanie pozostaje bez odpowiedzi.

Czy jest jakaś znacząca zaleta, że ​​nie wywiązuję się z tego, czego mi po prostu brakuje, czy też wydaje się uzasadnione, że domyślnym zachowaniem powinna być logiczna równość, a w przypadku powrotu do równości referencyjnej logiczna równość nie istnieje dla klasy?

Zamek błyskawiczny
źródło
3
Ponieważ zmienne są referencjami? Ponieważ zmienne działają jak wskaźniki, sensowne jest porównywanie ich w ten sposób
Daniel Gratzer,
C # używa logicznej równości dla typów wartości, takich jak struktury. Ale jaka powinna być „domyślna logiczna równość” dla dwóch obiektów różnych typów referencji? Lub dla dwóch obiektów, w których jeden jest typu A odziedziczony po B? Zawsze „fałsz” jak dla struktur? Nawet jeśli masz ten sam obiekt dwukrotnie, najpierw jako A, a następnie jako B? To nie ma dla mnie większego sensu.
Doc Brown,
3
Innymi słowy, czy pytasz dlaczego w C #, jeśli przesłonisz Equals(), to nie zmienia automatycznie zachowania ==?
svick

Odpowiedzi:

29

C # robi to, ponieważ Java. Java tak zrobiła, ponieważ Java nie obsługuje przeciążania operatora. Ponieważ równość wartości musi zostać na nowo zdefiniowana dla każdej klasy, nie mógł być operatorem, ale musiał być metodą. IMO to była zła decyzja. O wiele łatwiej jest pisać i czytać a == bniż o a.equals(b)wiele bardziej naturalnie dla programistów z doświadczeniem w C lub C ++, ale a == bprawie zawsze się myli. Błędy z wykorzystaniem ==gdzie .equalswymagany był zmarnowany niezliczone tysiące programistów godzin.

Kevin Cline
źródło
7
Myślę, że jest tylu zwolenników przeciążenia operatora, co przeciwników, więc nie powiedziałbym, że „to była zła decyzja” jako absolutne stwierdzenie. Przykład: w projekcie C ++, w którym pracuję, przeciążiliśmy ==wiele klas i kilka miesięcy temu odkryłem, że kilku programistów nie wiedziało, co ==właściwie robi. Zawsze istnieje takie ryzyko, gdy semantyka jakiegoś konstruktu nie jest oczywista. equals()Zapis mówi mi, że używam niestandardową metodę i że muszę szukać go gdzieś. Podsumowując: Myślę, że przeciążenie operatora jest ogólnie kwestią otwartą.
Giorgio
9
Powiedziałbym, że Java nie ma przeciążenia operatora zdefiniowanego przez użytkownika . Wiele operatorów ma podwójne (przeciążone) znaczenie w Javie. Spójrz na +przykład, który jednocześnie dodaje (wartości liczbowe) i konkatenację łańcuchów.
Joachim Sauer
14
Jak może a == bbyć bardziej naturalne dla programistów z doświadczeniem C, skoro C nie obsługuje przeciążenia operatora zdefiniowanego przez użytkownika? (Na przykład metoda C porównywania łańcuchów strcmp(a, b) == 0nie jest a == b.)
svick
Tak zasadniczo myślałem, ale pomyślałem, że poproszę osoby z większym doświadczeniem, aby upewnić się, że nie umknie mi coś oczywistego.
Zipper
4
@svick: w C nie ma żadnego typu ciągu ani żadnych typów odniesień. Operacje na łańcuchach są wykonywane za pośrednictwem char *. Wydaje mi się oczywiste, że porównanie dwóch wskaźników równości nie jest tym samym, co porównanie ciągów znaków.
kevin cline
15

Krótka odpowiedź: spójność

Aby jednak odpowiedzieć na twoje pytanie, proponuję zrobić krok do tyłu i przyjrzeć się temu, co oznacza równość w języku programowania. Istnieją co najmniej TRZY różne możliwości, które są używane w różnych językach:

  • Równość odniesienia : oznacza, że ​​a = b jest prawdziwe, jeśli aib odnoszą się do tego samego obiektu. Nie byłoby prawdą, gdyby aib odnosiły się do różnych obiektów, nawet gdyby wszystkie atrybuty aib były takie same.
  • Płytka równość : oznacza, że ​​a = b jest prawdziwe, jeśli wszystkie atrybuty obiektów, do których odnoszą się a i b, są identyczne. Płytką równość można łatwo wdrożyć przez bitowe porównanie przestrzeni pamięci reprezentującej dwa obiekty. Należy pamiętać, że równość odniesienia oznacza płytką równość
  • Głęboka równość : oznacza, że ​​a = b jest prawdą, jeśli każdy atrybut aib jest albo identyczny albo głęboko równy. Należy pamiętać, że głęboka równość jest implikowana zarówno przez równość odniesienia, jak i równość płytką. W tym sensie głęboka równość jest najsłabszą formą równości, a równość odniesienia jest najsilniejsza.

Te trzy typy równości są często używane, ponieważ są wygodne do wdrożenia: wszystkie trzy kontrole równości mogą być łatwo wygenerowane przez kompilator (w przypadku głębokiej równości kompilator może wymagać użycia bitów znaczników, aby zapobiec nieskończonym pętlom, jeśli struktura do porównania ma odniesienia cykliczne). Istnieje jednak inny problem: żaden z nich może nie być odpowiedni.

W systemach nietrywialnych równość obiektów jest często definiowana jako coś pomiędzy równością głęboką a równością odniesienia. Aby sprawdzić, czy chcemy uznać dwa obiekty za równe w pewnym kontekście, możemy wymagać porównania niektórych atrybutów z tym, gdzie stoi w pamięci, a innych głębokiej równości, podczas gdy niektóre atrybuty mogą być czymś zupełnie innym. To, co naprawdę chcielibyśmy, to „czwarty typ równości”, naprawdę miły, często nazywany w literaturze równością semantyczną . Rzeczy są równe, jeśli są równe, w naszej domenie. =)

Możemy więc wrócić do twojego pytania:

Czy jest jakaś istotna korzyść z niewywiązania się z tego, czego mi po prostu brakuje, czy też wydaje się uzasadnione, że domyślnym zachowaniem powinna być logiczna równość, i powrót do równości referencyjnej, jeśli nie istnieje logiczna równość dla klasy?

Co mamy na myśli, gdy piszemy „a == b” w dowolnym języku? Idealnie powinno być zawsze tak samo: równość semantyczna. Ale to nie jest możliwe.

Jednym z głównych założeń jest to, że przynajmniej dla prostych typów, takich jak liczby, oczekujemy, że dwie zmienne są równe po przypisaniu tej samej wartości. Patrz poniżej:

var a = 1;
var b = a;
if (a == b){
    ...
}
a = 3;
b = 3;
if (a == b) {
    ...
}

W tym przypadku oczekujemy, że „a jest równe b” w obu instrukcjach. Wszystko inne byłoby szalone. Większość (jeśli nie wszystkie) języków jest zgodna z tą konwencją. Dlatego dzięki prostym typom (czyli wartościom) wiemy, jak osiągnąć równość semantyczną. W przypadku obiektów może to być coś zupełnie innego. Patrz poniżej:

var a = new Something(1);
var b = a;
if (a == b){
    ...
}
b = new Something(1);
a.DoSomething();
b.DoSomething();
if (a == b) {
    ...
}

Oczekujemy, że pierwsze „jeśli” zawsze będzie prawdziwe. Ale czego oczekujesz od drugiego „jeśli”? To naprawdę zależy. Czy „DoSomething” może zmienić (semantyczną) równość aib?

Problem z równością semantyczną polega na tym, że kompilator nie może automatycznie wygenerować obiektów, ani nie wynika to z przypisań . Użytkownik musi zapewnić mechanizm definiowania równości semantycznej. W językach obiektowych mechanizm ten jest dziedziczoną metodą: równa się . Czytając fragment kodu OO, nie oczekujemy, że metoda będzie miała taką samą dokładną implementację we wszystkich klasach. Jesteśmy przyzwyczajeni do dziedziczenia i przeciążania.

Jednak w przypadku operatorów oczekujemy tego samego zachowania. Kiedy zobaczysz „a == b”, powinieneś spodziewać się tego samego rodzaju równości (od 4 powyżej) we wszystkich sytuacjach. Tak więc, dążąc do spójności , projektanci języków stosowali równość odniesienia dla wszystkich typów. Nie powinno to zależeć od tego, czy programista zastąpił metodę, czy nie.

PS: Język Dee jest nieco inny niż Java i C #: operator równości oznacza płytką równość dla prostych typów i równość semantyczną dla klas zdefiniowanych przez użytkownika (z odpowiedzialnością za wdrożenie operacji = leżącej po stronie użytkownika - nie podano domyślnej). Ponieważ w przypadku prostych typów płytka równość jest zawsze równością semantyczną, język jest spójny. Cena, którą płaci, polega jednak na tym, że operator równości jest domyślnie niezdefiniowany dla typów zdefiniowanych przez użytkownika. Musisz to zaimplementować. A czasem jest to po prostu nudne.

Hbas
źródło
2
When you see ‘a == b’ you should expect the same type of equality (from the 4 above) in all situations.Projektanci języka Java używali równości odniesienia dla obiektów i równości semantycznej dla prymitywów. Nie jest dla mnie oczywiste, że była to właściwa decyzja lub że ta decyzja jest bardziej „konsekwentna” niż pozwalająca ==na przeciążenie semantyczną równością obiektów.
Charles Salvia,
Użyli „ekwiwalentu równości odniesienia” również dla prymitywów. Kiedy używasz „int i = 3”, nie ma wskaźników dla liczby, więc nie możesz używać referencji. W przypadku łańcuchów, „rodzaju” prymitywnego typu, jest to bardziej oczywiste: musisz użyć „.intern ()” lub bezpośredniego przypisania (String s = „abc”), aby użyć == (równość odniesienia).
Hbas
1
PS: C #, z drugiej strony, nie był zgodny z jego łańcuchami. I w tym przypadku IMHO jest znacznie lepiej.
Hbas
@CharlesSalvia: W Javie, jeśli ai bsą tego samego typu, wyrażenie a==bsprawdza, czy ai btrzyma to samo. Jeśli jeden z nich zawiera odniesienie do obiektu # 291, a drugi zawiera odniesienie do obiektu # 572, nie przechowują tego samego. Na zawartość obiektu # 291 i # 572 może być równoważne, ale same zmienne trzymać różne rzeczy.
supercat
2
@CharlesSalvia Został zaprojektowany w taki sposób, abyś mógł zobaczyć a == bi wiedzieć, co robi. Podobnie możesz zobaczyć a.equals(b)i założyć, że przeciążenie equals. Jeśli a == bpołączenia a.equals(b)(jeśli są realizowane), czy są porównywane przez odniesienie czy treść? Nie pamiętasz Musisz sprawdzić klasę A. Kod nie jest już tak szybki do odczytania, jeśli nie jesteś nawet pewien, jak się nazywa. Byłoby tak, jakby dozwolone były metody o tej samej sygnaturze, a wywoływana metoda zależy od bieżącego zakresu. Takie programy byłyby niemożliwe do odczytania.
Neil
0

Próbowałem wymyślić, dlaczego oba te języki poszły tą drogą, zamiast odwrócić ją i mieć == być logiczną równością i użyć .ReferenceEquals () do równości odniesienia.

Ponieważ to drugie podejście byłoby mylące. Rozważać:

if (null.ReferenceEquals(null)) System.out.println("ok");

Czy ten kod powinien się wydrukować "ok", czy powinien rzucić NullPointerException?

Atsby
źródło
-2

W przypadku Java i C # korzyść polega na tym, że są zorientowane obiektowo.

Z punktu widzenia wydajności - łatwiejszy do pisania kod powinien również być szybszy: ponieważ OOP zamierza reprezentować logicznie odrębne elementy przez różne obiekty, sprawdzenie równości referencji byłoby szybsze, biorąc pod uwagę, że obiekty mogą stać się dość duże.

Z logicznego punktu widzenia - równość obiektu z innym nie musi być tak oczywista, jak porównanie właściwości obiektu dla równości (np. Jak logicznie interpretowana jest null == null? To może różnić się w zależności od przypadku).

Myślę, że sprowadza się to do spostrzeżenia, że ​​„zawsze chcesz logicznej równości zamiast równości odniesienia”. Konsensus między projektantami języków był prawdopodobnie przeciwny. Osobiście trudno mi to ocenić, ponieważ brakuje mi szerokiego spektrum doświadczenia w programowaniu. Z grubsza używam równości odniesienia bardziej w algorytmach optymalizacyjnych, a równości logicznej więcej w obsłudze zestawów danych.

Rafael Emshoff
źródło
7
Równość odniesienia nie ma nic wspólnego z orientacją obiektową. Wręcz przeciwnie: jedną z podstawowych właściwości orientacji obiektowej jest to, że obiekty, które zachowują się tak samo, są nierozróżnialne. Jeden obiekt musi być w stanie symulować inny obiekt. (W końcu OO został wymyślony do symulacji!) Równość odniesienia pozwala rozróżnić dwa różne obiekty, które zachowują się tak samo, pozwala odróżnić obiekt symulowany od rzeczywistego. Dlatego Równość odniesienia przerywa orientację obiektową. Program OO nie może wykorzystywać równości odniesienia.
Jörg W Mittag
@ JörgWMittag: Aby poprawnie wykonać program zorientowany obiektowo, musi istnieć sposób zapytania obiektu X, czy jego stan jest równy Y [stan potencjalnie przejściowy], a także sposób zapytania obiektu X, czy jest on równoważny Y [X jest równoważne Y tylko wtedy, gdy jego stan jest gwarantowany na wieczność równy Y]. Posiadanie oddzielnych wirtualnych metod równoważności i równości stanu byłoby dobre, ale dla wielu typów nierówność referencyjna implikuje brak równoważności i nie ma powodu, aby poświęcać czas na wysyłanie metod wirtualnych, aby to udowodnić.
supercat
-3

.equals()porównuje zmienne według ich zawartości. zamiast ==tego porównuje obiekty według ich zawartości ...

używanie obiektów jest bardziej dokładne .equals()

Nuno Dias
źródło
3
Twoje założenie jest nieprawidłowe. .equals () robi wszystko, co .equals () zostało do tego zakodowane. Zwykle jest to treść, ale nie musi tak być. Również nie jest bardziej dokładne użycie .equals (). To zależy tylko od tego, co próbujesz osiągnąć.
Zipper