Что такое «Лучшая практика» для сравнения двух экземпляров ссылочного типа? - PullRequest
44 голосов
/ 19 сентября 2008

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

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

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

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

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

Обновление

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

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

Ответы [ 9 ]

26 голосов
/ 19 сентября 2008

Правильно, эффективно реализовать равенство в .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;
        <b>return X.Equals(other.X) && Y.Equals(other.Y);</b>
    }

    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() => <b>X.GetHashCode() ^ Y.GetHashCode();</b>
}

Единственными подвижными частями в вышеприведенном коде являются жирные части: вторая строка в 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;
    <b>return X.Equals(other.X) && Y.Equals(other.Y);</b>
}
22 голосов
/ 19 сентября 2008

Похоже, что вы кодируете в 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 очень похожие механизмы. Метод equals () является частью класса Object , и ваш класс перегружает его, если вы хотите этот тип функциональности.

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

16 голосов
/ 08 апреля 2009

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


Основная информация

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

Обоснование

IEquatable

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

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

Метод IEquatable.Equals

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

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

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

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

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

== Оператор (C # Reference)

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

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

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

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

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

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

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

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

3 голосов
/ 19 сентября 2008

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

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

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

С Реализация метода равных

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

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

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

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

2 голосов
/ 19 сентября 2008

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

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

1 голос
/ 19 сентября 2016

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

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

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

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

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

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

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

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

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

1 голос
/ 19 сентября 2008

Я склонен использовать то, что автоматически делает 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.

0 голосов
/ 02 июля 2018

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

0 голосов
/ 17 декабря 2012

Я считаю, что получить что-то столь же простое, как проверка объектов на правильность, немного сложно с дизайном .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) равенства в первую очередь. Я бы не хотел серьезно относиться к руководству по РС, не только потому, что оно для меня непривычно, но и потому, что перегрузка == не приносит никакого серьезного вреда. В отличие от того, чтобы не отменять неуниверсальные 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 #, что мешает мне наследовать дополнительные классы.

...