Обработка коллекций в реализации GetHashCode

Я работаю над реализацией GetHashCode() на основе структуры HashCode в этом ответе здесь. Поскольку мой метод Equals будет рассматривать коллекции с использованием Enumerable.SequenceEqual(), мне нужно включить коллекции в мою реализацию GetHashCode().

В качестве отправной точки я использую встроенную реализацию GetHashCode() от Jon Skeet, чтобы проверить вывод реализации структуры HashCode. Это работает, как и ожидалось, используя следующий тест ниже -

private class MyObjectEmbeddedGetHashCode
{
    public int x;
    public string y;
    public DateTimeOffset z;

    public List<string> collection;

    public override int GetHashCode()
    {
        unchecked
        {
            int hash = 17;

            hash = hash * 31 + x.GetHashCode();
            hash = hash * 31 + y.GetHashCode();
            hash = hash * 31 + z.GetHashCode();

            return hash;
        }
    }
}

private class MyObjectUsingHashCodeStruct
{
    public int x;
    public string y;
    public DateTimeOffset z;

    public List<string> collection;

    public override int GetHashCode()
    {
        return HashCode.Start
            .Hash(x)
            .Hash(y)
            .Hash(z);
    }
}

[Test]
public void GetHashCode_CollectionExcluded()
{
    DateTimeOffset now = DateTimeOffset.Now;

    MyObjectEmbeddedGetHashCode a = new MyObjectEmbeddedGetHashCode() 
    { 
        x = 1, 
        y = "Fizz",
        z = now,
        collection = new List<string>() 
        { 
            "Foo", 
            "Bar", 
            "Baz" 
        } 
    };

    MyObjectUsingHashCodeStruct b = new MyObjectUsingHashCodeStruct()
    {
        x = 1,
        y = "Fizz",
        z = now,
        collection = new List<string>() 
        { 
            "Foo", 
            "Bar", 
            "Baz" 
        }
    };

    Console.WriteLine("MyObject::GetHashCode(): {0}", a.GetHashCode());
    Console.WriteLine("MyObjectEx::GetHashCode(): {0}", b.GetHashCode());

    Assert.AreEqual(a.GetHashCode(), b.GetHashCode());
}

Следующим шагом является рассмотрение коллекции в вычислении GetHashCode(). Это требует небольшого дополнения к реализации GetHashCode() в MyObjectEmbeddedGetHashCode.

public override int GetHashCode()
{
    unchecked
    {
        int hash = 17;

        hash = hash * 31 + x.GetHashCode();
        hash = hash * 31 + y.GetHashCode();
        hash = hash * 31 + z.GetHashCode();

        int collectionHash = 17;

        foreach (var item in collection)
        {
            collectionHash = collectionHash * 31 + item.GetHashCode();
        }

        hash = hash * 31 + collectionHash;

        return hash;
    }
}

Однако это немного сложнее в структуре HashCode. В этом примере, когда коллекция типа List передается в метод Hash, T является List, поэтому попытка привести obj к ICollection или IEnumberable не работает. Я могу успешно привести к IEnumerable, но это вызывает бокс, и я обнаружил, что должен беспокоиться об исключении таких типов, как string, которые реализуют IEnumerable.

Есть ли способ надежно привести obj к ICollection или IEnumerable в этом сценарии?

public struct HashCode
{
    private readonly int hashCode;

    public HashCode(int hashCode)
    {
        this.hashCode = hashCode;
    }

    public static HashCode Start
    {
        get { return new HashCode(17); }
    }

    public static implicit operator int(HashCode hashCode)
    {
        return hashCode.GetHashCode();
    }

    public HashCode Hash<T>(T obj)
    {
        // I am able to detect if obj implements one of the lower level
        // collection interfaces. However, I am not able to cast obj to
        // one of them since T in this case is defined as List<string>,
        // so using as to cast obj to ICollection<T> or IEnumberable<T>
        // doesn't work.
        var isGenericICollection = obj.GetType().GetInterfaces().Any(
            x => x.IsGenericType && 
            x.GetGenericTypeDefinition() == typeof(ICollection<>));

        var c = EqualityComparer<T>.Default;

        // This works but using IEnumerable causes boxing.
        // var h = c.Equals(obj, default(T)) ? 0 : ( !(obj is string) && (obj is IEnumerable) ? GetCollectionHashCode(obj as IEnumerable) : obj.GetHashCode());

        var h = c.Equals(obj, default(T)) ? 0 : obj.GetHashCode();
        unchecked { h += this.hashCode * 31; }
        return new HashCode(h);
    }

    public override int GetHashCode()
    {
        return this.hashCode;
    }
}

1 ответ

Решение

Вы можете решить проблему с коллекцией несколькими способами:

  1. Используйте неуниверсальный интерфейс, например ICollection или же IEnumerable,
  2. Добавьте перегрузку для Hash() метод, например Hash<T>(IEnumerable<T> list) { ... }

Тем не менее, ИМХО было бы лучше просто оставить struct HashCode в одиночку и поместите специфичный для коллекции код в ваш фактический GetHashCode() метод. Например:

public override int GetHashCode()
{
    HashCode hash = HashCode.Start
        .Hash(x)
        .Hash(y)
        .Hash(z);

    foreach (var item in collection)
    {
        hash = hash.Hash(item);
    }

    return hash;
}

Если вы хотите полнофункциональную версию struct HashCode типа, мне кажется, что на той же странице, на которую вы ссылались, есть одна: /questions/18495027/kakoj-luchshij-algoritm-dlya-pereopredelennogo-systemobjectgethashcode/18495049#18495049

Имена членов разные, но это в основном та же идея, что и struct HashCode тип, но с перегрузками для других сложных типов (как в моем предложении № 2 выше). Вы можете использовать это, или просто применить методы для реализации struct HashCode, сохраняя соглашения об именах, используемые в нем.

Другие вопросы по тегам