Feel Good.

24 сентября 2010

Шаблон реализации Equals

Работая с объектами, нам часто явно или неявно приходится их сравнивать их друг с другом. За сравнение объектов отвечает метод Equals или его типизированная версия из IEquatable, возвращающая true в случае если объекты равны, в противном случае false. Стандартная реализация метода Equals не всегда удовлетворяет потребностям разработчика, так как она не учитывает конкретных особенностей объекта, и в этом случае, реализация конкретного Equals/IEquatable ложится на плечи самого разработчика. Каждый разработчик вправе по-своему реализовать данную функциональность (не нарушая правил Implementing the Equals Method), как ему будет удобно. У меня, как у разработчика, на этот счет со временем выработался удобный шаблон реализации метода Equals/IEquatable, состоящий из 2-х простых правил:
  1. От простого к сложному: выполнять сравнение начиная с простых условий и заканчивая более сложными.
  2. Понять равенство/неравенство объектов как можно раньше: делайте незамедлительный "return true/false".

Реализация шаблона на языке C# в комментариях на примере класса Foo:

class Foo : IEquatable<Foo>

{

    public override bool Equals(object obj)

    {

        Foo foo = obj as Foo;

        return this.Equals(foo);

    }

 

    public bool Equals(Foo other)

    {

        // От ПРОСТОГО к  СЛОЖНОМУ.

        // Выполняйте сравнение начиная с

        // простых условий (проверка на null, сравнение ссылок)

        // и заканчивая более сложными (сравнение содержимого).

 

        if (other == null)

        {

            // this заведомо не null.

            return false;

        }

 

        if (Object.ReferenceEquals(this, other))

        {

            // обе ссылки ссылаются на один и тот же объект.

            return true;

        }

 

        // Сравнение содержимого.

        // Здесь опишите конкретное сравнения по полям.

        //

        // От ПРОСТОГО к  СЛОЖНОМУ.

        // 1. Выделите самые простые относительно

        // операции сравнения поля и сравните их первыми.

        // Более сложные оставьте на конец.

        //

        // 2. Делайте незамедлительный "return false"

        // при первом же несовпадении.

        //

        // 3. Сравнивайте поля класса методом Equals,

        // за исключением простых типов: field.Equals(other.field)

        //

        // 4. Не забывайте в конце вызвать base.Equals(other).

 

        return true;

    }

}



Ссылки:
  1. Implementing the Equals Method

10 комментариев:

  1. Несколько раз сталкивался с ситуацией, когда необходимо было сделать кастомное сравнивание.
    До конца не понял поведение framework, но далеко не всегда среда вызывает метод Equals, чаще всего мне приходилось переписывать метод GetHash(), потому что именно он вызывался при сравнении объектов.
    В частности, такое поведение было замечено у DependencyObject при методе List.Containts(item)

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

    ОтветитьУдалить
  3. Вообще-то если переопределяется Equals, то нужно переопределять и GetHashCode, что у одинаковых объектов были одинаковые хеш-коды.

    Я в GetHashCode XOR'ом объединял хеш-коды всех полей, котоыре я использовал в сравнени, возможно, конечно, это не лучший вариант.

    Иначе, например, объект нельзя будет использовать в качестве ключа хеш-таблицы.

    ОтветитьУдалить
  4. Решарпер автоматизирует эту рутину :)

    ОтветитьУдалить
  5. Предлагаю первые два if в реализации метода public bool Equals(Foo other) заменить на
    retutn base.Equal(other).

    Как вам?

    ОтветитьУдалить
  6. Иван, когда по вашему в таком случае будет выполняться сравнение содержимого объектов?

    ОтветитьУдалить
  7. @Иван

    Да, можно и неявно проверять равенство ссылок через:
    if(!base.Equal(other))
    return fasle;
    Но если Foo непосредственный наследник от Object, то мне кажется, что лучше явно проверить ссылки и не делать лишний вызов base.Equals.

    Причем для reference-типов можно смело применять это, а для value-типов нужно быть осторожнее, не забывая про boxing, unboxing.

    ОтветитьУдалить
  8. @Виктор Чистов
    @Илья Дубаденко

    Согласен с Вами, написал не подумав

    ОтветитьУдалить
  9. Хотел добавить по пункту 3.
    Для полей объекта, если они ссылочные лучше писать - Object.Equals(this.field,other.field). Так как field у нашего объекта может быть null и field.Equals(other.field) выдаст исключение.

    ОтветитьУдалить
  10. Сергей, спасибо за поправку, "слепой" вызов Equals может привести к NullReferenceException. Кстати стоит отметить, что Object.Equals(null, null) дает True.

    http://stackoverflow.com/questions/1451454/c-how-does-the-static-object-equals-check-for-equality

    ОтветитьУдалить