Интересности      Книги      Утилиты    

27 января 2010 г.

Сравниваешь по Equals подразумеваешь GetHashCode

В FxCop есть такое правило Override GetHashCode on overriding Equals, переопределяйте GetHashCode переопределяя Equals. Так вот с этим правилом связан подводный камень. В Rule Description там написано об этом но как на мой взгляд не совсем ясно.

Это связано с принципом работы  HashTable и Dictionary в .NET, и для того чтобы сравнение производилось верно при переопределении Equals необходимо обязательно переопределить GetHashCode в зависимости от тех данных которые учувствуют в сравнении. Иначе Equals просто не будет вызван, хотя многие разработчики ожидают что он будет вызыватся всегда. Equals вызывается только тогда когда GetHashCode возвращает одинаковые значения, что как было сказано выше, связано с принципом работы словарей и хеш-таблиц, для того чтобы разрешить коллизии в хеш-таблице.

С другой стороны на такие неправильные выводы вполне возможно наталкивает поведение System.Object метод GetHashCode которого возвращает значения, которые не зависят от данных хранящихся в объекте и возвращает разные хеш-коды для одинаковых объектов.

Эту распространенную ошибку можно рассмотреть на примере:

Есть CustomType, по сути в нем два поля Name и Age:

public class NamesComparer : IEqualityComparer<CustomType>
{
    #region IEqualityComparer<CustomType> Members

    public bool Equals(CustomType x, CustomType y)
    {
        return string.Equals(x.Name, y.Name);
    }

    public int GetHashCode(CustomType obj)
    {
        // распространенная ошибка, неверно
        return obj.GetHashCode();
    }

    #endregion
}

[DebuggerDisplay("Name: {Name}, age: {Age}")]
public class CustomType : IEqualityComparer<CustomType>
{
    public CustomType()
    {
    }

    public CustomType(string name, int age)
    {
        Name = name;
        Age = age;
    }

    public int Age { get; set; }
    public string Name { get; set; }

    public override bool Equals(CustomType x, CustomType y)
    {
        return string.Equals(x.Name, y.Name);
    }

    public override int GetHashCode(CustomType obj)
    {
        // распространенная ошибка, неверно
        return obj.GetHashCode();
    }
}


* This source code was highlighted with Source Code Highlighter.

Есть два списка:

CustomType[] customTypeShortList = new[] {
    new CustomType("reno", 1),
    new CustomType("opel", 2) };
 
CustomType[] customTypeLongList = new[] {
    new CustomType("reno", 3),
    new CustomType("opel", 1),
    new CustomType("subaru", 1),
    new CustomType("toyota", 5),
    new CustomType("nissan", 4),
    new CustomType("audi", 3)};


Найдем их пересечение через linq оператор Intersect. Так как мы работаем с составным не примитивным типом необходимо будет указать IEqualityComparer для этого типа:

IEnumerable<CustomType> intersect = customTypeLongList
  .Intersect(customTypeShortList, new NamesComparer());


В результате ожидаем получить в списке intersect два значения: reno - 3 и opel – 1. Но мы их не получим, потому что GetHashCode в CustomType имплементирован не верно. Так как у каждого объекта в целом разные хеш коды то метод Equals даже не будет вызываться.

Правильной имплементацией в данном случае будет вариант:

public int GetHashCode(CustomType obj)
{
  return obj.Name.GetHashCode();
}


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

public int GetHashCode(CustomType obj)
{
 // также неверно, из-за того что это ударит по производительности
 return string.Empty.GetHashCode();
}


Важно также не перепутать имплементацию GetHashCode из IEqualityComparer, а не переопределение GetHashCode объекта, т.к. во втором случае словарь будет работать неправильно.

Следовательно при имплементации Equals обязательно необходимо имплементировать GetHashCode причем с учетом тех данных что принимают участие в сравнение в Equals. В противном случае Equals может даже и не вызываться.

Так как много людей попадается на этот подводный камень, я и решил написать об этом.

Progg it

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

  1. Рихтер подробно разъяснил эту траблу!

    ОтветитьУдалить
  2. И не только Рихтер, еще например в Effective C# Била Вагнера, и еще наверное в многих книгах. Только народ на этом все еще попадается

    ОтветитьУдалить
  3. Узнаю планериста :)

    Если бы вы внимательно читали статью то поняли бы что такой вариант GetHashCode подается как распространенная ошибка, т.е. неверный.

    Принципы работы хеш-таблицы здесь затрагиваются косвенно (хотя это и связанно), речь о правиле FxCop и распространенной ошибке. Да оно взаимосвязано.

    Поэтому для этого указано упоминание - "Это связано с принципом работы HashTable и Dictionary".

    Но так как принцип работы хеш-таблицы- это основы и матчасть то он здесь не рассматривается. Потому что для этого есть статья в википедии. На которую в том числе и на мсдн доку даются ссылки из статьи. Чтобы сконцентрировать внимание читателя на распространенной ошибке.

    ОтветитьУдалить
  4. Пипец.
    Что же вы комментарии удаляете :)
    Стыдно?

    ОтветитьУдалить
  5. Я не читал об этой багофиче у Рихтера, но, очень полезное замечание, я, думаю, очень важно говорить о распространенных ошибках, даже если "глубокий анализ принципов работы .net" сразу прольет свет на ситуацию. Ибо глубокий анализ начинаешь тогда, когда напоролся на баг.

    Спасибо автору за полезное замечание, это избавит меня от ряда ошибок в коде.

    ОтветитьУдалить
  6. И будете использовать

    public int GetHashCode(CustomType obj)
    {
    return string.Empty.GetHashCode();
    }

    бугага

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