Есть ли полная ссылка на реализацию IEquatable?
Многие из моих вопросов здесь, на SO, касаются реализации IEquatable. Я обнаружил, что это очень сложно реализовать правильно, потому что в наивной реализации есть много скрытых ошибок, и статьи, которые я нашел об этом, довольно неполны. Я хочу найти или написать окончательную ссылку, которая должна включать:
- Как правильно реализовать IEquatable
- Как правильно отменить правильные значения.
- Как правильно заменить GetHashCode
- Как правильно реализовать метод ToString.
- Как правильно реализовать оператор ==
- Как реализовать оператор!= правильно
Такая полная ссылка уже существует?
PS: Даже ссылка MSDN кажется мне некорректной
Ответы
Ответ 1
Реализация IEquatable<T>
для типа значения
Реализация IEquatable<T>
для типа значения немного отличается от типа ссылки. Предположим, что у нас есть архетип типа "Реализованный ваш собственный-значение-тип", комплексное число struct.
public struct Complex
{
public double RealPart { get; set; }
public double ImaginaryPart { get; set; }
}
Наш первый шаг - реализовать IEquatable<T>
и переопределить Object.Equals
и Object.GetHashCode
:
public bool Equals(Complex other)
{
// Complex is a value type, thus we don't have to check for null
// if (other == null) return false;
return (this.RealPart == other.RealPart)
&& (this.ImaginaryPart == other.ImaginaryPart);
}
public override bool Equals(object other)
{
// other could be a reference type, the is operator will return false if null
if (other is Complex)
return this.Equals((Complex)other);
else
return false;
}
public override int GetHashCode()
{
return this.RealPart.GetHashCode() ^ this.ImaginaryPart.GetHashCode();
}
С очень небольшим усилием мы имеем правильную реализацию, за исключением операторов. Добавление операторов также является тривиальным процессом:
public static bool operator ==(Complex term1, Complex term2)
{
return term1.Equals(term2);
}
public static bool operator !=(Complex term1, Complex term2)
{
return !term1.Equals(term2);
}
Проницательный читатель заметил бы, что мы должны, вероятно, реализовать IEquatable<double>
, поскольку числа Complex
могут быть взаимозаменяемыми с базовым типом значения.
public bool Equals(double otherReal)
{
return (this.RealPart == otherReal) && (this.ImaginaryPart == 0.0);
}
public override bool Equals(object other)
{
// other could be a reference type, thus we check for null
if (other == null) return base.Equals(other);
if (other is Complex)
{
return this.Equals((Complex)other);
}
else if (other is double)
{
return this.Equals((double)other);
}
else
{
return false;
}
}
Нам нужны четыре оператора, если мы добавим IEquatable<double>
, потому что вы можете иметь Complex == double
или double == Complex
(и то же самое для operator !=
):
public static bool operator ==(Complex term1, double term2)
{
return term1.Equals(term2);
}
public static bool operator ==(double term1, Complex term2)
{
return term2.Equals(term1);
}
public static bool operator !=(Complex term1, double term2)
{
return !term1.Equals(term2);
}
public static bool operator !=(double term1, Complex term2)
{
return !term2.Equals(term1);
}
Итак, у вас есть это, с минимальными усилиями мы имеем правильную и полезную реализацию IEquatable<T>
для типа значения:
public struct Complex : IEquatable<Complex>, IEquatable<double>
{
}
Ответ 2
Я считаю, что получить что-то так же просто, как проверка объектов на правильность равенства немного сложно с .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>
или около того) в качестве шаблона, чтобы упростить повторное использование, но, к сожалению, на С#, который мешает мне получить дополнительные классы.
Ответ 3
При чтении MSDN я уверен, что лучший пример правильной реализации находится в странице IEquatable.Equals Method. Мое единственное отклонение:
public override bool Equals(Object obj)
{
if (obj == null) return base.Equals(obj);
if (! (obj is Person))
return false; // Instead of throw new InvalidOperationException
else
return Equals(obj as Person);
}
Для тех, кто задается вопросом об отклонении, он происходит от Object.Equals(Object) Страница MSDN:
Реализации равных не должны генерировать исключения.
Ответ 4
Я нашел другую ссылку, это реализация .NET Anonymous Type. Для анонимного типа с int и double в качестве свойств я разобрал следующий код С#:
public class f__AnonymousType0
{
// Fields
public int A { get; }
public double B { get; }
// Methods
public override bool Equals(object value)
{
var type = value as f__AnonymousType0;
return (((type != null)
&& EqualityComparer<int>.Default.Equals(this.A, type.A))
&& EqualityComparer<double>.Default.Equals(this.B, type.B));
}
public override int GetHashCode()
{
int num = -1134271262;
num = (-1521134295 * num) + EqualityComparer<int>.Default.GetHashCode(this.A);
return ((-1521134295 * num) + EqualityComparer<double>.Default.GetHashCode(this.B);
}
public override string ToString()
{
StringBuilder builder = new StringBuilder();
builder.Append("{ A = ");
builder.Append(this.A);
builder.Append(", B = ");
builder.Append(this.B);
builder.Append(" }");
return builder.ToString();
}
}
Ответ 5
Мне нужно только получить этот класс
public abstract class DataClass : IEquatable<DataClass>
{
public override bool Equals(object obj)
{
var other = obj as DataClass;
return this.Equals(other);
}
public bool Equals(DataClass other)
{
return (!ReferenceEquals(null, other))
&& this.Execute((self2, other2) =>
other2.Execute((other3, self3) => self3.Equals(other3), self2)
, other);
}
public override int GetHashCode()
{
return this.Execute(obj => obj.GetHashCode());
}
public override string ToString()
{
return this.Execute(obj => obj.ToString());
}
private TOutput Execute<TOutput>(Func<object, TOutput> function)
{
return this.Execute((obj, other) => function(obj), new object());
}
protected abstract TOutput Execute<TParameter, TOutput>(
Func<object, TParameter, TOutput> function,
TParameter other);
}
И затем реализовать абстрактный метод, подобный этому
public class Complex : DataClass
{
public double Real { get; set; }
public double Imaginary { get; set; }
protected override TOutput Execute<TParameter, TOutput>(
Func<object, TParameter, TOutput> function,
TParameter other)
{
return function(new
{
Real = this.Real,
Imaginary = this.Imaginary,
}, other);
}
}