Operatory równości — testowanie, czy dwa obiekty są równe, czy nie

Operatory == (równości) i!=(nierówności) sprawdzają, czy ich operandy są równe, czy nie. Typy wartości są równe, gdy ich zawartość jest równa. Typy referencyjne są równe, gdy dwie zmienne odwołują się do tego samego magazynu.

Dokumentacja języka C# zawiera ostatnio wydaną wersję języka C#. Zawiera również początkową dokumentację dla funkcjonalności w publicznych wersjach testowych nadchodzącego wydania języka.

Dokumentacja identyfikuje dowolną funkcję po raz pierwszy wprowadzoną w ostatnich trzech wersjach języka lub w bieżącej publicznej wersji zapoznawczej.

Wskazówka

Aby dowiedzieć się, kiedy funkcja została po raz pierwszy wprowadzona w języku C#, zapoznaj się z artykułem dotyczącym historii wersji języka C#.

Możesz użyć is operatora dopasowywania wzorca jako alternatywy dla == testu podczas testowania względem stałej wartości. Operator is używa domyślnej semantyki równości dla wszystkich typów wartości i odwołań.

Operator równości ==

Operator == równości zwraca true wartość , jeśli jego operandy są równe, false w przeciwnym razie.

Równość typów wartości

Operandy wbudowanych typów wartości są równe, jeśli ich wartości są równe:

int a = 1 + 2 + 3;
int b = 6;
Console.WriteLine(a == b);  // output: True

char c1 = 'a';
char c2 = 'A';
Console.WriteLine(c1 == c2);  // output: False
Console.WriteLine(c1 == char.ToLower(c2));  // output: True

Uwaga

==W przypadku operatorów , ,<> , <=i >= , jeśli którykolwiek z operandów nie jest liczbą (Double.NaN lub Single.NaN), wynikiem operacji jest false. Ten warunek oznacza, że NaN wartość nie jest większa niż, mniejsza lub równa żadnej innej double wartości (lub float), w tym NaN. Aby uzyskać więcej informacji i przykładów, zobacz Double.NaN artykuł lub reference (Dokumentacja Single.NaN ).

Dwa operandy tego samego typu wyliczenia są równe, jeśli odpowiadające im wartości bazowego typu całkowitego są równe.

Typy struktur zdefiniowanych przez użytkownika nie obsługują == operatora domyślnie. Aby obsługiwać == operator, struktura zdefiniowana przez użytkownika musi ją przeciążyć.

Krotki języka C# mają wbudowaną obsługę == operatorów i != . Aby uzyskać więcej informacji, zobacz sekcję Równości krotki w artykule Typy krotki.

Równość typów odwołań

Domyślnie operandy typu odwołania, z wyłączeniem rekordów, są równe, jeśli odwołują się do tego samego obiektu:

public class ReferenceTypesEquality
{
    public class MyClass
    {
        private int id;

        public MyClass(int id) => this.id = id;
    }

    public static void Main()
    {
        var a = new MyClass(1);
        var b = new MyClass(1);
        var c = a;
        Console.WriteLine(a == b);  // output: False
        Console.WriteLine(a == c);  // output: True
    }
}

Jak pokazano w poprzednim przykładzie, typy referencyjne zdefiniowane przez użytkownika domyślnie obsługują == operator. Jednak typ odwołania może przeciążyć == operatora. Jeśli typ odwołania przeciąża == operatora, użyj Object.ReferenceEquals metody , aby sprawdzić, czy dwa odwołania tego typu odwołują się do tego samego obiektu.

Równość typów rekordów

Typy rekordów obsługują == operatory i != , które domyślnie zapewniają semantyka równości wartości. Oznacza to, że dwa operandy rekordów są równe, gdy oba z nich są null lub odpowiadające wartości wszystkich pól i automatycznie zaimplementowane właściwości są równe.

public class RecordTypesEquality
{
    public record Point(int X, int Y, string Name);
    public record TaggedNumber(int Number, List<string> Tags);

    public static void Main()
    {
        var p1 = new Point(2, 3, "A");
        var p2 = new Point(1, 3, "B");
        var p3 = new Point(2, 3, "A");

        Console.WriteLine(p1 == p2);  // output: False
        Console.WriteLine(p1 == p3);  // output: True

        var n1 = new TaggedNumber(2, new List<string>() { "A" });
        var n2 = new TaggedNumber(2, new List<string>() { "A" });
        Console.WriteLine(n1 == n2);  // output: False
    }
}

Jak pokazano w poprzednim przykładzie, równość elementów członkowskich typu odwołania jest porównywana przy użyciu ich konkretnych implementacji równości.

Równość ciągów

Dwa operandy ciągów są równe, gdy oba z nich są null lub oba wystąpienia ciągów mają taką samą długość i mają identyczne znaki w każdej pozycji znaku:

string s1 = "hello!";
string s2 = "HeLLo!";
Console.WriteLine(s1 == s2.ToLower());  // output: True

string s3 = "Hello!";
Console.WriteLine(s1 == s3);  // output: False

Porównania równości ciągów to porównania porządkowe z uwzględnieniem wielkości liter. Aby uzyskać więcej informacji na temat porównywania ciągów, zobacz Jak porównać ciągi w języku C#.

Deleguj równość

Dwa operandy delegatów tego samego typu czasu wykonywania są równe, gdy oba z nich są null lub ich listy wywołań mają taką samą długość i mają równe wpisy w każdej pozycji:

Action a = () => Console.WriteLine("a");

Action b = a + a;
Action c = a + a;
Console.WriteLine(object.ReferenceEquals(b, c));  // output: False
Console.WriteLine(b == c);  // output: True

Ważne

Równe wpisy na liście wywołań obejmują wszystkie stałe parametry w wywołaniu, w tym odbiornik. Odbiornik jest wystąpieniem obiektu reprezentowanego przez this wywołanie wpisu.

var o1 = new object();
var o2 = new object();
var d1 = o1.ToString;
var d2 = o2.ToString;
Console.WriteLine(object.ReferenceEquals(d1, d2));  // output: False
Console.WriteLine(d1 == d2);  // output: False (different receivers)

Aby uzyskać więcej informacji, zobacz sekcję Delegowanie operatorów równości specyfikacji języka C#.

Delegaty, które pochodzą z obliczania semantycznie identycznych wyrażeń lambda , nie są równe, jak pokazano w poniższym przykładzie:

Action a = () => Console.WriteLine("a");
Action b = () => Console.WriteLine("a");

Console.WriteLine(a == b);  // output: False
Console.WriteLine(a + b == a + b);  // output: True
Console.WriteLine(b + a == a + b);  // output: False

Operator nierówności !=

Operator != nierówności zwraca, true jeśli jego operandy nie są równe i false w przeciwnym razie. W przypadku operandów wbudowanych typów wyrażenie generuje ten sam wynik co wyrażenie x != y!(x == y). Aby uzyskać więcej informacji na temat równości typów, zobacz sekcję Operator równości .

W poniższym przykładzie pokazano, jak używać != operatora:

int a = 1 + 1 + 2 + 3;
int b = 6;
Console.WriteLine(a != b);  // output: True

string s1 = "Hello";
string s2 = "Hello";
Console.WriteLine(s1 != s2);  // output: False

object o1 = 1;
object o2 = 1;
Console.WriteLine(o1 != o2);  // output: True

Równość w hierarchiach klas

Rekordy obsługują dziedziczenie poprawnie bez ręcznej pracy. Wygenerowany przez kompilator równość sprawdza zarówno typ środowiska uruchomieniowego, jak i wszystkie zadeklarowane właściwości, dzięki czemu automatycznie spełnia wymagania dotyczące symetrii i przechodniości. Preferuj record ręczną niezaużytą hierarchię, gdy równość wartości jest celem.

Ważne

Użyj record zawsze, gdy jest to możliwe — kompilator generuje wszystkie wymagane elementy członkowskie równości. Implementacja ręczna jest wymagana tylko wtedy, gdy typ musi pochodzić z klasy innej niż rekord lub ma inne ograniczenia, które uniemożliwiają record.

Samodzielnie zaimplementuj równość, kiedy typ nie może być rekordem

Oto minimalna implementacja ręczna dla typu wartości, który nie może być rekordem:

class Color : IEquatable<Color>
{
    public Color(int r, int g, int b)
    {
        R = r;
        G = g;
        B = b;
    }

    public int R { get; }
    public int G { get; }
    public int B { get; }

    public bool Equals(Color? other) =>
        other is not null && R == other.R && G == other.G && B == other.B;

    public override bool Equals(object? obj) => obj is Color other && Equals(other);
    public override int GetHashCode() => HashCode.Combine(R, G, B);
}

Implementacja zapewnia trzy wymagane elementy członkowskie: Equals(T?) jako podstawowe porównanie wywołań override Equals(object?) na poziomie obiektu i override GetHashCode() dlatego kolekcje oparte na skrótach działają poprawnie. HashCode.Combine to pomocnik biblioteki, który tworzy jeden skrót z tych samych wartości używanych przez Equalsprogram . Implementacja IEquatable<T>Equals(T?) (przeciążenie) jest opcjonalna, ale pozwala uniknąć boksowania, gdy osoby wywołujące mają już konkretny typ.

Podczas definiowania == i !=język wymaga ich jako pary; ostrzeżenia CS0660 i CS0661 przypominają o zachowaniu spójności wszystkich czterech elementów członkowskich.

W przypadku powyższych trzech elementów członkowskich odzwierciedla równość wartości, ale == nadal testuje tożsamość, Equals ponieważ żaden operator nie == został jeszcze zadeklarowany:

var red1 = new Color(255, 0, 0);
var red2 = new Color(255, 0, 0);

Console.WriteLine(red1.Equals(red2)); // => True
Console.WriteLine(red1 == red2);      // => False  (no == overload; identity check)

Prawidłowa implementacja musi również spełniać kontrakt równoważności (załóżmy x, , yi z są inne niż null):

  1. Refleksyjny: x.Equals(x) zwraca wartość true.
  2. Symetryczne: x.Equals(y) zwraca tę samą wartość co y.Equals(x).
  3. Przechodnie: jeśli x.Equals(y) i y.Equals(z) mają wartość true, x.Equals(z) musi to być true.
  4. Spójne: kolejne wywołania zwracające x.Equals(y) tę samą wartość, o ile żaden obiekt nie ulegnie zmianie.
  5. Zachowanie wartości null: x.Equals(null) zwraca wartość false; x.Equals(y) nie może być zgłaszana w przypadku wywołania elementu innego niż null x.

Równość wartości w niezaufanej hierarchii klas wymaga większej staranności niż w klasie zapieczętowanej, aby spełnić reguły symetryczne i przechodnie. Zagrożenie polega na tym, że IEquatable<T>.Equals(T? other) wysyłka jest zgodna z zadeklarowanym typem (typem zapisanym w deklaracji zmiennej) zmiennej, a nie typem środowiska uruchomieniowego. Jeśli Shape deklaruje wartość innąvirtualEquals(Shape? other) niż, zmienna typowana jako Shape przechowywana Circle w czasie wykonywania wywołuje Shape.Equalsmetodę — dyskretnie ignorując Circle-określone pola. Dwa Circle obiekty z różnymi promieniami mogą być porównywane jako równe w przypadku uzyskiwania dostępu za pośrednictwem zmiennej Shape .

Prawidłowy wzorzec wymaga dwóch współpracujących wymagań: ustaw metodę virtual typowąEquals, aby każda klasa pochodna mogła rozszerzyć porównanie i dodać GetType() == other.GetType() ochronę w implementacji klasy bazowej, aby obiekty różnych typów środowiska uruchomieniowego nigdy nie zostały uznane za równe.

Implementacja klasy bazowej

// Shape is an unsealed base class. Making Equals virtual and guarding with GetType()
// ensures a derived instance is never equal to an instance of a different runtime type.
class Shape : IEquatable<Shape>
{
    public string Color { get; }
    public Shape(string color) => Color = color;

    public override bool Equals(object? obj) => Equals(obj as Shape);

    // virtual so derived classes can override and augment the comparison
    public virtual bool Equals(Shape? other) =>
        other is not null &&
        GetType() == other.GetType() &&   // reject different runtime types
        Color == other.Color;

    // GetType() is included because equality requires matching runtime types
    public override int GetHashCode() => HashCode.Combine(GetType(), Color);

    public static bool operator ==(Shape? l, Shape? r) => l?.Equals(r) ?? r is null;
    public static bool operator !=(Shape? l, Shape? r) => !(l == r);
}

Najważniejsze kwestie:

  • virtual typed : każda Equalsklasa pochodna zastępuje tę metodę w celu rozszerzenia porównania z własnymi polami.
  • GetType() ochrona: GetType() == other.GetType() uniemożliwia Circle wyrównanie Shape elementu o tym samym kolorze i uniemożliwia równe sobie obiekty różnych typów pochodnych.
  • GetHashCode zawiera GetType(): ponieważ dwa obiekty są równe tylko wtedy, gdy ich typy środowiska uruchomieniowego są zgodne, GetHashCode musi zawierać skrót typu środowiska uruchomieniowego, a także pola danych. Pominięcie GetType() tutaj powoduje nieprawidłowe zachowanie w systemach Dictionary<TKey,TValue> i HashSet<T>.
  • == delegatów do Equals: utrzymuje spójność operatorów i metod.

Implementacja klasy pochodnej

Klasa pochodna, która dodaje pola zastępuje typizowane Equals, rzutuje do własnego typu, wywołuje metodę base.Equals, a następnie porównuje własne pola:

class Circle : Shape
{
    public double Radius { get; }
    public Circle(string color, double radius) : base(color) => Radius = radius;

    public override bool Equals(object? obj) => Equals(obj as Shape);

    // Calls base.Equals to verify Color and runtime type, then adds Radius
    public override bool Equals(Shape? other) =>
        other is Circle c && base.Equals(c) && Radius == c.Radius;

    public override int GetHashCode() => HashCode.Combine(GetType(), Color, Radius);
}

base.Equals(c) GetType() wymusza ochronę i sprawdza pola udostępnione. Rzutowanie za pomocą metody other is Circle c szybko kończy się niepowodzeniem, gdy argument jest dowolnym innym typem Shape pochodnym.

Użycie za pomocą zmiennej typu podstawowego

Shape circle1 = new Circle("red", 5.0);
Shape circle2 = new Circle("red", 7.0);
Shape circle3 = new Circle("red", 5.0);
Shape shape1  = new Shape("red");

Console.WriteLine(circle1.Equals(circle2)); // => False  (Radius differs)
Console.WriteLine(circle1.Equals(circle3)); // => True
Console.WriteLine(circle1.Equals(shape1));  // => False  (different runtime types)

Klasy zapieczętowane są prostsze

Nie można podklasy sealed klasy, więc typy czasu kompilacji i środowiska uruchomieniowego są zawsze zgodne. Nie potrzebujesz straży ani virtual wysyłaniaGetType(). Wzorzec IEquatable<T> pokazany w sekcji Implementowanie równości samodzielnie, gdy typ nie może być rekordem , jest poprawny i kompletny dla zapieczętowanej klasy.

Przeciążenie operatora

Operatory i można ==!= w typie zdefiniowanym przez użytkownika. Jeśli przeciążysz jeden z tych dwóch operatorów, musisz również przeciążyć drugi operator.

Nie można jawnie przeciążyć == operatorów i != w typie rekordu. Aby zmienić zachowanie operatorów i == dla typu !=rekordu T , zaimplementuj metodę IEquatable<T>.Equals przy użyciu następującego podpisu:

public virtual bool Equals(T? other);

specyfikacja języka C#

Aby uzyskać więcej informacji, zobacz sekcję Operatory relacyjne i testowe typów specyfikacji języka C#.

Aby uzyskać więcej informacji na temat równości typów rekordów, zobacz sekcję Elementy członkowskie równościspecyfikacji języka C#.

Zobacz też