Правила переопределения метода Equals и оператора равенства (==) (Руководство по программированию в C#)

Обновлен: Ноябрь 2007

В языке C# существует два различных типа равенства: равенство ссылок и равенство значений. Равенство значений – это общее понятие равенства: два объекта содержат одинаковые значения. Например, две целых числа со значением 2 обладают равенством значений. Равенство ссылок означает, что для сравнения имеется не два объекта. Вместо них имеет две ссылки на объект, обе из которых связаны с одним объектом. Это достигается путем одного присвоения, как показано в следующем примере:

System.Object a = new System.Object();
System.Object b = a;
System.Object.ReferenceEquals(a, b);  //returns true

В этом коде существует только один объект, но на него имеется несколько ссылок: a и b. Так как обе ссылки связаны с одним и тем же объектом, они обладают равенством ссылок. Если два объекта обладают равенством ссылок, то они также имеют равенство значений, но равенство значений не может гарантировать равенство ссылок.

Для проверки равенства ссылок используется ReferenceEquals. Для проверки равенства значений используется Equals.

Переопределение Equals

Поскольку Equals является виртуальным методом, любой класс может переопределить его реализацию. Любой класс, представляющий значение (в принципе, любой тип значения) или набор значений в качестве группы, такой как класс сложных чисел, должен переопределять Equals. Если тип реализует IComparable, он должен переопределять Equals.

Новая реализация Equals должна соответствовать всем гарантиям Equals:

  • x.Equals(x) возвращает true.

  • x. Equals (y) возвращает то же значение, что и y. Equals (x).

  • если (x. Equals (y) && y. Equals (z)) возвращает true, то x. Equals (z) возвращает true.

  • Последовательные вызовы x. Equals (y) возвращают то же значение до тех пор, пока объекты, на которые ссылается x и y, не будут изменены.

  • x. Equals (ноль) возвращает false (только для не нулевых типов значений. Дополнительные сведения см. в разделе Типы, допускающие значения NULL (руководство по программированию на C#)).

Новая реализация Equals не должна создавать исключения. Рекомендуется, чтобы любой класс, переопределяющий Equals, также переопределял Object.GetHashCode. Также рекомендуется, чтобы помимо реализации Equals (объект), любой класс также реализовывал Equals (тип) для собственных типов с целью повышения производительности. Пример.

class TwoDPoint : System.Object
{
    public readonly int x, y;

    public TwoDPoint(int x, int y)  //constructor
    {
        this.x = x;
        this.y = y;
    }

    public override bool Equals(System.Object obj)
    {
        // If parameter is null return false.
        if (obj == null)
        {
            return false;
        }

        // If parameter cannot be cast to Point return false.
        TwoDPoint p = obj as TwoDPoint;
        if ((System.Object)p == null)
        {
            return false;
        }

        // Return true if the fields match:
        return (x == p.x) && (y == p.y);
    }

    public bool Equals(TwoDPoint p)
    {
        // If parameter is null return false:
        if ((object)p == null)
        {
            return false;
        }

        // Return true if the fields match:
        return (x == p.x) && (y == p.y);
    }

    public override int GetHashCode()
    {
        return x ^ y;
    }
}

Любой производный класс, который может вызвать Equals в базовом классе, должен вызывать этот метод до завершения сравнения. В следующем примере Equals вызывает базовый класс Equals, который проверяет нулевой параметр и сравнивает тип параметра с типом производного класса. На реализацию Equals в производном классе это накладывает задачу проверки новых полей данных, объявленных в производном классе:

class ThreeDPoint : TwoDPoint
{
    public readonly int z;

    public ThreeDPoint(int x, int y, int z)
        : base(x, y)
    {
        this.z = z;
    }

    public override bool Equals(System.Object obj)
    {
        // If parameter cannot be cast to ThreeDPoint return false:
        ThreeDPoint p = obj as ThreeDPoint;
        if ((object)p == null)
        {
            return false;
        }

        // Return true if the fields match:
        return base.Equals(obj) && z == p.z;
    }

    public bool Equals(ThreeDPoint p)
    {
        // Return true if the fields match:
        return base.Equals((TwoDPoint)p) && z == p.z;
    }

    public override int GetHashCode()
    {
        return base.GetHashCode() ^ z;
    }
}

Перегрузка оператора равенства (==)

По умолчанию оператор == проверяет равенство ссылок, определяя указывают ли две ссылки на один и тот же объект. Таким образом, ссылочные типы не должны реализовывать оператор ==, чтобы получить эту функцию. Если тип является неизменяемым, то есть когда содержащиеся в экземпляре данные изменять не допускается, перегрузка оператора == для сравнения равенства значений вместо равенства ссылок может иметь смысл, поскольку как неизменяемые объекты, они могут считаться таковыми до тех пор, пока будут иметь одинаковое значение. Не рекомендуется переопределять оператор == в типах, не являющихся неизменяемыми.

Реализация перегруженного оператора == не должна приводить к исключениям. Любой тип, перегружающий оператор ==, также должен перегружать оператор !=. Пример.

//add this code to class ThreeDPoint as defined previously
//
public static bool operator ==(ThreeDPoint a, ThreeDPoint b)
{
    // If both are null, or both are same instance, return true.
    if (System.Object.ReferenceEquals(a, b))
    {
        return true;
    }

    // If one is null, but not both, return false.
    if (((object)a == null) || ((object)b == null))
    {
        return false;
    }

    // Return true if the fields match:
    return a.x == b.x && a.y == b.y && a.z == b.z;
}

public static bool operator !=(ThreeDPoint a, ThreeDPoint b)
{
    return !(a == b);
}
ms173147.alert_note(ru-ru,VS.90).gifПримечание.

Распространенной ошибкой при перегрузке оператора == является использование (a == b), (a == null) или (b == null) для проверки равенства ссылок. Вместо нужного результата, это создает вызов перегруженного оператора ==, приводя к бесконечному циклу. Чтобы избежать цикла, используйте ReferenceEquals или приведите тип к Object.

См. также

Основные понятия

Руководство по программированию в C#

Ссылки

Операторы и выражения (Руководство по программированию на C#)

Операторы (руководство по программированию на C#)

Перегружаемые операторы (Руководство по программированию в C#)

Оператор == (справочник по C#)

Оператор () (Справочник по C#)