Метод расширения GetHashCode - PullRequest
12 голосов
/ 18 апреля 2009

После прочтения всех вопросов и ответов по StackOverflow относительно переопределения GetHashCode() я написал следующий метод расширения для простого и удобного переопределения GetHashCode():

public static class ObjectExtensions
{
    private const int _seedPrimeNumber = 691;
    private const int _fieldPrimeNumber = 397;
    public static int GetHashCodeFromFields(this object obj, params object[] fields) {
        unchecked { //unchecked to prevent throwing overflow exception
            int hashCode = _seedPrimeNumber;
            for (int i = 0; i < fields.Length; i++)
                if (fields[i] != null)
                    hashCode *= _fieldPrimeNumber + fields[i].GetHashCode();
            return hashCode;
        }
    }
}

(я в основном только рефакторинг кода, который кто-то разместил там, потому что мне действительно нравится, что он может быть использован в целом)

который я использую вот так:

    public override int GetHashCode() {
        return this.GetHashCodeFromFields(field1, field2, field3);
    }

Видите ли вы проблемы с этим кодом?

Ответы [ 9 ]

3 голосов
/ 11 июля 2009

Некоторое время назад я написал кое-что, что вы могли бы решить вашу проблему ... (И на самом деле, возможно, это можно было бы улучшить, включив в себя семена, которые у вас есть ...)

В любом случае, проект называется Essence (http://essence.codeplex.com/), и он использует библиотеки System.Linq.Expression для генерации (на основе атрибутов) стандартных представлений Equals / GetHashCode / CompareTo / ToString, а также возможность создавать классы IEqualityComparer и IComparer на основе списка аргументов. (У меня также есть некоторые дополнительные идеи, но я хотел бы получить обратную связь от сообщества, прежде чем продолжать слишком много дальше.)

(Это означает, что он почти такой же быстрый, как и рукописный - основной, где это не так, является CompareTo (); потому что в Linq.Expressions нет понятия переменной в версии 3.5 - поэтому вы должны вызывать CompareTo () для базового объекта дважды, когда вы не получаете совпадение. Использование расширений DLR для Linq.Expressions решает эту проблему. Полагаю, я мог бы использовать emit il, но меня это не вдохновило в то время.)

Это довольно простая идея, но я не видел, чтобы это было сделано раньше.

Теперь дело в том, что я как бы потерял интерес к его полировке (что включало бы написание статьи для codeproject, документирование некоторого кода и т. П.), Но я мог бы убедить вас сделать это, если вы чувствуете это будет что-то интересное.

(На сайте codeplex нет загружаемого пакета; просто перейдите к исходному коду и возьмите его - о, он написан на f # (хотя весь тестовый код на c #), так как это было тем, что мне было интересно изучать .)

В любом случае, вот пример c # из теста в проекте:

    // --------------------------------------------------------------------
    // USING THE ESSENCE LIBRARY:
    // --------------------------------------------------------------------
    [EssenceClass(UseIn = EssenceFunctions.All)]
    public class TestEssence : IEquatable<TestEssence>, IComparable<TestEssence>
    {
        [Essence(Order=0] public int MyInt           { get; set; }
        [Essence(Order=1] public string MyString     { get; set; }
        [Essence(Order=2] public DateTime MyDateTime { get; set; }

        public override int GetHashCode()                                { return Essence<TestEssence>.GetHashCodeStatic(this); }
    ...
    }

    // --------------------------------------------------------------------
    // EQUIVALENT HAND WRITTEN CODE:
    // --------------------------------------------------------------------
    public class TestManual
    {
        public int MyInt;
        public string MyString;
        public DateTime MyDateTime;

        public override int GetHashCode()
        {
            var x = MyInt.GetHashCode();
            x *= Essence<TestEssence>.HashCodeMultiplier;
            x ^= (MyString == null) ? 0 : MyString.GetHashCode();
            x *= Essence<TestEssence>.HashCodeMultiplier;
            x ^= MyDateTime.GetHashCode();
            return x;
        }
    ...
    }

Во всяком случае, проект, если кто-то считает, что стоит, нуждается в доработке, но идеи есть ...

2 голосов
/ 18 апреля 2009

Это выглядит как надежный способ сделать это.

Мое единственное предложение состоит в том, что если вы действительно обеспокоены производительностью с ним, вы можете добавить общие версии для нескольких распространенных случаев (т. Е., Вероятно, 1-4 аргумента). Таким образом, для этих объектов (которые, скорее всего, будут небольшими, составными объектами в стиле ключа), у вас не будет лишних затрат на создание массива для передачи в метод, цикл, любой бокс общих значений и т. Д. Синтаксис вызова будет точно таким же, но вы будете выполнять немного более оптимизированный код для этого случая. Конечно, я бы проверил некоторые тесты на это, прежде чем вы решите, стоит ли компромисс с обслуживанием.

Примерно так:

public static int GetHashCodeFromFields<T1,T2,T3,T4>(this object obj, T1 obj1, T2 obj2, T3 obj3, T4 obj4) {
    int hashCode = _seedPrimeNumber;
    if(obj1 != null)
        hashCode *= _fieldPrimeNumber + obj1.GetHashCode();
    if(obj2 != null)
        hashCode *= _fieldPrimeNumber + obj2.GetHashCode();
    if(obj3 != null)
        hashCode *= _fieldPrimeNumber + obj3.GetHashCode();
    if(obj4 != null)
        hashCode *= _fieldPrimeNumber + obj4.GetHashCode();
    return hashCode;
}
1 голос
/ 18 апреля 2009

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

0 голосов
/ 08 января 2015

Одна проблема, которая может возникнуть, когда умножение достигает 0, окончательный hashCode всегда равен 0, как я только что испытал для объекта с множеством свойств, в следующем коде:

hashCode *= _fieldPrimeNumber + fields[i].GetHashCode();

Я бы предложил:

hashCode = hashCode * _fieldPrimeNumber + fields[i].GetHashCode();

Или что-то похожее с xor, например this :

hashCode = hashCode * _fieldPrimeNumber ^ fields[i].GetHashCode();
0 голосов
/ 25 декабря 2011

Помимо проблем, возникающих при использовании params object[] fields, я думаю, что не использование информации о типе может быть проблемой производительности и в некоторых ситуациях. Предположим, что два класса A, B имеют одинаковый тип и количество полей и реализуют один и тот же интерфейс I. Теперь, если вы поместите A и B объекты в Dictionary<I, anything> объекты с одинаковыми полями и разными типами, вы попадете в одну корзину. Я бы, наверное, вставил какое-нибудь утверждение типа hashCode ^= GetType().GetHashCode();

Принятый ответ Джонатана Руппа касается массива params, но не имеет отношения к типам значений бокса. Таким образом, если производительность очень важна, я бы, вероятно, объявил GetHashCodeFromFields не имеющим объект, а int параметрами, и отправил бы не сами поля, а хеш-коды полей. т.е.

public override int GetHashCode() 
{
    return this.GetHashCodeFromFields(field1.GetHashCode(), field2.GetHashCode());
}
0 голосов
/ 19 апреля 2009

Я должен отметить, что при реализации GetHashCode вы почти никогда не должны выделять ресурсы (вот некоторые полезные блог сообщений об этом).

То, как работает params (создание нового массива на лету), означает, что это не очень хорошее общее решение. Было бы лучше использовать вызов метода для каждого поля и поддерживать состояние хеш-функции как передаваемую им переменную (это упрощает использование лучших функций хеширования и лавинного преобразования).

0 голосов
/ 19 апреля 2009

Более оптимально:

  1. Создайте генератор кода, который использует отражение для просмотра полей вашего бизнес-объекта и создает новый частичный класс, который переопределяет GetHashCode () (и Equals ()).
  2. Запустите генератор кода, когда ваша программа запускается в режиме отладки, и, если код изменился, выйдите с сообщением разработчику для перекомпиляции.

Преимущества этого:

  • Используя рефлексию, вы знаете, какие поля являются типами значений или нет, и, следовательно, нужны ли им проверки на нуль.
  • Нет никаких накладных расходов - никаких дополнительных вызовов функций, построение списков и т. Д. Это важно, если вы выполняете много поисков в словаре.
  • Длинные реализации (в классах с большим количеством полей) скрыты в частичных классах, вне вашего важного бизнес-кода.

Недостатки:

  • Избыточность, если вы не выполняете много словарных поисков / обращений к GetHashCode ().
0 голосов
/ 18 апреля 2009
public override int GetHashCode() {
    return this.GetHashCodeFromFields(field1, field2, field3, this);
}

(да, я очень педантичный, но это единственная проблема, которую я вижу)

0 голосов
/ 18 апреля 2009

По общему принципу вы должны охватить unchecked настолько узко, насколько это возможно, хотя здесь это не имеет большого значения Кроме того, выглядит хорошо.

...