Что такое "Лучшая практика" для сравнения двух экземпляров ссылочного типа?

Я наткнулся на это недавно, до сих пор я с радостью переопределял оператор равенства (==) и/или Equals, чтобы увидеть, есть ли две ссылки типы фактически содержали те же данные (т.е. два разных экземпляра, которые выглядят одинаково).

Я использую это еще больше, так как я получил больше возможностей для автоматического тестирования (сравнение ссылочных/ожидаемых данных с возвратом).

При просмотре некоторых правил стандартов кодирования в MSDN я столкнулся с статья, которая советует против этого. Теперь я понимаю, почему статья говорит об этом (потому что это не тот же экземпляр), но он не отвечает на вопрос:

  • Каков наилучший способ сравнения двух ссылочных типов?
  • Должны ли мы реализовать IComparable? (Я также заметил, что это должно быть зарезервировано только для типов значений).
  • Есть ли какой-то интерфейс, о котором я не знаю?
  • Должны ли мы просто рулить наши?!

Большое спасибо ^ _ ^

Update

Похоже, я неправильно прочитал часть документации (это был длинный день), и переопределение Equals может быть способом..

Если вы используете ссылку типов, вы должны учитывать переопределение метод Equals на ссылочном типе если ваш тип выглядит как базовый тип такие как Point, String, BigNumber, и так далее. Большинство ссылочных типов должны не перегружать оператор равенства, даже , если они переопределяют Equals. Однако, если вы используете ссылку тип, который должен иметь ценность семантика, такая как комплексное число типа, вы должны переопределить равенство Оператор.

Ответ 1

Похоже, вы кодируете на С#, который имеет метод 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;
    }
}

Java имеет очень похожие механизмы. Метод equals() является частью класса Object, и ваш класс перегружает его, если вы хотите использовать этот тип функций.

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

Ответ 2

Реализация равенства в.NET корректно, эффективно и без дублирования кода сложна. В частности, для ссылочных типов со значениями семантики (т.е. неизменяемыми типами, которые рассматривают эквивальность как равенство), вы должны реализовать интерфейс System.IEquatable<T>, и вы должны реализовать все различные операции (Equals, GetHashCode и == !=),

Например, heres класс, реализующий значение равенства:

class Point : IEquatable<Point> {
    public int X { get; }
    public int Y { get; }

    public Point(int x = 0, int y = 0) { X = x; Y = y; }

    public bool Equals(Point other) {
        if (other is null) return false;
        return X.Equals(other.X) && Y.Equals(other.Y);
    }

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

    public static bool operator ==(Point lhs, Point rhs) => object.Equals(lhs, rhs);

    public static bool operator !=(Point lhs, Point rhs) => ! (lhs == rhs);

    public override int GetHashCode() => X.GetHashCode() ^ Y.GetHashCode();
}

Единственными подвижными частями в этом коде являются полужирные части: вторая строка в Equals(Point other) и метод GetHashCode(). Другой код должен оставаться неизменным.

Для ссылочных классов, которые не представляют неизменяемые значения, не реализуйте операторы == и !=. Вместо этого используйте значение по умолчанию, которое заключается в сравнении идентификатора объекта.

Код намеренно приравнивает четные объекты типа производного класса. Часто это может быть нежелательно, поскольку равенство между базовым классом и производными классами не определено. К сожалению,.NET и рекомендации по кодированию здесь не совсем понятны. Код, созданный Resharper, отправленный в другом ответе, восприимчив к нежелательному поведению в таких случаях, потому что Equals(object x) и Equals(SecurableResourcePermission x) будут рассматривать этот случай по-другому.

Чтобы изменить это поведение, необходимо ввести дополнительную проверку типа в строго указанном методе Equals выше:

public bool Equals(Point other) {
    if (other is null) return false;
    if (other.GetType() != GetType()) return false;
    return X.Equals(other.X) && Y.Equals(other.Y);
}

Ответ 3

Ниже я подвел итог тому, что вам нужно сделать при реализации IEquatable и предоставил обоснование с различных страниц документации MSDN.


Резюме

  • При тестировании для равенства значений желательно (например, при использовании объектов в коллекциях) вы должны реализовать интерфейс IEquatable, переопределить Object.Equals и GetHashCode для своего класса.
  • При тестировании для ссылочного равенства желательно использовать оператор ==, operator!= и Object.ReferenceEquals.
  • Вы должны только переопределять оператор == и operator!= для ValueTypes и неизменные ссылочные типы.

Оправдание

IEquatable

Интерфейс System.IEquatable используется для сравнения двух экземпляров объекта для равенства. Объекты сравниваются на основе логики, реализованной в классе. Результатом сравнения является логическое значение, указывающее, отличаются ли объекты. Это контрастирует с интерфейсом System.IComparable, который возвращает целое число, указывающее, как разные значения объекта.

Интерфейс IEquatable объявляет два метода, которые должны быть переопределены. Метод Equals содержит реализацию для выполнения фактического сравнения и возвращает true, если значения объекта равны, или false, если они не являются. Метод GetHashCode должен возвращать уникальное значение хэша, которое может использоваться для уникальной идентификации идентичных объектов, которые содержат разные значения. Тип используемого алгоритма хеширования специфичен для реализации.

IEquatable.Equals Method

  • Вы должны реализовать IEquatable для своих объектов, чтобы обрабатывать возможность их хранения в массиве или общей коллекции.
  • Если вы реализуете IEquatable, вы также должны переопределять реализации базового класса Object.Equals(Object) и GetHashCode, чтобы их поведение соответствовало их методу IEquatable.Equals

Рекомендации по переопределению равных() и операторов == (Руководство по программированию на С#)

  • x.Equals(x) возвращает true.
  • x.Equals(y) возвращает то же значение, что и y.Equals(x)
  • if (x.Equals(y) & y.Equals(z)) возвращает true, тогда x.Equals(z) возвращает true.
  • Последовательные вызовы x. Equals (y) возвращают одно и то же значение, если объекты, на которые ссылаются x и y, не изменяются.
  • х. Equals (null) возвращает false (только для типов с нулевым значением). Для получения дополнительной информации см. Nullable Types (Руководство по программированию на С#).)
  • Новая реализация Equals не должна генерировать исключения.
  • Рекомендуется, чтобы любой класс, который переопределяет Equals, также переопределяет Object.GetHashCode.
  • Рекомендуется, чтобы в дополнение к реализации Equals (object) любой класс также применял Equals (type) для своего собственного типа, чтобы повысить производительность.

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

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

== Оператор (ссылка на С#)

  • Для предопределенных типов значений оператор равенства (==) возвращает true, если значения его операндов равны, в противном случае - false.
  • Для ссылочных типов, отличных от строки, == возвращает true, если два операнда относятся к одному и тому же объекту.
  • Для строкового типа == сравнивает значения строк.
  • При тестировании нулевого использования == сравнения внутри переопределения оператора == убедитесь, что вы используете оператор класса базового объекта. Если вы этого не сделаете, произойдет бесконечная рекурсия, что приведет к переходу stackoverflow.

Object.Equals Method (Object)

Если ваш язык программирования поддерживает перегрузку оператора, и если вы решите перегрузить оператор равенства для данного типа, этот тип должен переопределить метод Equals. Такие реализации метода Equals должны возвращать те же результаты, что и оператор равенства

Следующие рекомендации предназначены для реализации типа значения:

  • Рассмотрим переопределение равных для увеличения производительности по сравнению с тем, которое обеспечивается реализацией по умолчанию Equals в ValueType.
  • Если вы переопределяете Equals и язык поддерживает перегрузку оператора, вы должны перегрузить оператор равенства для вашего типа значения.

Следующие рекомендации предназначены для реализации ссылочного типа :

  • Рассмотрим переопределение Equals в ссылочном типе, если семантика этого типа основана на том, что тип представляет некоторое значение (s).
  • Большинство ссылочных типов не должны перегружать оператор равенства, даже если они переопределяют Equals. Однако, если вы реализуете ссылочный тип, который предназначен для семантики значения, например сложного типа номера, вы должны переопределить оператор равенства.

Дополнительные Gotchas

Ответ 4

Эта статья просто рекомендует не переопределять оператор равенства (для ссылочных типов), а не переопределять равные. Вы должны переопределить Equals внутри вашего объекта (ссылка или значение), если проверки равенства означают нечто большее, чем контрольные проверки. Если вам нужен интерфейс, вы также можете реализовать IEquatable (используемый коллекциями общего назначения). Однако, если вы реализуете IEquatable, вы также должны переопределить equals, так как раздел примечаний IEquatable гласит:

Если вы реализуете IEquatable <T> , вы также должны переопределить реализации базового класса объектов Object.Equals(Object) и GetHashCode, чтобы их поведение соответствовало их методу метода IEquatable <T> .Equals. Если вы переопределяете Object.Equals(Object), ваша переопределенная реализация также вызывается при вызове метода статического метода Equals (System.Object, System.Object) в вашем классе. Это гарантирует, что все вызовы метода Equals возвращают согласованные результаты.

В отношении того, следует ли вам использовать Equals и/или оператор равенства:

От Внедрение метода Equals

Большинство ссылочных типов не должны перегружать оператор равенства, даже если они переопределяют Equals.

От Рекомендации по внедрению равных и оператора равенства (==)

Переопределите метод Equals всякий раз, когда вы реализуете оператор равенства (==), и делайте им то же самое.

Это говорит только о том, что вам нужно переопределить Equals всякий раз, когда вы выполняете оператор равенства. Он не говорит, что вам нужно переопределить оператор равенства при переопределении Equals.

Ответ 5

Для сложных объектов, которые будут давать конкретные сравнения, тогда реализация IComparable и определение сравнения в методах сравнения является хорошей реализацией.

Например, у нас есть объекты "Vehicle", где единственным отличием может быть регистрационный номер, и мы используем его для сравнения, чтобы гарантировать, что ожидаемое значение, возвращаемое при тестировании, является тем, которое мы хотим.

Ответ 6

Я стараюсь использовать то, что автоматически производит Resharper. например, он автоматически создал это для одного из моих ссылочных типов:

public override bool Equals(object obj)
{
    if (ReferenceEquals(null, obj)) return false;
    if (ReferenceEquals(this, obj)) return true;
    return obj.GetType() == typeof(SecurableResourcePermission) && Equals((SecurableResourcePermission)obj);
}

public bool Equals(SecurableResourcePermission obj)
{
    if (ReferenceEquals(null, obj)) return false;
    if (ReferenceEquals(this, obj)) return true;
    return obj.ResourceUid == ResourceUid && Equals(obj.ActionCode, ActionCode) && Equals(obj.AllowDeny, AllowDeny);
}

public override int GetHashCode()
{
    unchecked
    {
        int result = (int)ResourceUid;
        result = (result * 397) ^ (ActionCode != null ? ActionCode.GetHashCode() : 0);
        result = (result * 397) ^ AllowDeny.GetHashCode();
        return result;
    }
}

Если вы хотите переопределить == и по-прежнему выполнять проверки ref, вы все равно можете использовать Object.ReferenceEquals.

Ответ 7

Microsoft, похоже, изменила свою настройку, или, по крайней мере, существует противоречивая информация о том, что она не перегружает оператор равенства. В соответствии с этим статья Microsoft, озаглавленная" Как определить значение равенства для типа:

"Операторы == и!= могут использоваться с классами, даже если класс не перегружает их. Однако поведение по умолчанию заключается в выполнении контрольной проверки равенства. В классе, если вы перегружаете метод Equals, вы должен перегружать операторы == и! =, но это не требуется."

По словам Эрика Липперта в ответе на вопрос, я задал вопрос Минимальный код для равенства в С# - он говорит:

"Опасность, с которой вы столкнулись, заключается в том, что вы получаете определенный для вас оператор ==, который по умолчанию ссылается на равенство. Вы легко можете оказаться в ситуации, когда перегруженный метод Equals делает значение равенства, а == делает ссылку на равенство, а затем вы случайно используете ссылочное равенство для объектов, не равных по ссылке, которые являются равными по значению. Это склонная к ошибкам практика, которую трудно обнаружить при проверке кода человека.

Несколько лет назад я работал над алгоритмом статического анализа, чтобы статистически обнаружить эту ситуацию, и мы обнаружили степень дефекта около двух экземпляров на миллион строк кода во всех изученных нами кодах. Рассматривая только кодовые базы, которые где-то переопределили Equals, уровень дефекта был явно выше!

Кроме того, рассмотрим затраты и риски. Если у вас уже есть реализации IComparable, тогда писать все операторы - тривиальные однострочные, которые не будут иметь ошибок и никогда не будут изменены. Это самый дешевый код, который вы когда-либо собираетесь писать. Если у вас есть выбор между фиксированной стоимостью написания и проверкой дюжины крошечных методов против неограниченной стоимости поиска и исправления труднодоступной ошибки, в которой вместо равенства ценности используется ссылочное равенство, я знаю, какой из них я бы выбрал ".

.NET Framework никогда не будет использовать == или!= с любым типом, который вы пишете. Но опасность заключается в том, что произойдет, если кто-то другой сделает это. Итак, если класс для третьей стороны, я всегда предоставлял операторы == и! =. Если класс предназначен только для внутреннего использования группой, я все же, вероятно, буду реализовывать операторы == и! =.

Я бы реализовал только операторы <, < =, > и > =, если был реализован IComparable. IComparable должен быть реализован только в том случае, если тип должен поддерживать упорядочение - например, при сортировке или использовании в упорядоченном универсальном контейнере, таком как SortedSet.

Если у группы или компании была политика, чтобы никогда не реализовывать операторы == и! =, я бы, конечно, следовал этой политике. Если бы такая политика была на месте, тогда было бы разумно применять ее с помощью инструмента анализа кода Q/A, который маркирует любое вхождение операторов == и!= При использовании с ссылочным типом.

Ответ 8

Я считаю, что получить что-то так же просто, как проверка объектов на правильность равенства немного сложно с .NET-дизайном.

Для Struct

1) Внесите IEquatable<T>. Это заметно улучшает производительность.

2) Так как теперь у вас есть свой Equals, переопределите GetHashCode и совместите с различными проверками проверки равенства object.Equals.

3) Операции перегрузки == и != не должны быть религиозно выполнены, поскольку компилятор будет предупреждать, если вы непреднамеренно приравниваете структуру к другому с помощью == или !=, но это полезно сделать в соответствии с методами Equals.

public struct Entity : IEquatable<Entity>
{
    public bool Equals(Entity other)
    {
        throw new NotImplementedException("Your equality check here...");
    }

    public override bool Equals(object obj)
    {
        if (obj == null || !(obj is Entity))
            return false;

        return Equals((Entity)obj);
    }

    public static bool operator ==(Entity e1, Entity e2)
    {
        return e1.Equals(e2);
    }

    public static bool operator !=(Entity e1, Entity e2)
    {
        return !(e1 == e2);
    }

    public override int GetHashCode()
    {
        throw new NotImplementedException("Your lightweight hashing algorithm, consistent with Equals method, here...");
    }
}

Для класса

От MS:

Большинство ссылочных типов не должны перегружать оператор равенства, даже если они переопределяют Equals.

Мне == нравится как равенство ценности, больше похоже на синтаксический сахар для метода Equals. Запись a == b гораздо интуитивнее, чем запись a.Equals(b). Редко нам нужно проверить ссылочное равенство. В абстрактных уровнях, касающихся логических представлений физических объектов, это не то, что нам нужно будет проверить. Я думаю, что разная семантика для == и Equals может быть путаницей. Я считаю, что для равенства ценности и Equals должно было быть == для ссылочного (или лучшего имени типа IsSameAs)). Мне бы очень хотелось не относиться к MS главным образом здесь, а не только потому, что это неестественно для меня, но также и потому, что перегрузка == не наносит серьезного вреда. Это в отличие от не переопределяющих не общих Equals или GetHashCode, которые могут откусывать назад, потому что фреймворк не использует == где угодно, но только если мы его сами используем. Единственное реальное преимущество, которое я получаю от перегрузки == и !=, будет соответствовать дизайну всей структуры, над которой я не контролирую. И это действительно большая вещь, так грустно, что я буду придерживаться ее.

С помощью ссылочной семантики (изменяемые объекты)

1) Переопределить Equals и GetHashCode.

2) Реализация IEquatable<T> не обязательна, но будет приятной, если у вас ее есть.

public class Entity : IEquatable<Entity>
{
    public bool Equals(Entity other)
    {
        if (ReferenceEquals(this, other))
            return true;

        if (ReferenceEquals(null, other))
            return false;

        //if your below implementation will involve objects of derived classes, then do a 
        //GetType == other.GetType comparison
        throw new NotImplementedException("Your equality check here...");
    }

    public override bool Equals(object obj)
    {
        return Equals(obj as Entity);
    }

    public override int GetHashCode()
    {
        throw new NotImplementedException("Your lightweight hashing algorithm, consistent with Equals method, here...");
    }
}

С семантикой значения (неизменяемые объекты)

Это сложная часть. Может быть легко запутано, если не позаботиться.

1) Переопределить Equals и GetHashCode.

2) Перегрузка == и != соответствует Equals. Убедитесь, что он работает для нулей.

2) Реализация IEquatable<T> не обязательна, но будет приятной, если у вас ее есть.

public class Entity : IEquatable<Entity>
{
    public bool Equals(Entity other)
    {
        if (ReferenceEquals(this, other))
            return true;

        if (ReferenceEquals(null, other))
            return false;

        //if your below implementation will involve objects of derived classes, then do a 
        //GetType == other.GetType comparison
        throw new NotImplementedException("Your equality check here...");
    }

    public override bool Equals(object obj)
    {
        return Equals(obj as Entity);
    }

    public static bool operator ==(Entity e1, Entity e2)
    {
        if (ReferenceEquals(e1, null))
            return ReferenceEquals(e2, null);

        return e1.Equals(e2);
    }

    public static bool operator !=(Entity e1, Entity e2)
    {
        return !(e1 == e2);
    }

    public override int GetHashCode()
    {
        throw new NotImplementedException("Your lightweight hashing algorithm, consistent with Equals method, here...");
    }
}

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

В целом старайтесь не дублировать код. Я мог бы создать общий абстрактный базовый класс (IEqualizable<T> или около того) в качестве шаблона, чтобы упростить повторное использование, но, к сожалению, на С#, который мешает мне получить дополнительные классы.

Ответ 9

Замечательно, как трудно это сделать правильно...

Рекомендация Microsoft о том, что Equals и == делают разные вещи в этом случае, не имеет для меня смысла. В какой-то момент кто-то (по праву) ожидает, что Equals и == будут получать тот же результат, и код будет бомбить.

Я искал решение, которое будет:

  • производят тот же результат, если во всех случаях используются значения Equals или ==
  • быть полностью полиморфным (вызывать производное равенство посредством базовых ссылок) во всех случаях

Я придумал это:

class MyClass : IEquatable<MyClass> {
  public int X { get; }
  public int Y { get; }
  public MyClass(int x = 0, int y = 0) { X = x; Y = y; }

  public override bool Equals(object obj) {
    var o = obj as MyClass;
    return o is null ? false : X.Equals(o.X) && Y.Equals(o.Y);
  }
  public bool Equals(MyClass o) => object.Equals(this, o);
  public static bool operator ==(MyClass o1, MyClass o2) => object.Equals(o1, o2);
  public static bool operator !=(MyClass o1, MyClass o2) => !object.Equals(o1, o2);

  public override int GetHashCode() => HashCode.Combine(X, Y);
}

Здесь все заканчивается в Equals(object) который всегда является полиморфным, так что обе цели достигнуты.

Выполните так:

class MyDerived : MyClass, IEquatable<MyDerived> {
  public int Z { get; }
  public int K { get; }
  public MyDerived(int x = 0, int y = 0, int z=0, int k=0) : base(x, y) { Z = z; K = k; }

  public override bool Equals(object obj) {
    var o = obj as MyDerived;
    return o is null ? false : base.Equals(obj) && Z.Equals(o.Z) && K.Equals(o.K);
  }
  public bool Equals(MyDerived other) => object.Equals(this, o);
  public static bool operator ==(MyDerived o1, MyDerived o2) => object.Equals(o1, o2);
  public static bool operator !=(MyDerived o1, MyDerived o2) => !object.Equals(o1, o2);

  public override int GetHashCode() => HashCode.Combine(base.GetHashCode(), Z, K);
}

Что в основном одно и то же, за исключением одного полученного - когда Equals(object) хочет вызвать base.Equals будьте осторожны, чтобы вызвать base.Equals(object) а не base.Equals(MyClass) (что вызовет бесконечную рекурсию).

Оговорка здесь в том, что Equals(MyClass) в этой реализации сделает какой-то бокс, однако бокс/распаковка сильно оптимизирована, и для меня это стоит того, чтобы достичь поставленных целей.

demo: https://dotnetfiddle.net/cCx8WZ

(Обратите внимание, что для С#> 7.0)
(на основе ответа Конарда)