Porównania równości w języku C#

Wskazówka

Ten artykuł jest częścią sekcji Podstawy dla deweloperów, którzy już znają co najmniej jeden język programowania i uczą się języka C#. Jeśli dopiero zaczynasz programować, najpierw zacznij od samouczków Wprowadzenie .

Pochodzi z innego języka? W Javie == dla obiektów i w języku JavaScript === dla obiektów sprawdzają tożsamość, a nie zawartość. Klasy języka C# działają tak samo jak domyślnie. W Python == domyślnie wywołuje __eq__ i testuje zawartość, podobnie jak w przypadku porównywania rekordów języka C#. C# struktury również są porównywane według wartości, gdy wywołasz Equals.

Język C# rozróżnia dwa rodzaje równości. Równość wartości oznacza, że dwa wystąpienia są równe, gdy ich dane są zgodne. Równość odwołań oznacza, że dwie zmienne są równe tylko wtedy, gdy wskazują ten sam obiekt w pamięci. Ten warunek jest również nazywany tożsamością. Typy wartości zwykle porównują dane, a typy referencyjne zwykle porównują tożsamość. Autorzy typów mogą zmienić te wartości domyślne, ale model mentalny uniemożliwia subtelne błędy, w których dwa obiekty, które wyglądają identycznie, nie są traktowane jako równe lub gdy mutacja za pomocą jednej zmiennej dyskretnie zmienia to, co widzi inna zmienna.

Typy wartościowe, typy referencyjne i domyślne reguły równości

Każdy typ w języku C# jest typem wartości lub typem odwołania. Typ wartości przechowuje swoje dane bezpośrednio w zmiennej. Typ odwołania zawiera odwołanie do obiektu. Po przypisaniu zmiennej typu odwołania do innej zmiennej oba zmienne odwołują się do tego samego obiektu. Aby uzyskać więcej informacji o typach wartości i typach referencyjnych, zobacz Omówienie systemu typów.

Domyślne zachowanie równości zwykle jest zgodne z typem:

  • Wbudowane typy liczbowe i wyliczenia są typami wartości. Dwie int zmienne są równe, gdy ich wartości liczbowe są zgodne.
  • Struktury są typami wartości. Zwykły struct używa porównania wartości podczas wywołania Equals.
  • Krotki są typami wartości. Dwie krotki są równe, gdy wartości wszystkich ich elementów są takie same.
  • Klasy są typami referencyjnymi. Klasa zwykła używa równości odwołań, więc == i Equals przetestuj, czy dwie zmienne wskazują ten sam obiekt.

Klasa używa równości odwołań. Dwa oddzielne obiekty z tymi samymi danymi nie są równe, ale dwie zmienne odwołujące się do tego samego obiektu są równe:

var order1 = new Order(42, "Shoes");
var order2 = new Order(42, "Shoes");

Console.WriteLine(order1 == order2);               // => False
Console.WriteLine(order1.Equals(order2));          // => False
Console.WriteLine(ReferenceEquals(order1, order2)); // => False

Order order3 = order1;
Console.WriteLine(order1 == order3);               // => True

Element struct pokazuje równość wartości za pomocą elementu Equals. Dwie instancje struktury są sobie równe, gdy ich pola są identyczne:

var pt1 = new Point(3, 4);
var pt2 = new Point(3, 4);

Console.WriteLine(pt1.Equals(pt2)); // => True

Zwykłe struktury nie otrzymują wstępnie zdefiniowanego == operatora. Zapis p1 == p2 w zwykłej strukturze kompiluje się tylko wtedy, gdy struktura deklaruje własny operator ==. Jeśli potrzebujesz porównań operatorów dla struktury, zdefiniuj == element i != jako parę i zachowaj ich spójność z elementami Equals i GetHashCode.

Krotki są też typami wartości. Dwie krotki są równe, gdy wartości wszystkich elementów są takie same. Nazwy elementów w krotce nazwanej są udogodnieniem na etapie kompilacji i nie są brane pod uwagę przy porównywaniu. Ważne są tylko pozycje i wartości:

var t1 = (Name: "Grace", Role: "Engineer");
var t2 = (Name: "Grace", Role: "Engineer");

Console.WriteLine(t1 == t2); // => True

Aby uzyskać więcej informacji na temat składni krotek i dekonstrukcji, zobacz Krotki i dekonstrukcja.

Użyj Object.ReferenceEquals, aby bezpośrednio sprawdzić tożsamość

ReferenceEquals zawsze testuje tożsamość niezależnie od tego, jak typ zastępuje Equals lub przeciąża ==element . Użyj go jako diagnostyki tożsamości, gdy musisz potwierdzić, czy dwie zmienne wskazują dokładnie ten sam obiekt:

var doc1 = new Document("Report");
var doc2 = new Document("Report");
var doc3 = doc1;

Console.WriteLine(ReferenceEquals(doc1, doc2)); // => False
Console.WriteLine(ReferenceEquals(doc1, doc3)); // => True

Typowym zastosowaniem jest użycie tego wewnątrz przesłonięcia Equals, aby pominąć pełne porównanie: gdy oba argumenty są tą samą referencją, są zawsze równe bez sprawdzania poszczególnych pól.

Uwaga / Notatka

Gdy zmienne są wpisywane jako interfejs, sprawdza, == czy zmienne interfejsu odwołują się do tego samego obiektu. Wywołanie elementu Equals nadal uruchamia implementację danego obiektu.

Uwaga / Notatka

ReferenceEquals funkcja zawsze zwraca wartość false podczas porównywania typów wartości, nawet jeśli oba argumenty zawierają te same wartości. To zachowanie występuje, ponieważ każdy argument typu wartości jest niezależnie opakowywany do osobnego obiektu na stercie podczas przekazywania do ReferenceEquals.

Typy mogą definiować różne semantyki równości

Typy mogą definiować semantykę równości, która różni się od domyślnego zachowania. Najczęstszą przyczyną jest zaimplementowanie równości wartości. Jeśli utworzysz typ reprezentujący dane, takie jak konto bankowe, produkt w spisie lub użytkownik w systemie, rozważ wystąpienia z tymi samymi wartościami co równe. Wybierz typy rekordów do implementowania równości wartości, a kompilator generuje wszystkie niezbędne elementy członkowskie równości.

Uwaga / Notatka

Łańcuchy są klasami, ale == i Equals porównują treść łańcucha, a nie tożsamość.

  • ==: operator równości. Większość typów używa tego operatora jako podstawowej metody sprawdzania równości. Jego działanie zależy od tego, czy typ ma operator == wbudowany, czy zdefiniowany przez użytkownika.
  • != – operator nierówności. Jeśli typ definiuje operator == zdefiniowany przez użytkownika, musi również definiować !=.
  • Equals: metoda wirtualna dziedziczona przez każdy typ. Można go przesłonić, aby zmienić semantykę równości dla danego typu.
  • GetHashCode: metoda wirtualna używana przez kolekcje oparte na skrótach. Gdy dwie wartości są równe, ich kody skrótów również muszą być równe.
  • ReferenceEquals: metoda statyczna, która zawsze testuje tożsamość.

Używaj rekordów do porównywania wartości

Użyj modyfikatora record, aby nadać typowi równość wartości opartą na danych, jeśli typ może być rekordem. Kompilator generuje składowe Equals, GetHashCode i ==/!=, które porównują wartość każdej zadeklarowanej właściwości.

Element record class jest nadal typem odwołania, ale porównuje wartości zamiast tożsamości:

var person1 = new Person("Ada", "Lovelace");
var person2 = new Person("Ada", "Lovelace");

Console.WriteLine(person1 == person2);               // => True
Console.WriteLine(person1.Equals(person2));          // => True
Console.WriteLine(ReferenceEquals(person1, person2)); // => False

ReferenceEquals potwierdza, że person1 i person2 są różnymi obiektami w pamięci, podczas gdy == i Equals zwracają True, ponieważ wygenerowane przez kompilator porównanie równości porównuje wartości właściwości.

Ta sama generacja kompilatora ma zastosowanie do record struct typów:

var dim1 = new Dimension(1920, 1080);
var dim2 = new Dimension(1920, 1080);

Console.WriteLine(dim1 == dim2);      // => True
Console.WriteLine(dim1.Equals(dim2)); // => True

Typy rekordów generują cały zestaw równości dla własnego typu. Typy record class i record struct przesłaniają Equals i GetHashCode. Generują również operatory == i !=, a także typizowaną metodę Equals dla typu rekordowego. W przeciwieństwie do zwykłego elementu struct, element record struct automatycznie obsługuje również == i !=. Aby uzyskać więcej informacji na temat typów rekordów i ich semantyki równości, zobacz Rekordy.

Rekordy ze składowymi typu referencyjnego

Równość rekordów opiera się na własnej semantyce równości ich składowych. Każda właściwość lub pole jest porównywana przy użyciu własnej Equals metody. W przypadku większości wartości skalarnych, takich jak int, stringlub DateTime, to podejście porównuje wartości elementów członkowskich rekordu. Subtelność pojawia się w przypadku typowych modyfikowalnych kolekcji, takich jak List<T> lub T[]: te typy są porównywane według referencji, więc dwie instancje rekordów zawierające różne obiekty list o tej samej zawartościnie są uznawane za równe przez wygenerowany mechanizm porównywania równości rekordów.

var playlist1 = new Playlist("Chill", new List<string> { "Song A", "Song B" });
var playlist2 = new Playlist("Chill", new List<string> { "Song A", "Song B" });

Console.WriteLine(playlist1.Equals(playlist2));                        // => False (different List instances)
Console.WriteLine(playlist1.Tracks.SequenceEqual(playlist2.Tracks));   // => True

playlist1 i playlist2 są oddzielnymi List<string> wystąpieniami. Mimo że ich zawartość jest zgodna, Equals funkcja zwraca wartość false.

Jeśli potrzebujesz równości rekordów, aby odzwierciedlić zawartość kolekcji, masz kilka opcji:

  • Zaimplementuj IEquatable<T> w rekordzie i przesłoń Equals, aby używać Enumerable.SequenceEqual dla elementów kolekcji.
  • Użyj typu kolekcji z porównywaniem według wartości — na przykład niestandardowego IEqualityComparer<T> lub typu, którego własne Equals porównuje elementy.
  • Projektowanie wokół tożsamości: jeśli rekord reprezentuje jednostkę, a nie czystą wartość, równość referencyjna dla jej składowych kolekcji może być celowa.

Ważna

Ręczna implementacja równości jest rzadka dzisiaj w języku C#. Rekordy obsługują typowy scenariusz równości wartości automatycznie. Jeśli musisz ręcznie zaimplementować równość — na przykład dlatego, że typ musi pochodzić z klasy bazowej innej niż rekord — zobacz Implementowanie równości samodzielnie, gdy typ nie może być rekordem w dokumentacji językowej.

Zobacz także