Что такое "Лучшая практика" для сравнения двух экземпляров ссылочного типа?
Я сталкивался с этим недавно, до сих пор я успешно переопределял оператор равенства (==) и / или метод Equals, чтобы увидеть, действительно ли два ссылочных типа содержат одни и те же данные (т.е. два разных экземпляра, которые выглядят одинаково).
Я использую это еще больше, так как я больше разбираюсь в автоматизированном тестировании (сравнивая справочные / ожидаемые данные с возвращенными).
Просматривая некоторые из руководств по стандартам кодирования в MSDN, я наткнулся на статью, которая советует против этого. Теперь я понимаю, почему статья говорит об этом (потому что это не один и тот же экземпляр), но она не отвечает на вопрос:
- Каков наилучший способ сравнения двух типов ссылок?
- Должны ли мы реализовать IComparable? (Я также видел упоминание, что это должно быть зарезервировано только для типов значений).
- Есть ли какой-то интерфейс, о котором я не знаю?
- Должны ли мы просто свернуть наши собственные?!
Большое спасибо ^_^
Обновить
Похоже, что я неправильно прочитал некоторые документы (это был долгий день), и, возможно, лучше использовать Equals.
Если вы реализуете ссылочные типы, вам следует рассмотреть возможность переопределения метода Equals для ссылочного типа, если ваш тип выглядит как базовый тип, такой как Point, String, BigNumber и т. Д. Большинство ссылочных типов не должны перегружать оператор равенства, даже если они переопределяют Equals. Однако, если вы реализуете ссылочный тип, который должен иметь семантику значений, например тип комплексного числа, вы должны переопределить оператор равенства.
9 ответов
Похоже, что вы кодируете в 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, и ваш класс перегружает его, если вы хотите этот тип функциональности.
Причина, по которой перегрузка '==' может быть плохой идеей для объектов, заключается в том, что, как правило, вы все еще хотите иметь возможность выполнять сравнения "это один и тот же указатель". На них обычно полагаются, например, для вставки элемента в список, где дубликаты не допускаются, и некоторые из ваших компонентов инфраструктуры могут не работать, если этот оператор перегружен нестандартным способом.
Правильно, эффективно и без дублирования кода реализовать равенство в.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;
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);
}
Ниже я подытожил, что нужно делать при реализации IEquatable, и предоставил обоснование на различных страницах документации MSDN.
Резюме
- Когда требуется проверка на равенство значений (например, при использовании объектов в коллекциях), вы должны реализовать интерфейс IEquatable, переопределить Object.Equals и GetHashCode для вашего класса.
- Когда требуется проверка на равенство ссылок, вы должны использовать operator==,operator!= И Object.ReferenceEquals.
- Вы должны переопределять только оператор == и оператор! = Для ValueTypes и неизменяемых ссылочных типов.
обоснование
Интерфейс System.IEquatable используется для сравнения двух экземпляров объекта на равенство. Объекты сравниваются на основе логики, реализованной в классе. Сравнение приводит к логическому значению, указывающему, отличаются ли объекты. Это отличается от интерфейса System.IComparable, который возвращает целое число, указывающее, как значения объекта отличаются.
Интерфейс IEquatable объявляет два метода, которые должны быть переопределены. Метод Equals содержит реализацию, которая выполняет фактическое сравнение и возвращает true, если значения объекта равны, или false, если они не равны. Метод GetHashCode должен возвращать уникальное хеш-значение, которое можно использовать для уникальной идентификации идентичных объектов, которые содержат разные значения. Тип используемого алгоритма хеширования зависит от реализации.
- Вы должны реализовать IEquatable для ваших объектов, чтобы обрабатывать возможность того, что они будут храниться в массиве или общей коллекции.
- Если вы реализуете IEquatable, вам также следует переопределить реализации базового класса Object.Equals(Object) и 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.
- Последовательные вызовы х. Функция Equals(y) возвращает одно и то же значение, если объекты, на которые ссылаются x и y, не изменены.
- Икс. Функция Equals (null) возвращает false (только для типов значений, не допускающих значения NULL. Дополнительные сведения см. В разделе Типы Nullable (Руководство по программированию в C#).)
- Новая реализация Equals не должна бросать исключения.
- Рекомендуется, чтобы любой класс, переопределяющий Equals, также переопределял Object.GetHashCode.
- Рекомендуется, чтобы в дополнение к реализации Equals (объект) любой класс также реализовывал Equals (тип) для своего собственного типа, чтобы повысить производительность.
По умолчанию оператор == проверяет равенство ссылок, определяя, указывают ли две ссылки на один и тот же объект. Следовательно, ссылочные типы не должны реализовывать оператор == для получения этой функциональности. Когда тип является неизменяемым, то есть данные, содержащиеся в экземпляре, не могут быть изменены, может быть полезен оператор перегрузки == для сравнения равенства значений вместо ссылочного равенства, поскольку в качестве неизменяемых объектов они могут рассматриваться как длинные так как они имеют одинаковое значение. Не стоит переопределять оператор == в неизменяемых типах.
- Перегруженный оператор == реализации не должен генерировать исключения.
- Любой тип, который перегружает оператор ==, должен также перегружать оператор! =.
- Для предопределенных типов значений оператор равенства (==) возвращает true, если значения его операндов равны, в противном случае - false.
- Для ссылочных типов, отличных от string, == возвращает true, если два его операнда ссылаются на один и тот же объект.
- Для типа строки == сравнивает значения строк.
- При проверке на нулевое значение с использованием == сравнений внутри переопределений вашего оператора == убедитесь, что вы используете оператор базового класса объектов. Если вы этого не сделаете, произойдет бесконечная рекурсия, что приведет к переполнению стека.
Если ваш язык программирования поддерживает перегрузку операторов и если вы решите перегрузить оператор равенства для данного типа, этот тип должен переопределить метод Equals. Такие реализации метода Equals должны возвращать те же результаты, что и оператор равенства
Следующие рекомендации предназначены для реализации типа значения:
- Рассмотрите возможность переопределения Equals для увеличения производительности по сравнению с реализацией по умолчанию Equals для ValueType.
- Если вы переопределяете Equals и язык поддерживает перегрузку операторов, вы должны перегрузить оператор равенства для вашего типа значения.
Следующие рекомендации предназначены для реализации ссылочного типа:
- Рассмотрим переопределение Equals для ссылочного типа, если семантика типа основана на том факте, что тип представляет некоторое значение (я).
- Большинство ссылочных типов не должны перегружать оператор равенства, даже если они переопределяют Equals. Однако если вы реализуете ссылочный тип, который должен иметь семантику значений, например тип комплексного числа, вы должны переопределить оператор равенства.
Дополнительные Gotchas
- При переопределении GetHashCode () убедитесь, что вы тестируете ссылочные типы для NULL, прежде чем использовать их в хеш-коде.
- Я столкнулся с проблемой интерфейсного программирования и перегрузки операторов, описанной здесь: Перегрузка операторов с интерфейсным программированием в C#
Эта статья просто рекомендует не переопределять оператор равенства (для ссылочных типов), а не переопределять 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.
Для сложных объектов, которые приведут к конкретным сравнениям, хорошей реализацией будет реализация IComparable и определение сравнения в методах сравнения.
Например, у нас есть объекты "Автомобиль", где единственным отличием может быть регистрационный номер, и мы используем его для сравнения, чтобы убедиться, что ожидаемое значение, возвращаемое при тестировании, соответствует желаемому.
Я склонен использовать то, что автоматически делает 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 делает равенство значений, а == ссылается на равенство, а затем вы случайно используете равенство ссылок на вещах, не равных ссылкам, которые равны по значению.Это склонная к ошибкам практика, которую трудно обнаружить с помощью анализа человеческого кода.
Пару лет назад я работал над алгоритмом статического анализа для статистического обнаружения этой ситуации, и мы обнаружили, что частота дефектов составляет около двух экземпляров на миллион строк кода во всех изученных нами базах кода. При рассмотрении только кодовых баз, которые были где-то переопределены равными, уровень дефектов был явно значительно выше!
Кроме того, рассмотрите затраты против рисков. Если у вас уже есть реализации IComparable, тогда написание всех операторов - тривиальные однострочные, которые не будут содержать ошибок и никогда не будут изменены. Это самый дешевый код, который вы когда-либо будете писать. Если бы у меня был выбор между фиксированной стоимостью написания и тестирования дюжины крошечных методов по сравнению с неограниченной стоимостью поиска и исправления трудно видимой ошибки, в которой вместо равенства значений используется равенство ссылок, я знаю, какой из них я бы выбрал ".
.NET Framework никогда не будет использовать == или!= С любым типом, который вы пишете. Но опасность состоит в том, что случится, если кто-то другой сделает. Так что, если класс предназначен для третьей стороны, я бы всегда предоставлял операторы == и!=. Если класс предназначен только для внутреннего использования группой, я все равно, вероятно, реализовал бы операторы == и!=.
Я бы реализовал операторы <, <=,>и>= только в случае реализации IComparable. IComparable должен быть реализован только в том случае, если тип должен поддерживать упорядочение - например, при сортировке или использовании в упорядоченном универсальном контейнере, таком как SortedSet.
Если бы у группы или компании была политика, запрещающая использование операторов == и!=, Я бы, конечно, следовал этой политике. Если бы такая политика существовала, то было бы разумно применять ее с помощью инструмента анализа кода Q/A, который помечает любое вхождение операторов == и!= При использовании со ссылочным типом.
Удивительно, как трудно это сделать правильно...
Рекомендация Microsoft о том, чтобы в этом случае использовать Equals и == делать разные вещи, не имеет смысла для меня. В какой-то момент кто-то (по праву) ожидает, что Equals и == приведут к тому же результату, и код будет бомбить.
Я искал решение, которое будет:
- дает одинаковый результат, независимо от того, используется ли Equals или == во всех случаях
- быть полностью полиморфным (вызывая производное равенство посредством базовых ссылок) во всех случаях
Я придумал это:
class MyClass : IEquatable<MyClass> {
public int X { get; }
public int Y { get; }
public MyClass(int X, int Y) { this.X = X; this.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, int Y, int Z, int K) : base(X, Y) { this.Z = Z; this.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 o) => 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)
Будет ли в этой реализации делать некоторый бокс, однако бокс / распаковка сильно оптимизирован, и для меня это стоит того, чтобы достичь поставленных целей.
демо: https://dotnetfiddle.net/BCfViG
(Обратите внимание, что для C#> 7.0)
(основываясь на ответе Конарда)
Я считаю, что получить что-то столь же простое, как проверка объектов на правильность, немного сложно с дизайном.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#, что мешает мне получить дополнительные классы.