Операторы равенства — проверка того, равны ли два объекта или нет

Операторы == (равенство) и != (неравенство) проверяют равенство или неравенство своих операндов. Типы значений равны, если их содержимое равно. Ссылочные типы равны, если две переменные ссылаются на одно хранилище.

Справочные документы по языку C# описывают последнюю выпущенную версию языка C#. Она также содержит начальную документацию по функциям в общедоступных предварительных версиях для предстоящего языкового выпуска.

Документация определяет любую функцию, впервые представленную в последних трех версиях языка или в текущих общедоступных предварительных версиях.

Подсказка

Чтобы узнать, когда функция впервые появилась в C#, ознакомьтесь со статьей об истории версий языка C#.

Оператор сопоставления шаблонов можно использовать is в качестве альтернативы == тесту при тестировании с константным значением. Оператор is использует семантику равенства по умолчанию для всех типов значений и ссылок.

Оператор равенства ==

Оператор равенства == возвращает значение true, если его операнды равны. В противном случае возвращается значение false.

Равенство типов значений

Операнды встроенных типов значений равны, если равны их значения.

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

Примечание.

==Для операторов ,< , ><=и >= операторов, если любой из операндов не является числом (Double.NaNилиSingle.NaN), результатом операции является false. Это условие означает, что NaN значение не превышает, меньше или равно любому другому double (или float) значению, включая NaN. Дополнительные сведения и примеры см. в справочных статьях по Double.NaN или Single.NaN.

Два операнда одного типа enum равны, если равны соответствующие значения базового целочисленного типа.

По умолчанию пользовательские типы struct не поддерживают оператор ==. Чтобы поддерживать оператор ==, пользовательская структура должна перегружать его.

Кортежи C# имеют встроенную поддержку == операторов и != операторов. Дополнительные сведения см. в разделе Равенство кортежей статьи Типы кортежей.

Равенство ссылочных типов

По умолчанию операнды ссылочного типа, за исключением записей, равны, если они ссылаются на тот же объект:

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
    }
}

Как показано в предыдущем примере, определяемые пользователем ссылочные типы поддерживают == оператор по умолчанию. Однако ссылочный тип может перегружать оператор ==. Если ссылочный тип перегружает оператор ==, воспользуйтесь методом Object.ReferenceEquals, чтобы проверить, что две ссылки этого типа указывают на один и тот же объект.

Равенство типов записей

Типы записей поддерживают == операторы != , которые по умолчанию предоставляют семантику равенства значений. То есть два операнда записей равны, если оба из них являются null или соответствующими значениями всех полей и автоматически реализованы свойства равны.

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
    }
}

Как показано в предыдущем примере, равенство элементов ссылочного типа сравнивается с использованием конкретных реализаций равенства.

Равенство строк

Два операнда string равны, если они оба имеют значение null или оба экземпляра строки имеют одинаковую длину и идентичные символы в каждой позиции символа.

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

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

Сравнение равенства строк — это порядковые сравнения с учетом регистра. Дополнительные сведения о том, как сравнивать строки, см. в статье Сравнение строк в C#.

Равенство делегатов

Два операнда одного и того же типа времени выполнения равны, если оба из них являются null или их списки вызовов имеют одинаковую длину и имеют равные записи в каждой позиции:

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

Это важно

Равные записи в списке вызовов включают все фиксированные параметры в вызове, включая приемника. Получатель — это экземпляр объекта, this представленного при вызове записи.

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)

Подробные сведения см. в разделе Delegate equality operators (Операторы равенства делегатов) в спецификации языка C#.

Делегаты, поступающие от оценки семантических идентичных лямбда-выражений , не равны, как показано в следующем примере:

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

Оператор неравенства !=

Оператор != неравенства возвращается true , если его операнды не равны и false в противном случае. Для операндов встроенных типов выражение x != y дает тот же результат, что и выражение !(x == y). Дополнительные сведения о равенстве типов см. в разделе Оператор равенства.

В следующем примере показано, как использовать != оператор:

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

Равенство в иерархиях классов

Записи правильно обрабатывают наследование без ручной работы. Созданный компилятором равенство проверяет тип среды выполнения и все объявленные свойства, поэтому он автоматически удовлетворяет требованиям симметрии и транзитивности. Предпочитайте record неуправляемую иерархию вручную, если равенство значений является целью.

Это важно

Используйте record каждый раз, когда это возможно, — компилятор создает все необходимые элементы равенства для вас. Реализация вручную требуется только в том случае, если тип должен быть производным от класса, отличного от записи, или имеет другие ограничения, которые препятствуют record.

Реализуйте равенство самостоятельно, когда тип не может быть записью

Ниже приведена минимальная реализация вручную для типа значения, который не может быть записью:

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);
}

Реализация предоставляет три обязательных элемента: Equals(T?) в качестве основного сравнения override Equals(object?) для вызовов на уровне объектов и override GetHashCode() поэтому хэш-коллекции работают правильно. HashCode.Combine — это вспомогательный элемент библиотеки, который создает один хэш из одинаковых значений, используемых Equals. Реализация (перегрузкаEquals(T?)) является необязательным IEquatable<T> , но избегает бокса, если вызывающие объекты уже имеют конкретный тип.

Если вы также определяете == и !=, язык требует их в качестве пары; предупреждения CS0660 и CS0661 напоминают вам, чтобы обеспечить согласованность всех четырех членов.

С тремя элементами, приведенными выше, отражает равенство значений, но == по-прежнему проверяет удостоверение, Equals так как оператор еще не == объявлен:

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)

Правильная реализация также должна соответствовать контракту эквивалентности (предположим x, yи z не равно null):

  1. Рефлекторный: x.Equals(x) возвращается true.
  2. Симметричный: x.Equals(y) возвращает то же значение, что y.Equals(x)и .
  3. Транзитивный: если x.Equals(y) и y.Equals(z) оба true, то x.Equals(z) должны быть true.
  4. Согласованный: последовательные вызовы для x.Equals(y) возврата того же значения, если ни один объект не изменяется.
  5. Поведение NULL: возвращаетсяfalse; x.Equals(null)x.Equals(y) не должно вызываться при вызове ненулевого xзначения.

Равенство значений в иерархии незапечатанных классов требует больше заботы, чем в запечатанном классе для удовлетворения симметричного и транзитивного правил. Опасность заключается в том, что IEquatable<T>.Equals(T? other) диспетчеризация следует объявленному типу (типу , записанному в объявлении переменной) переменной, а не типу среды выполнения. Если Shape объявляетсяEquals(Shape? other)virtual не, переменная, типизуемая как Shape которая содержит CircleShape.Equalsвызов во время выполнения, автоматически игнорирует Circleполя с определенными значениями. Два Circle объекта с разными радиями могут сравниваться как равные при доступе Shape через переменную.

Для правильного шаблона требуются два требования к сотрудничеству: сделайте типизированный Equals метод virtual , чтобы каждый производный класс может расширить сравнение, и добавить GetType() == other.GetType() защиту в реализацию базового класса, чтобы объекты разных типов среды выполнения никогда не считались равными.

Реализация базового класса

// 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);
}

Основные моменты:

  • virtual типизированный Equals: каждый производный класс переопределяет этот метод для расширения сравнения с собственными полями.
  • GetType() guard: GetType() == other.GetType() предотвращает Circle равенство Shape с одинаковым цветом и запрещает объектам различных производных типов равенства друг друга.
  • GetHashCode включает GetType(): поскольку два объекта равны только в том случае, если их типы среды выполнения соответствуют, GetHashCode должны хэшируют тип среды выполнения, а также поля данных. Опущение GetType() здесь приводит к неправильному поведению и Dictionary<TKey,TValue>HashSet<T>.
  • == делегаты в Equals: сохраняют согласованность операторов и методов.

Реализация производного класса

Производный класс, добавляющий поля, переопределяет типизированные Equals, приведение к собственному типу, вызовы base.Equals, а затем сравнивает свои собственные поля:

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() принудительно применяет охранник и проверяет общие поля. Приведение через other is Circle c ошибку выполняется быстро, если аргумент является любым другим производным типом Shape .

Использование с помощью переменной базового типа

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)

Запечатанные классы проще

Вы не можете подклассить sealed класс, поэтому типы времени компиляции и среды выполнения всегда согласны. Вам не нужен GetType() охранник или virtual диспетчер. Шаблон, показанный IEquatable<T> в разделе "Реализация равенства", когда тип не может быть записью правильной и завершенной для запечатаемого класса.

Возможность перегрузки оператора

Вы можете перегрузить==!= операторы в определяемом пользователем типе. При перегрузке одного из этих двух операторов необходимо также перегрузить другой оператор.

Невозможно явно перегружать ==!= операторы в типе записи. Чтобы изменить поведение == и != операторы для типа Tзаписи, реализуйте метод со следующей сигнатурой IEquatable<T>.Equals :

public virtual bool Equals(T? other);

Спецификация языка C#

Дополнительные сведения см. в разделе Операторы отношения и проверки типа в статье по спецификации языка C#.

Дополнительные сведения о равенстве типов записей см. в разделе "Члены равенства " спецификации языка C#.

См. также