【问题标题】:Handling collections in GetHashCode implementation在 GetHashCode 实现中处理集合
【发布时间】:2015-04-17 05:57:07
【问题描述】:

我正在根据这个答案here 中的 HashCode 结构实现 GetHashCode()。由于我的 Equals 方法将考虑使用 Enumerable.SequenceEqual() 的集合,因此我需要在 GetHashCode() 实现中包含这些集合。

作为起点,我使用 Jon Skeet 的嵌入式 GetHashCode() 实现来测试 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() 计算中考虑集合。这需要对 MyObjectEmbeddedGetHashCode 中的 GetHashCode() 实现进行少量添加。

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,但它会导致装箱,我发现我不得不担心排除实现 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;
    }
}

【问题讨论】:

  • 旁注:GetHashCode 预计会很快,对所有项目的反射和迭代并不是“快速”代码的最佳示例。
  • 您要解决什么问题?您不必将集合包含在哈希中。如果 XYZ 为您提供了良好的哈希分布,那么就到此为止。
  • @AlexeiLevenkov 我在此处使用和不使用 LINQ 查询的情况下围绕 GetHashCode 的调用放置了一个秒表,它运行 5 毫秒,没有运行 3 毫秒,因此需要考虑性能影响。

标签: c# generics collections gethashcode iequatable


【解决方案1】:

您可以通过以下几种方式解决收集问题:

  1. 使用非通用接口,例如ICollectionIEnumerable
  2. Hash() 方法添加重载,例如Hash&lt;T&gt;(IEnumerable&lt;T&gt; 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 类型的全功能版本,在我看来,您引用的同一页面似乎有一个:https://stackoverflow.com/a/2575444/3538012

成员的命名不同,但它与struct HashCode 类型的想法基本相同,但对其他复杂类型有重载(如我上面的建议#2)。您可以使用它,或者只是将那里的技术应用到您的 struct HashCode 实现中,保留其中使用的命名约定。

【讨论】:

  • 我将此标记为答案,因为为 Hash 方法添加重载提供了所需的行为。但是,由于我的几个类之间的双向关联,我遇到了一个无限递归问题,导致我的 Equals() 覆盖中出现 StackOverflowException。这导致我放弃在我的 IEquatable 实现中包含集合和我的引用类型,我最终使用了类似于@Blam 在我的问题的评论中提到的解决方案。
猜你喜欢
  • 2011-02-23
  • 2012-05-21
  • 2019-10-26
  • 2017-02-08
  • 1970-01-01
  • 2011-02-22
  • 2019-10-10
  • 2022-01-23
  • 1970-01-01
相关资源
最近更新 更多