Есть ли полная ссылка на реализацию IEquatable? - PullRequest
45 голосов
/ 20 августа 2009

Многие из моих вопросов здесь о SO касаются реализации IEquatable. Я обнаружил, что это крайне сложно реализовать правильно, потому что в наивной реализации много скрытых ошибок, и статьи, которые я нашел по этому поводу, довольно неполны. Я хочу найти или написать окончательную ссылку, которая должна включать:

  • Как правильно реализовать IEquatable
  • Как правильно переопределить Equals
  • Как правильно переопределить GetHashCode
  • Как правильно реализовать метод ToString
  • Как правильно реализовать оператор == правильно
  • Как реализовать оператор! = Правильно

Такая полная ссылка уже существует?

PS: Даже ссылка на MSDN мне кажется ошибочной

Ответы [ 5 ]

23 голосов
/ 20 августа 2009

Реализация IEquatable<T> для типа значения

Реализация IEquatable<T> для типа значения немного отличается от справочного типа. Предположим, у нас есть архетип типа Implement-Your-Own-Value-Type, структура сложных чисел.

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>
{
}
10 голосов
/ 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 #, что мешает мне наследовать дополнительные классы.

4 голосов
/ 16 сентября 2009

Я нашел другую ссылку, это реализация .NET Anonymous Type. Для анонимного типа со свойствами типа int и double, я разобрал следующий код C #:

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();
    }
}
4 голосов
/ 20 августа 2009

Прочитав MSDN, я уверен, что лучший пример правильной реализации - на странице Метод IEquatable.Equals . Мое единственное отклонение заключается в следующем:

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:

Реализации Equals не должны генерировать исключения.

1 голос
/ 22 декабря 2011

Я только должен быть производным от этого класса

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);
    }
}
...