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

StackOverflow https://stackoverflow.com/questions/104158

Вопрос

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

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

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

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

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

Обновить

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

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

Это было полезно?

Решение

Похоже, что вы кодируете на C #, в котором есть метод 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 имеет очень похожие механизмы.Тот Самый равно() метод является частью Объект class, и ваш класс перегружает его, если вам нужна функциональность такого типа.

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

Другие советы

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

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

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;
        возвращает X.Равно(other.X) && Y.Равно(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;
    возвращает X.Равно(other.X) && Y.Равно(other.Y);
}

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


Краткие сведения

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

Обоснование

Сравнимый

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

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

IEquatable.Метод равенства

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

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

  • 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 (null) возвращает false (только для ненулевых типов значений.Для получения дополнительной информации см. Обнуляемые типы (Руководство по программированию на C #).)
  • Новая реализация Equals не должна вызывать исключений.
  • Рекомендуется, чтобы любой класс, который переопределяет Equals, также переопределял Object.GetHashCode .
  • Рекомендуется, чтобы в дополнение к реализации Equals (object) любой класс также реализовывал Equals (type) для своего собственного типа, чтобы повысить производительность.

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

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

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

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

Object.Метод Equals (Объект)

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

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

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

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

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

Дополнительные Подводные камни

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

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

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

От Реализация метода Equals

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

От Рекомендации по реализации Equals и оператора равенства (==)

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

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

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

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

Я обычно использую то, что 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;
    }
}

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

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

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

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

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

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

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

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

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

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

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

Для Структуры

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> или около того) в качестве шаблона, позволяющего упростить повторное использование, но, к сожалению, в C # это мешает мне извлекать из дополнительных классов.

Все приведенные выше ответы не учитывают полиморфизм, часто вы хотите, чтобы производные ссылки использовали производные Equals даже при сравнении через базовую ссылку.Пожалуйста, ознакомьтесь с вопросом / обсуждением / ответами здесь - Равенство и полиморфизм

Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top