C# - универсальная реализация хэш-кода для классов

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

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

// Old version, see edit
public static class HashCodeBuilder
{
    public static int Hash(params object[] keys)
    {
        if (object.ReferenceEquals(keys, null))
        {
            return 0;
        }

        int num = 42;

        checked
        {
            for (int i = 0, length = keys.Length; i < length; i++)
            {
                num += 37;
                if (object.ReferenceEquals(keys[i], null))
                { }
                else if (keys[i].GetType().IsArray)
                {
                    foreach (var item in (IEnumerable)keys[i])
                    {
                        num += Hash(item);
                    }
                }
                else
                {
                    num += keys[i].GetHashCode();
                }
            }
        }

        return num;
    }
}

и используйте его так:

// Old version, see edit
public sealed class A : IEquatable<A>
{
    public A()
    { }

    public string Key1 { get; set; }
    public string Key2 { get; set; }
    public string Value { get; set; }

    public override bool Equals(object obj)
    {
        return this.Equals(obj as A);
    }

    public bool Equals(A other)
    {
        if(object.ReferenceEquals(other, null)) 
            ? false 
            : Key1 == other.Key1 && Key2 == other.Key2;
    }

    public override int GetHashCode()
    {
        return HashCodeBuilder.Hash(Key1, Key2);
    }
}

будет много проще, что всегда есть собственный метод, нет? Я что-то упускаю?


редактировать

по всем замечаниям, я получил следующий код :

public static class HashCodeBuilder
{
    public static int Hash(params object[] args)
    {
        if (args == null)
        {
            return 0;
        }

        int num = 42;

        unchecked
        {
            foreach(var item in args)
            {
                if (ReferenceEquals(item, null))
                { }
                else if (item.GetType().IsArray)
                {
                    foreach (var subItem in (IEnumerable)item)
                    {
                        num = num * 37 + Hash(subItem);
                    }
                }
                else
                {
                    num = num * 37 + item.GetHashCode();
                }
            }
        }

        return num;
    }
}


public sealed class A : IEquatable<A>
{
    public A()
    { }

    public string Key1 { get; set; }
    public string Key2 { get; set; }
    public string Value { get; set; }

    public override bool Equals(object obj)
    {
        return this.Equals(obj as A);
    }

    public bool Equals(A other)
    {
        if(ReferenceEquals(other, null))
        {
            return false;
        }
        else if(ReferenceEquals(this, other))
        {
            return true;
        }

        return Key1 == other.Key1
            && Key2 == other.Key2;
    }

    public override int GetHashCode()
    {
        return HashCodeBuilder.Hash(Key1, Key2);
    }
}

3 ответов


ваш метод Equals сломан-предполагается, что два объекта с одинаковым хэш-кодом обязательно равны. Это просто не так.

ваш метод хэш-кода выглядел хорошо с первого взгляда, но на самом деле мог бы сделать некоторые с некоторой работой - см. ниже. Это означает, что бокс любого значения типа values и создание массива в любое время, когда вы его вызываете, но кроме этого все в порядке (как указал SLaks, есть некоторые проблемы вокруг обработки коллекции). Возможно, вы захотите рассмотрите возможность написания некоторых общих перегрузок, которые позволили бы избежать этих штрафов за производительность для общих случаев (1, 2, 3 или 4 аргумента, возможно). Вы также можете использовать foreach цикл вместо простого for цикл, просто чтобы быть идиоматичным.

вы могли бы сделать то же самое вроде вещи для равенства, но это было бы немного сложнее и грязнее.

EDIT:для самого хэш-кода Вы только добавляете значения. Я!--7-->подозреваемый вы пытались сделать такого рода вещи:

int hash = 17;
hash = hash * 31 + firstValue.GetHashCode();
hash = hash * 31 + secondValue.GetHashCode();
hash = hash * 31 + thirdValue.GetHashCode();
return hash;

а то умножение хэш на 31, это не добавить 31. В настоящее время ваш хэш-код всегда будет возвращать то же самое для тех же значений, независимо от того, находятся ли они в том же порядке, что не идеально.

EDIT: кажется, есть некоторая путаница в том, для чего используются хэш-коды. Я предлагаю всем, кто не уверен, читать документацию для Object.GetHashCode а потом Эрика Липперта запись в блоге о хэшировании и равенстве.


это то, что я использую:

public static class ObjectExtensions
{
    /// <summary>
    /// Simplifies correctly calculating hash codes based upon
    /// Jon Skeet's answer here
    /// http://stackoverflow.com/a/263416
    /// </summary>
    /// <param name="obj"></param>
    /// <param name="memberThunks">Thunks that return all the members upon which
    /// the hash code should depend.</param>
    /// <returns></returns>
    public static int CalculateHashCode(this object obj, params Func<object>[] memberThunks)
    {
        // Overflow is okay; just wrap around
        unchecked
        {
            int hash = 5;
            foreach (var member in memberThunks)
                hash = hash * 29 + member().GetHashCode();
            return hash;
        }
    }
}

пример использования:

public class Exhibit
{
    public virtual Document Document { get; set; }
    public virtual ExhibitType ExhibitType { get; set; }

    #region System.Object
    public override bool Equals(object obj)
    {
        return Equals(obj as Exhibit);
    }

    public bool Equals(Exhibit other)
    {
        return other != null &&
            Document.Equals(other.Document) &&
            ExhibitType.Equals(other.ExhibitType);
    }

    public override int GetHashCode()
    {
        return this.CalculateHashCode(
            () => Document, 
            () => ExhibitType);
    }
    #endregion
}

вместо keys[i].GetType().IsArray, вы должны попытаться бросить его в IEnumerable (через as ключевое слово).

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

обратите внимание, однако, что мой код не обрабатывает свойства коллекции.