Runtime: 添加 HashCode 类型以帮助组合哈希码

创建于 2016-04-25  ·  206评论  ·  资料来源: dotnet/runtime

新问题 dotnet/corefx#14354替换 200 多条评论的冗长讨论

此问题已关闭!!!


动机

Java 有Objects.hash用于快速组合组成字段的哈希码以返回Object.hashCode() 。 不幸的是,.NET 没有这样的等价物,开发人员被迫像这样滚动他们自己的哈希:

public override int GetHashCode()
{
    unchecked
    {
        int result = 17;
        result = result * 23 + field1.GetHashCode();
        result = result * 23 + field2.GetHashCode();
        return result;
    }
}

有时人们甚至为此使用Tuple.Create(field1, field2, ...).GetHashCode() ,这很糟糕(显然),因为它分配了。

提议

  • 当前提案的更改列表(针对最后批准的版本 https://github.com/dotnet/corefx/issues/8034#issuecomment-262331783):

    • Empty属性(作为类似于ImmutableArray自然起点)

    • 参数名称更新: hash -> hashCode , obj -> item

namespace System
{
    public struct HashCode : IEquatable<HashCode>
    {
        public HashCode();

        public static HashCode Empty { get; }

        public static HashCode Create(int hashCode);
        public static HashCode Create<T>(T item);
        public static HashCode Create<T>(T item, IEqualityComparer<T> comparer);

        public HashCode Combine(int hashCode);
        public HashCode Combine<T>(T item);
        public HashCode Combine<T>(T item, IEqualityComparer<T> comparer);

        public int Value { get; }

        public static implicit operator int(HashCode hashCode);

        public static bool operator ==(HashCode left, HashCode right);
        public static bool operator !=(HashCode left, HashCode right);

        public bool Equals(HashCode other);
        public override bool Equals(object obj);
        public override int GetHashCode();
        public override string ToString();
    }
}

用法:

```c#
int hashCode1 = HashCode.Create(f1).Combine(f2).Value;
int hashCode2 = hashes.Aggregate(HashCode.Empty, (seed, hash) => seed.Combine(hash));

var hashCode3 = HashCode.Empty;
foreach (int hash in hash) { hashCode3 = hashCode3.Combine(hash); }
(int)hashCode3;
``

笔记

实现应该使用HashHelpers算法。

Design Discussion api-needs-work area-System.Numerics

最有用的评论

[@redknightlois] 如果我们需要的是为什么要购买System的理由,我可以尝试辩解。 我们构建了HashCode来帮助实现object.GetHashCode() ,这听起来很合适,两者都可以共享命名空间。

这也是@KrzysztofCwalina和我使用的理由。 卖!

所有206条评论

如果你想要一些快速而肮脏的东西,那么你可以使用ValueTuple.Create(field1, field2).GetHashCode() 。 它与Tuple中使用的算法相同(就此而言,类似于Objects )并且没有分配开销。

否则会有一些问题,比如你需要多好的散列,可能的字段值是什么(这会影响哪些算法会给出好的或坏的结果),是否有可能发生 hashDoS 攻击,是否以二进制为模进行碰撞 -偶数伤害(就像他们对二进制偶数哈希表所做的那样)等等,使得一刀切不适用。

@JonHanna我认为这些问题也适用于,例如, string.GetHashCode() 。 我不明白为什么提供Hash应该比这更难。

其实应该更简单,因为有特殊要求的用户可以很容易地停止使用Hash ,但是停止使用string.GetHashCode()更难。

如果你想要一些快速而肮脏的东西,那么你可以使用 ValueTuple.Create(field1, field2).GetHashCode()。

啊,好主意,我在发这篇文章时没想到ValueTuple 。 不幸的是,我认为在 C# 7/下一个框架版本发布之前,它不会可用,或者甚至不知道它是否会如此高效(对EqualityComparer那些属性/方法调用可以加起来)。 但我没有采取任何基准来衡量这一点,所以我真的不知道。 我只是认为应该有一个专用/简单的散列类,人们可以使用它而无需使用元组作为黑客的解决方法。

否则会有一些问题,比如你需要多好的散列,可能的字段值是什么(这会影响哪些算法会给出好的或坏的结果),是否有可能发生 hashDoS 攻击,是否以二进制为模进行碰撞 -偶数伤害(就像他们对二进制偶数哈希表所做的那样)等等,使得一刀切不适用。

完全同意,但我认为大多数实现都没有考虑到这一点,例如ArraySegment当前实现非常幼稚。 这个类(以避免分配一起)的主要目的是提供一展身手,以实现对人不是很了解散列谁做,以防止他们做一些愚蠢像这样。 需要处理你描述的情况的人可以实现自己的哈希算法。

不幸的是,我认为在 C# 7/下一个框架发布之前它不会可用

我认为您可以将它与 C# 2 一起使用,只是不能与内置支持一起使用。

甚至知道它是否会那么高效(对 EqualityComparer 的那些属性/方法调用可以加起来)

这个类会有什么不同? 如果显式调用obj == null ? 0 : obj.GetHashCode()更快,则应将其移入ValueTuple

几周前我本来倾向于 +1 这个提议,但我不太倾向于考虑ValueTuple减少使用Tuple的技巧的分配开销,这对我来说似乎介于两个凳子之间:如果您不需要特别专业的东西,那么您可以使用ValueTuple ,但是如果您需要除此之外的东西,那么像这样的课程不会走多远足够的。

当我们有了 C#7 时,它就会有语法糖来使它更容易。

@乔汉娜

这个类会有什么不同? 如果显式调用 obj == null ? 0 : obj.GetHashCode() 比移入 ValueTuple 的速度要快。

为什么不让ValueTuple只使用Hash类来获取哈希码? 这也会显着减少文件中的 LOC(现在大约是 2000 行)。

编辑:

如果您不需要特别专业的东西,那么您可以使用 ValueTuple

是的,但问题是很多人可能没有意识到这一点并实现了他们自己的低级朴素哈希函数(就像我上面链接的那个)。

我确实可以落后。

可能超出了这个问题的范围。 但是拥有一个散列命名空间,我们可以在其中找到专家编写的高性能加密和非加密散列,这将是一个胜利。

例如,我们必须自己编写 xxHash32、xxHash64、Metro128 以及从 128 到 64 和从 64 到 32 位的下采样。 拥有一系列优化的函数可以帮助开发人员避免编写自己的未优化和/或错误的函数(我知道,我们自己也发现了一些错误); 但仍然可以根据需要进行选择。

如果有兴趣,我们很乐意捐赠我们的实现,以便专家进一步审查和优化它们。

@redknightlois我很乐意将我的 SpookyHash 实现添加到这样的工作中。

@svick小心 string.GetHashCode() 虽然,这是非常具体的,有很好的理由,Hash DoS 攻击。

@terrajobst ,这在 API 分类/审查队列中有多远? 我认为这是一个我们一直想添加到平台的简单 API,可能我们现在有足够的临界质量来实际执行它?

抄送:@ellismg

我认为它已准备好以当前状态进行审查。

@mellinoe太好了! 我对提案进行了一些清理,使其更加简洁,并在最后添加了一些我认为应该解决的问题。

@jamesqo也应该基于long

@redknightlois ,听起来很合理。 我更新了提案以包含Combine long重载。

@JonHanna的建议不够好吗?

C# return ValueTuple.Create(a, b, c).GetHashCode();

除非有足够充分的理由说明它不够好,否则我们认为它不会成功。

除了生成的代码差几个数量级之外,我想不出任何其他足够好的理由。 当然,除非在新的运行时中有处理这种特殊情况的优化,在这种情况下,这种分析没有实际意义。 话虽如此,我在 1.0.1 上尝试过这个。

让我用一个例子来说明。

假设我们采用用于ValueTuple的实际代码并使用常量来调用它。

        internal static class HashHelpers
        {
            public static int Combine(int h1, int h2)
            {
                // The jit optimizes this to use the ROL instruction on x86
                // Related GitHub pull request: dotnet/coreclr#1830
                uint shift5 = ((uint)h1 << 5) | ((uint)h1 >> 27);
                return ((int)shift5 + h1) ^ h2;
            }
        }

        [MethodImpl(MethodImplOptions.NoInlining)]
        public static int TryStaticCall()
        {
            return HashHelpers.Combine(10202, 2003);
        }

        [MethodImpl(MethodImplOptions.NoInlining)]
        public static int TryValueTuple()
        {
            return ValueTuple.Create(10202, 2003).GetHashCode();
        }
    }

现在,在优化编译器下,应该没有任何区别,但实际上确实存在。

这是ValueTuple的实际代码

image
那么现在可以在这里看到什么? 首先我们在堆栈中创建一个结构体,然后我们调用实际的哈希码。

现在将它与HashHelper.Combine的使用进行比较,出于所有目的,它可能是Hash.Combine的实际实现

image

我知道!!!
但我们不要停在那里......让我们使用实际参数:

        [MethodImpl(MethodImplOptions.NoInlining)]
        public static int TryStaticCall(int h1, int h2)
        {
            return HashHelpers.Combine(h1, h2);
        }

        [MethodImpl(MethodImplOptions.NoInlining)]
        public static int TryValueTuple(int h1, int h2)
        {
            return ValueTuple.Create(h1, h2).GetHashCode();
        }

        static unsafe void Main(string[] args)
        {
            var g = new Random();
            int h1 = g.Next();
            int h2 = g.Next(); 
            Console.WriteLine(TryStaticCall(h1, h2));
            Console.WriteLine(TryValueTuple(h1, h2));
        }

image

好东西,这是非常稳定的。 但是让我们将其与替代方案进行比较:

image

现在让我们过火...

        internal static class HashHelpers
        {
            public static int Combine(int h1, int h2)
            {
                // The jit optimizes this to use the ROL instruction on x86
                // Related GitHub pull request: dotnet/coreclr#1830
                uint shift5 = ((uint)h1 << 5) | ((uint)h1 >> 27);
                return ((int)shift5 + h1) ^ h2;
            }
            public static int Combine(int h1, int h2, int h3, int h4)
            {
                return Combine(Combine(h1, h2), Combine(h3, h4));
            }
        }

        [MethodImpl(MethodImplOptions.NoInlining)]
        public static int TryStaticCall(int h1, int h2, int h3, int h4)
        {
            return HashHelpers.Combine(h1, h2, h3, h4);
        }

结果非常具有说明性

image

我无法真正检查 JIT 为调用生成的实际代码,但仅序言和结语就足以证明包含该提案的合理性。

image

分析的要点很简单:持有类型是struct并不意味着它是免费的:)

会议期间提出了绩效。 问题是这个 API 是否可能在热门路径上。 需要明确的是,我并不是说我们不应该拥有 API。 我只是想说,除非有具体的场景,否则设计 API 会更困难,因为我们不能说“我们需要它用于 X,因此衡量成功的标准是 X 是否可以使用它”。 这对于不允许您做新的事情,而是以更优化的方式做同样的事情的 API 很重要。

我认为拥有快速、高质量的散列越重要,调整用于对象的算法和可能看到的值范围就越重要,因此您越需要这样一个帮手,越不需要使用这样的帮手。

@terrajobst ,性能是这个提议的主要动机,但不是唯一的动机。 拥有专用类型将有助于提高可发现性; 即使使用 C# 7 中的内置元组支持,开发人员也不一定知道它们与值相等。 即使他们这样做了,他们也可能忘记元组覆盖GetHashCode ,并且可能最终不得不谷歌如何在 .NET 中实现GetHashCode

此外,使用ValueTuple.Create.GetHashCode存在一个微妙的正确性问题。 过去 8 个元素,只对最后 8 个元素进行哈希处理; 其余的被忽略。

@terrajobst在 RavenDB,GetHashCode 的性能对我们的底线造成了如此大的打击,以至于我们最终实施了一整套高度优化的例程。 甚至 Roslyn 也有自己的内部散列https://github.com/dotnet/roslyn/blob/master/src/Compilers/Core/Portable/InternalUtilities/Hash.cs也在这里专门检查 Roslyn 的讨论: https://github .com/dotnet/coreclr/issues/1619 ...所以当性能是关键时,我们不能使用提供的平台,必须自己推出(并承担后果)。

@jamesqo问题也完全有效。 不需要组合这么多散列,但是对于 100 万个案例,有人会与那个人一起跨过悬崖。

@乔汉娜

我认为拥有快速、高质量的散列越重要,调整用于对象的算法和可能看到的值范围就越重要,因此您越需要这样一个帮手,越不需要使用这样的帮手。

所以你是说添加一个辅助类会很糟糕,因为它会鼓励人们直接加入辅助函数而不考虑如何进行正确的散列?

实际上,情况似乎正好相反; Hash.Combine通常应该改进GetHashCode实现。 知道如何进行散列的人可以评估Hash.Combine以查看它是否适合他们的用例。 不太了解散列的新手会使用Hash.Combine而不是异或(或更糟的是,添加)组成字段,因为他们不知道如何进行正确的散列。

我们对此进行了更多讨论,您说服了我们:-)

还有几个问题:

  1. 我们需要决定把这个类型放在哪里。 引入一个新的命名空间似乎很奇怪; System.Numerics可能会起作用。 System.Collections.Generic也可能有效,因为它具有比较器,并且散列最常用于集合的上下文中。
  2. 我们是否应该提供一种无需分配的构建器模式来组合未知数量的哈希码?

在 (2) @Eilon 上有这样的说法:

作为参考,ASP.NET Core(及其前辈和相关项目)使用 HashCodeCombiner: https :

@David Fowler 几个月前在 GitHub 线程中提到了它。)

这是一个示例用法: https :

``` C#
var hashCodeCombiner = HashCodeCombiner.Start();
hashCodeCombiner.Add(IsMainPage ? 1 : 0);
hashCodeCombiner.Add(ViewName, StringComparer.Ordinal);
hashCodeCombiner.Add(ControllerName, StringComparer.Ordinal);
hashCodeCombiner.Add(AreaName, StringComparer.Ordinal);

如果(ViewLocationExpanderValues != null)
{
foreach(ViewLocationExpanderValues 中的 var 项目)
{
hashCodeCombiner.Add(item.Key, StringComparer.Ordinal);
hashCodeCombiner.Add(item.Value, StringComparer.Ordinal);
}
}

返回 hashCodeCombiner;
``

我们对此进行了更多讨论,您说服了我们:-)

🎉

引入一个新的命名空间似乎很奇怪; System.Numerics 可能会工作。

如果我们决定不添加新的命名空间,那么应该注意的是,任何具有名为Hashusing System.Numerics指令的代码都将无法编译,并出现不明确的类型错误。

我们是否应该提供一种无需分配的构建器模式来组合未知数量的哈希码?

这听起来是个好主意。 作为一对夫妇的初步建议,也许我们应该将其命名为HashBuilder (一拉StringBuilder ),并把它return this后,每Add方法,使其更容易添加哈希,如下所示:

public override int GetHashCode()
{
    return HashBuilder.Create(_field1)
        .Add(_field2)
        .Add(_field3)
        .ToHash();
}

@jamesqo请在对线程达成共识时更新顶部的提案。 然后我们可以做最后的审查。 在您推动设计时暂时分配给您;-)

如果我们决定不添加新的命名空间,那么应该注意的是,任何具有名为Hashusing System.Numerics指令的代码都将无法编译,并出现不明确的类型错误。

视实际情况而定。 在许多情况下,编译器会更喜欢您的类型,因为在考虑使用指令之前,会遍历编译单元的定义命名空间层次结构。

但即便如此:添加 API 可能是一个破坏源头的变化。 然而,避免这种情况是不切实际的,假设我们想要取得进展😄我们通常会努力避免冲突,例如使用不太通用的名称。 例如,我认为我们不应该调用Hash 。 我认为HashCode可能会更好。

作为一些初步建议,也许我们应该将其命名为 HashBuilder

作为第一个近似值,我正在考虑将静态和构建器合并为一个类型,如下所示:

``` C#
命名空间 System.Collections.Generic
{
公共结构哈希码
{
public static int Combine(int hash1, int hash2);
public static int Combine(int hash1, int hash2, int hash3);
public static int Combine(int hash1, int hash2, int hash3, int hash4);
public static int Combine(int hash1, int hash2, int hash3, int hash4, int hash5);
public static int Combine(int hash1, int hash2, int hash3, int hash4, int hash5, int hash6);

    public static long Combine(long hash1, long hash2);
    public static long Combine(long hash1, long hash2, long hash3);
    public static long Combine(long hash1, long hash2, long hash3, long hash4);
    public static long Combine(long hash1, long hash2, long hash3, long hash4, long hash5);
    public static long Combine(long hash1, long hash2, long hash3, long hash4, long hash5, longhash6);

    public static int CombineHashCodes<T1, T2>(T1 o1, T2 o2);
    public static int CombineHashCodes<T1, T2, T3>(T1 o1, T2 o2, T3 o3);
    public static int CombineHashCodes<T1, T2, T3, T4>(T1 o1, T2 o2, T3 o3, T4 o4);
    public static int CombineHashCodes<T1, T2, T3, T4, T5>(T1 o1, T2 o2, T3 o3, T4 o4, T5 o5);
    public static int CombineHashCodes<T1, T2, T3, T4, T5, T6>(T1 o1, T2 o2, T3 o3, T4 o4, T5 o5, T6 o6);

    public void Combine(int hashCode);
    public void Combine(long hashCode);
    public void Combine<T>(T obj);
    public void Combine(string text, StringComparison comparison);

    public int Value { get; }
}

}

This allows for code like this:

``` C#
return HashCode.Combine(value1, value2);

也:

``` C#
var hashCode = new HashCode();
hashCode.Combine(IsMainPage ? 1 : 0);
hashCode.Combine(ViewName, StringComparer.Ordinal);
hashCode.Combine(ControllerName, StringComparer.Ordinal);
hashCode.Combine(AreaName, StringComparer.Ordinal);

如果(ViewLocationExpanderValues != null)
{
foreach(ViewLocationExpanderValues 中的 var 项目)
{
hashCode.Combine(item.Key, StringComparer.Ordinal);
hashCode.Combine(item.Value, StringComparer.Ordinal);
}
}

返回 hashCode.Value;
``

想法?

我喜欢@jamesqo的链接调用的想法(从实例方法Combine返回this Combine )。

我什至会完全删除静态方法并只保留实例方法......

Combine(long hashCode)将直接转换为int 。 我们真的想要那个吗?
long重载的用例是什么?

@karelz请不要删除它们,结构不是免费的。 哈希可以在非常热的路径中使用,当静态方法基本上是免费的时,您当然不想浪费指令。 看看我对代码的分析,我展示了封闭结构的真正影响。

我们使用Hashing静态类来避免名称冲突,代码看起来不错。

@redknightlois我想知道在具有一个 int 字段的非泛型结构的情况下,我们是否也应该期待相同的“坏”代码。
如果那仍然是“糟糕的”汇编代码,我想知道我们是否可以改进 JIT 以在此处更好地进行优化。 添加 API 只是为了节省一些指令应该是我们最后的手段 IMO。

@redknightlois好奇,如果结构体(在本例中HashCode )可以放入寄存器,JIT 是否会生成更糟糕的代码? 它只会是int大。

此外,我最近在 coreclr 中看到了很多拉取请求,以改进围绕结构生成的代码,看起来 dotnet/coreclr#8057 将启用这些优化。 或许JIT 生成的代码在这个改变之后会更好?

编辑:我看到@karelz已经在这里提到了我的观点。

@karelz ,我同意你的看法——假设 JIT 为int大小的结构生成体面的代码(我相信它确实如此,例如ImmutableArray没有开销)那么静态重载是多余的,可以删除。

@terrajobst我还有一些想法:

  • 我认为我们可以将您和我的想法结合起来。 HashCode似乎是个好名字; 它不必是遵循构建器模式的可变结构。 相反,它可以是int周围的不可变包装器,并且每个Combine操作都可以返回一个新的HashCode值。 例如
public struct HashCode
{
    private readonly int _hash;

    public HashCode Combine(int hash) => return new HashCode(CombineCore(_hash, hash));

    public HashCode Combine<T>(T item) => Combine(EqualityComparer<T>.Default.GetHashCode(item));
}

// Usage
HashCode combined = new HashCode(_field1)
    .Combine(_field2)
    .Combine(_field3);
  • 我们应该有一个隐式运算符来转换为int这样人们就不必最后一次调用.Value
  • Re Combine ,这是最好的名字吗? 这听起来更具描述性,但Add更短且更易于输入。 ( Mix是另一种选择,但打字有点痛苦。)

    • public void Combine(string text, StringComparison comparison) :我不认为这真的属于同一类型,因为这与字符串无关。 此外,在您需要这样做的极少数情况下,编写StringComparer.XXX.GetHashCode(str)很容易。

    • 我们应该从这个类型中移除 long 重载并为 longs 设置一个单独的HashCode类型。 像Int64HashCodeLongHashCode

我在 TryRoslyn 上做了一个小示例实现: http ://tinyurl.com/zej9yux

幸运的是,它很容易检查。 好消息是它可以正常工作👍

image

我们应该只需要一个隐式运算符来转换为 int,这样人们就不必进行最后一个 .Value 调用。

可能代码并不那么简单,进行隐式转换会稍微清理一下。 我仍然喜欢能够有多个参数接口的想法。

        [MethodImpl(MethodImplOptions.NoInlining)]
        public static int TryHashCombiner(int h1, int h2, int h3, int h4)
        {
            var h = new HashCode(h1).Combine(h2).Combine(h3).Combine(h4);
            return h.Value;
        }

重新组合,这是最好的名字吗? 这听起来更具描述性,但 Add 更短且更易于输入。 (Mix 是另一种选择,但打字有点痛苦。)

Combine 是哈希社区 afaik 中使用的实际名称。 它可以让您清楚地了解它正在做什么。

@jamesqo有很多散列函数,我们必须为 RavenDB 实现非常快的版本,从 32 位、64 位到 128 位(我们将每一个都用于不同的目的)。

我们可以在这个设计中使用一些像这样的可扩展机制来思考:

        internal interface IHashCode<T> where T : struct
        {
            T Combine(T h1, T h2);
        }

        internal struct RotateHashCode : IHashCode<int>, IHashCode<long>
        {
            long IHashCode<long>.Combine(long h1, long h2)
            {
                // The jit optimizes this to use the ROL instruction on x86
                // Related GitHub pull request: dotnet/coreclr#1830
                ulong shift5 = ((ulong)h1 << 5) | ((ulong)h1 >> 27);
                return ((int)shift5 + h1) ^ h2;
            }

            int IHashCode<int>.Combine(int h1, int h2)
            {
                // The jit optimizes this to use the ROL instruction on x86
                // Related GitHub pull request: dotnet/coreclr#1830
                uint shift5 = ((uint)h1 << 5) | ((uint)h1 >> 27);
                return ((int)shift5 + h1) ^ h2;
            }
        }

        internal struct HashCodeCombiner<T, W> where T : struct, IHashCode<W>
                                               where W : struct
        {
            private static T hasher;
            public W Value;

            static HashCodeCombiner()
            {
                hasher = new T();
            }

            [MethodImpl(MethodImplOptions.AggressiveInlining)]
            public HashCodeCombiner(W seed)
            {
                this.Value = seed;
            }

            [MethodImpl(MethodImplOptions.AggressiveInlining)]
            public HashCodeCombiner<T,W> Combine( W h1 )
            {
                Value = hasher.Combine(this.Value, h1);
                return this;
            }
        }

        [MethodImpl(MethodImplOptions.NoInlining)]
        public static int TryHashCombinerT(int h1, int h2, int h3, int h4)
        {
            var h = new HashCodeCombiner<RotateHashCode, int>(h1).Combine(h2).Combine(h3).Combine(h4);
            return h.Value;
        }

我不知道为什么 JIT 会为此创建一个非常烦人的序言代码。 不应该,所以它可能可以优化,我们应该向 JIT 开发人员询问它。 但是对于其余部分,您可以根据需要实现任意数量的不同组合器,而不会浪费一条指令。 话虽如此,这种方法对于实际的散列函数可能比对于组合器更有用。 抄送@CarolEidt @AndyAyersMS

编辑:在这里大声思考将加密和非加密散列函数结合在单个散列概念伞下的通用机制。

@jamesqo

它不必是遵循构建器模式的可变结构

没错。 在那种情况下,我可以接受这种模式。 如果操作有副作用,我通常不喜欢返回实例的模式。 如果 API 遵循不可变的WithXxx模式,则尤其糟糕。 但在这种情况下,模式本质上是一个不可变的数据结构,因此模式可以正常工作。

我认为我们可以将您和我的想法结合起来。

👍,那又如何:

``` C#
公共结构哈希码
{
公共静态 HashCode 创建(T 对象);

[Pure] public HashCode Combine(int hashCode);
[Pure] public HashCode Combine(long hashCode);
[Pure] public HashCode Combine<T>(T obj);
[Pure] public HashCode Combine(string text, StringComparison comparison);

public int Value { get; }

public static implicit operator int(HashCode hashCode);

}

This allows for code like this:

``` C#
public override int GetHashCode()
{
    return HashCode.Create(value1).Combine(value2);
}

还有这个:

``` C#
var hashCode = new HashCode()
.Combine(IsMainPage ? 1 : 0)
.Combine(ViewName, StringComparer.Ordinal)
.Combine(ControllerName, StringComparer.Ordinal)
.Combine(AreaName, StringComparer.Ordinal);

如果(ViewLocationExpanderValues != null)
{
foreach(ViewLocationExpanderValues 中的 var 项目)
{
hashCode = hashCode.Combine(item.Key, StringComparer.Ordinal);
hashCode = hashCode.Combine(item.Value, StringComparer.Ordinal);
}
}

返回 hashCode.Value;
``

@terrajobst想法:

  1. Create<T>工厂方法应该被删除。 否则,将有 2 种编写相同内容的方法, HashCode.Create(_val)new HashCode().Combine(_val) 。 此外,为Create / Combine使用不同的名称不会对差异友好,因为如果添加新的第一个字段,则必须更改 2 行。
  2. 我认为接受字符串/StringComparison 的重载不属于这里; HashCode与字符串无关。 相反,也许我们应该在字符串中添加一个GetHashCode(StringComparison) api? (所有这些都是序数比较,这是string.GetHashCode的默认行为。)
  3. 如果已经存在用于转换为int的隐式运算符,那么拥有Value有什么意义? 同样,这会导致不同的人写不同的东西。
  4. 我们必须将long重载移动到新类型。 HashCode将只有 32 位宽; 它不能长。
  5. 让我们添加一些采用无符号类型的重载,因为它们在散列中更常见。

这是我建议的 API:

public struct HashCode
{
    public HashCode Combine(int hash);
    public HashCode Combine(uint hash);
    public HashCode Combine<T>(T obj);

    public static implicit operator int(HashCode hashCode);
    public static implicit operator uint(HashCode hashCode);
}

public struct Int64HashCode
{
    public Int64HashCode Combine(long hash);
    public Int64HashCode Combine(ulong hash);

    public static implicit operator long(Int64HashCode hashCode);
    public static implicit operator ulong(Int64HashCode hashCode);
}

仅使用这些方法,ASP.NET 中的示例仍然可以编写为

var hashCode = new HashCode()
    .Combine(IsMainPage ? 1 : 0)
    .Combine(ViewName)
    .Combine(ControllerName)
    .Combine(AreaName);

if (ViewLocationExpanderValues != null)
{
    foreach (var item in ViewLocationExpanderValues)
    {
        hashCode = hashCode.Combine(item.Key);
        hashCode = hashCode.Combine(item.Value);
    }
}

return hashCode;

@jamesqo

如果已经存在用于转换为int的隐式运算符,那么拥有Value有什么意义? 同样,这会导致不同的人写不同的东西。

运算符重载的框架设计指南说:

考虑提供具有与每个重载运算符相对应的友好名称的方法。

许多语言不支持运算符重载。 出于这个原因,建议重载运算符的类型包括具有适当的域特定名称的辅助方法,该名称提供等效的功能。

具体来说,F# 是一种使调用隐式转换运算符变得笨拙的语言。


另外,我不认为只有一种做事方式那么重要。 在我看来,让 API 方便更重要。 如果我只想组合几个值的哈希码,我认为HashCode.CombineHashCodes(value1, value2, value3)new HashCode().Combine(value1).Combine(value2).Combine(value3)更简单、更短、更容易理解。

实例方法API对于更复杂的情况还是有用的,但是我认为更常见的情况应该有更简单的静态方法API。

@svick ,您关于其他语言不支持运算符的观点是合法的。 我让步,让我们加上Value

我不认为只有一种做事方式那么重要。

这很重要。 如果有人以一种方式做,然后阅读以另一种方式做的人的代码,那么他/她将不得不在谷歌上搜索另一种方式。

如果我只想组合几个值的哈希码,我认为 HashCode.CombineHashCodes(value1, value2, value3) 比 new HashCode().Combine(value1).Combine(value2).Combine() 更简单、更短、更容易理解值 3)。

  • 静态方法的问题在于,由于不会有params int[]重载,我们将不得不为每个不同的元数添加重载,这更不划算。 让一种方法涵盖所有​​用例要好得多。
  • 第二种形式一旦你看到一两次就会很容易理解。 事实上,您可能会争辩说它更具可读性,因为它更容易垂直链接(从而在添加/删除字段时最小化差异):
public override int GetHashCode()
{
    return new HashCode()
        .Combine(_field1)
        .Combine(_field2)
        .Combine(_field3)
        .Combine(_field4);
}

[@svick] 我不认为只有一种做事方式那么重要。

我认为尽量减少做同一件事的方法很重要,因为它可以避免混淆。 同时,如果有助于实现其他目标,例如可发现性、便利性、性能或可读性,我们的目标不是 100% 重叠免费。 一般来说,我们的目标是最小化概念,而不是 API。 例如,多个重载比使用多个不同的方法与不相交的术语问题要少。

我添加工厂方法的原因是要清楚人们如何获得初始哈希码。 创建后跟Combine的空结构似乎不是很直观。 合乎逻辑的事情是添加 .ctor,但为了避免装箱,它必须是通用的,而使用 .ctor 则无法做到这一点。 一个通用的工厂方法是下一个最好的东西。

一个很好的副作用是它看起来与框架中不可变数据结构的外观非常相似。 在 API 设计中,我们强烈支持几乎所有其他方面的一致性。

[@svick] 如果我只想组合几个值的哈希码,我认为 HashCode.CombineHashCodes(value1, value2, value3) 比 new HashCode().Combine(value1).Combine(value2) 更简单、更短、更容易理解). 组合(值3)。

我同意@jamesqo :我喜欢构建器模式,它可以扩展到任意数量的参数,而性能损失最小(如果有的话,取决于我们的内联程序有多好)。

[@jamesqo] 我认为接受字符串/StringComparison 的重载不属于这里; HashCode 与字符串无关

有道理。 我添加它是因为它在@Eilon的代码中被引用。 根据经验,我会说字符串非常常见。 另一方面,我不确定是否指定比较。 让我们暂时搁置它。

[@jamesqo] 我们必须将长重载移至新类型。 HashCode将只有 32 位宽; 它不能长。

那是个很好的观点。 我们甚至需要long版本吗? 我把它留在里面只是因为上面提到过,我并没有真正考虑它。

既然如此,看来我们应该只保留 32 位,因为这就是 .NET GetHashCode()所在。 在这种情况下,我什至不确定我们是否应该添加uint版本。 如果您在该领域之外使用散列,我认为可以将人们指向我们在System.Security.Cryptography拥有的更通用的散列算法。

```C#
公共结构哈希码
{
公共静态 HashCode 创建(T 对象);

[Pure] public HashCode Combine(int hashCode);
[Pure] public HashCode Combine<T>(T obj);

public int Value { get; }

public static implicit operator int(HashCode hashCode);

}
``

既然是这样,看来我们应该只保留 32 位,因为这就是 .NET GetHashCode() 的用途。 在这种情况下,我什至不确定我们是否应该添加 uint 版本。 如果您在该领域之外使用散列,我认为可以向人们指出我们在 System.Security.Cryptography 中拥有的更通用的散列算法。

@terrajobst有非常不同类型的哈希算法,一个真正的动物园。 事实上,大概 70% 的设计是非加密的。 并且可能有超过一半的设计用于处理 64+ 位(常见目标是 128/256)。 我敢打赌,框架决定使用 32 位(我没有去过那里)是因为当时 x86 仍然是一个巨大的消费者,散列在所有地方都被使用,因此在较小硬件上的性能是最重要的。

同样严格地说,大多数散列函数实际上是在uint域上定义的,而不是在int上定义的,因为移位规则是不同的。 事实上,如果您检查我之前发布的代码, int会因此立即转换为uint (并使用ror/rol优化)。 以防万一,如果我们想要严格,唯一的哈希值应该是uint ,可以将框架在这种情况下返回int视为疏忽。

将其限制为int并不比我们今天拥有的更好。 如果这是我的电话,我会敦促设计团队研究我们如何能够支持 128 和 256 变体以及不同的哈希函数(即使我们会在您的指纹下抛出一个不让我思考的替代方案)。

过度简化导致的问题有时比被迫处理复杂事物时引入的设计问题更糟糕。 将功能简化到如此大的程度,因为开发人员认为not being able to deal with having multiple options很容易导致进入当前 SIMD 状态的路径。 大多数注重性能的开发人员不能使用它,其他人也不会使用它,因为大多数人都没有处理具有如此精细吞吐量目标的性能敏感应用程序。

散列的情况类似,您将使用 32 位的域非常受限制(大多数已被框架本身覆盖),其余的您就不走运了。

image

此外,一旦您必须处理超过 75000 个元素,您就有 50% 的机会发生冲突,这在大多数情况下都是糟糕的(假设您有一个设计良好的哈希函数)。 这就是为什么在运行时结构的边界之外如此使用 64 位和 128 位的原因。

将设计固定在int我们只涵盖了 2000 年没有周一报纸造成的问题(所以现在每个人都自己写他们糟糕的散列),但我们甚至不会推进到艺术。

这是我参与讨论的 2 美分。

@redknightlois ,我想我们了解 int 哈希的局限性。 但我同意@terrajobst :这个功能应该是关于计算哈希的 API,目的是从 Object.GetHashCode 覆盖中返回它们。 我们可能另外有一个用于更现代散列的单独库,但我认为这应该是一个单独的讨论,因为它需要包括决定如何处理 Object.GetHashCode 和所有现有的散列数据结构。

除非您认为以 128 位进行哈希组合然后转换为 int 仍然有益,以便可以从 GetHahsCode 返回结果。

@KrzysztofCwalina我同意这是两种不同的方法。 一个是修复2000年引起的问题; 另一种方法是解决一般的散列问题。 如果我们都同意这是前者的解决方案,那么讨论就结束了。 然而,对于“未来”里程碑的设计讨论,我有一种感觉,它会落空,主要是因为我们在这里要做的事情会影响未来的讨论。 在这里犯错,会产生影响。

@redknightlois ,我会提出以下建议:让我们设计一个 API,就好像我们不必担心未来一样。 然后,让我们讨论我们认为哪些设计选择会给未来的 API 带来问题。 此外,我们可以做的是将 c2000 API 添加到 corfx 并同时尝试在 corfxlab 中试验未来的 API,如果我们想这样做的话,这应该会发现与此类添加相关的任何问题。

@redknightlois

在这里犯错,会产生影响。

我认为,如果将来我们想要支持更高级的场景,那么我们可以使用与HashCode不同的类型来实现。 这里的决定不应该真正影响这些案例。

我创建了一个不同的问题来开始解决这个问题。

@redknightlois :+1:. 顺便说一句,你在我编辑我的评论之前就做出了回应,但我确实尝试了你的想法(上面),让哈希处理任何类型(int、long、decimal 等)并将核心哈希逻辑封装在一个结构中: https://github.com/jamesqo/HashApi (示例用法在这里)。 但是,有两个泛型类型参数最终太复杂了,当我尝试使用 API 时,编译器类型推断最终不起作用。 所以是的,现在将更高级的哈希处理成一个单独的问题是个好主意。

@terrajobst API 似乎已准备就绪,但我还想更改 1 或 2 件事。

  • 我最初不想要静态工厂方法,因为HashCode.Create(x)new HashCode().Combine(x)具有相同的效果。 但是,我改变了主意,因为这意味着 1 个额外的哈希值。 相反,我们为什么不将Create重命名Combine ? 必须为第一个字段输入一个内容而为第二个字段输入另一个内容似乎有点烦人。
  • 我认为我们应该让HashCode实现IEquatable<HashCode>并实现一些相等运算符。 如果您有异议,请随时告诉我。

(希望)最终提案:

public struct HashCode : IEquatable<HashCode>
{
    public static HashCode Combine(int hash);
    public static HashCode Combine<T>(T obj);

    public HashCode Combine(int hash);
    public HashCode Combine<T>(T obj);

    public int Value { get; }

    public static implicit operator int(HashCode hashCode);

    public static bool operator ==(HashCode left, HashCode right);
    public static bool operator !=(HashCode left, HashCode right);

    public override bool Equals(object obj);
    public override bool Equals(HashCode other);
    public override int GetHashCode();
}

// Usage:

public override int GetHashCode()
{
    return HashCode
        .Combine(_field1)
        .Combine(_field2)
        .Combine(_field3)
        .Combine(_field4);
}

@terrajobst说:

有道理。 我添加它是因为它在@Eilon的代码中被引用。 根据经验,我会说字符串非常常见。 另一方面,我不确定是否指定比较。 让我们暂时搁置它。

这实际上非常重要:为字符串创建哈希通常涉及考虑该字符串的用途,这涉及其文化和区分大小写。 StringComparer 本身不是关于比较,而是关于提供特定的 GetHashCode 实现,这些实现是文化/案例感知的。

如果没有这个 API,你需要做一些奇怪的事情,比如:

HashCode.Combine(str1.ToLowerInvariant()).Combine(str2.ToLowerInvariant())

那是充满了分配,遵循不良的文化敏感性模式等。

@Eilon在这种情况下,我希望代码应该显式调用string.GetHashCode(StringComparison comparison) ,这是文化/案例感知并将结果作为int传递给Combine

c# HashCode.Combine(str1.GetHashCode(StringComparer.Ordinal)).Combine(...)

@Eilon ,你可以只使用 StringComparer.InvariantCultureIgnoreCase.GetHashCode。

就分配而言,这些当然更好,但这些调用看起来并不漂亮......我们在整个 ASP.NET 中使用了所有散列需要包含区域性/区分大小写的字符串。

很公平,结合上面所说的所有内容,那么这个形状怎么样:

``` C#
命名空间 System.Collections.Generic
{
公共结构哈希码:IEquatable
{
public static HashCode Combine(int hash);
公共静态哈希码组合(T 对象);
public static HashCode Combine(string text, StringComparison比较);

    public HashCode Combine(int hash);
    public HashCode Combine<T>(T obj);
    public HashCode Combine(string text, StringComparison comparison);

    public int Value { get; }

    public static implicit operator int(HashCode hashCode);

    public static bool operator ==(HashCode left, HashCode right);
    public static bool operator !=(HashCode left, HashCode right);

    public override bool Equals(object obj);
    public override bool Equals(HashCode other);
    public override int GetHashCode();
}

}

// 用法:

公共覆盖 int GetHashCode()
{
返回 HashCode.Combine(_field1)
.Combine(_field2)
.Combine(_field3)
.Combine(_field4);
}
``

装运它! :-)

@terrajobst _Hold on--_ 不能将Combine(string, StringComparison)仅作为扩展方法来实现吗?

public static class HashCodeExtensions
{
    public static HashCode Combine(this HashCode hashCode, string text, StringComparison comparison)
    {
        switch (comparison)
        {
            case StringComparison.Ordinal:
                return HashCode.Combine(StringComparer.Ordinal.GetHashCode(text));
            case StringComparison.OrdinalIgnoreCase:
                ...
        }
    }
}

我非常希望它是一个扩展方法,而不是类型签名的一部分。 但是,如果您或@Elion绝对认为这应该是内置方法,我不会阻止此提议。

编辑:另外System.Numerics可能是一个更好的命名空间,除非我们今天在 Collections.Generic 中有我不知道的与哈希相关的类型。)

LGTM。 我会去扩展。

是的,它可能是一种扩展方法,但它解决了什么问题?

@terrajobst

是的,它可能是一种扩展方法,但它解决了什么问题?

我在 ASP.NET 代码中建议。 如果这对于他们的用例很常见,那很好,但对于其他库/应用程序可能并非如此。 如果后来证明这很常见,我们可以随时重新评估并决定将其添加到单独的提案中。

嗯,无论如何这是核心。 无论如何,一旦定义,它将成为签名的一部分。 废掉评论。 没关系。

使用扩展方法对于以下情况很有用:

  1. 它是一种现有类型,我们希望对其进行扩充,而无需对类型本身进行更新
  2. 解决分层问题
  3. 将超级常用的 API 与很少使用的 API 分开。

我不认为 (1) 或 (2) 在这里适用。 (3) 仅在我们将代码移动到与HashCode不同的程序集或将其移动到不同的命名空间时才有帮助。 我认为字符串很常见,不值得。 事实上,我什至认为它们是如此普遍,以至于将它们视为一流比试图在扩展类型上人为地将它们分开更有意义。

@terrajobst ,要清楚的是,我建议完全放弃string API,让 ASP.NET 编写他们自己的字符串扩展方法。

我认为字符串很常见,不值得。 事实上,我什至认为它们是如此普遍,以至于将它们视为一流比试图在扩展类型上人为地将它们分开更有意义。

是的,但是对于想要获取字符串的非序数哈希码的人来说有多常见,这是现有Combine<T>重载无法处理的唯一情况? (例如,有人在他们的覆盖中调用StringComparer.CurrentCulture.GetHashCode ?)我可能错了,但我没有见过很多。

很抱歉在这方面有所回击; 只是一旦添加了 API,就再也回不去了。

是的,但是对于想要获取字符串的非序数哈希码的人来说,这有多常见

我可能有偏见,但大小写不变性非常流行。 当然,没有多少(如果有的话)关心特定于文化的哈希码,而是忽略大小写的哈希码,我完全可以看到——这似乎是@Eilon所追求的(即StringComparison.OrdinalIgnoreCase )。

很抱歉在这方面有所回击; 只是一旦添加了 API,就再也回不去了。

不开玩笑 😈 同意,但即使 API 没有被使用那么多,它也是有用的并且不会造成任何伤害。

@terrajobst好吧,让我们添加它:+1:最后一个问题:我上面提到了这一点,但是我们可以创建命名空间 Numerics 而不是 Collections.Generic 吗? 如果我们将来像@redknightlois建议的那样添加更多与散列相关的类型,我认为它们在集合中有点用词不当。

我就喜欢。 🍔

我不认为散列在概念上属于集合。 System.Runtime 呢?

我打算建议相同的,甚至 System. 它也不是数字。

@karelz , System.Runtime 可以工作。 @redknightlois系统会很方便,因为您可能已经导入了该命名空间。 不过,不知道这是否合适(同样,如果添加了更多散列类型)。

我们不应该把它放在System.Runtime因为这是用于深奥且非常特殊的情况。 我和@KrzysztofCwalina 谈过,我们都认为这是两者之一:

  • System
  • System.Collections.*

我们都倾向于System

如果我们需要的是为什么要购买System的理由,我可以尝试辩解。 我们构建了HashCode来帮助实现object.GetHashCode() ,这听起来很合适,两者都可以共享命名空间。

@terrajobst我认为System应该是命名空间。 让我们 :shipit:

更新了描述中的 API 规范。

[@redknightlois] 如果我们需要的是为什么要购买System的理由,我可以尝试辩解。 我们构建了HashCode来帮助实现object.GetHashCode() ,这听起来很合适,两者都可以共享命名空间。

这也是@KrzysztofCwalina和我使用的理由。 卖!

@jamesqo

我假设您也想为 PR 提供实现?

@terrajobst是的,当然。 感谢您抽出宝贵时间对此进行审核!

当然是。

甜的。 在那种情况下,我会把它分配给你。 你好吗@karelz?

感谢您抽出宝贵时间对此进行审核!

感谢您抽出时间与我们一起研究 API 形状。 来回走动可能是一个痛苦的过程。 我们非常感谢您的耐心等待!

我期待删除 ASP.NET Core 实现并使用它来代替 😄

public static HashCode Combine(string text, StringComparison比较);
公共 HashCode 组合(字符串文本,StringComparison 比较);

尼特: String上的方法需要StringComparison (例如EqualsCompareStartsWithEndsWith等.) 使用comparisonType作为参数的名称,而不是comparison 。 此处的参数是否也应该命名为comparisonType以保持一致?

@justinvp ,这似乎更像是 String 方法中的命名缺陷; Type是多余的。 我认为我们不应该仅仅为了“遵循先例”而使新 API 中的参数名称更加冗长。

作为另一个数据点, xUnit 也选择使用comparisonType

@justinvp你说服了我。 现在我直觉地思考它,“不区分大小写”或“文化相关”是一种“类型”的比较。 我来改名

我对这个形状没问题,但关于 StringComparison,一个可能的替代方案:

不包括:

``` C#
public static HashCode Combine(string text, StringComparison比较);
公共 HashCode 组合(字符串文本,StringComparison 比较);

Instead, add a method:

``` C#
public class StringComparer
{
    public static StringComparer FromComparison(StringComparison comparison);
    ...
}

然后而不是写:

``` C#
公共覆盖 int GetHashCode()
{
返回 HashCode.Combine(_field1)
.Combine(_field2)
.Combine(_field3)
.Combine(_field4, _comparison);
}

you write:

``` C#
public override int GetHashCode()
{
    return HashCode.Combine(_field1)
                   .Combine(_field2)
                   .Combine(_field3)
                   .Combine(StringComparer.FromComparison(_comparison).GetHashCode(_field4));
}

是的,它有点长,但它解决了同样的问题,而无需 HashCode 上的两个专门方法(我们刚刚将其提升为 System),并且您将获得一个静态辅助方法,可以在其他不相关的情况下使用。 如果您已经有一个 StringComparer(因为我们不是在谈论比较器重载),它也会保持它类似于您使用它的方式:

C# public override int GetHashCode() { return HashCode.Combine(_field1) .Combine(_field2) .Combine(_field3) .Combine(_comparer.GetHashCode(_field4)); }

@stephentoubFromComparison听起来是个好主意。 我实际上在线程中向上提议添加一个string.GetHashCode(StringComparison) api,这使您的示例更加简单(假设非空字符串):

public override int GetHashCode()
{
    return HashCode.Combine(_field1)
                   .Combine(_field2)
                   .Combine(_field3)
                   .Combine(_field4.GetHashCode(_comparison));
}

@Elion说它添加了太多电话。

(编辑:为您的 api 提出了建议。)

我也不喜欢在HashCode为字符串添加 2 个专门的方法。
@Eilon您提到该模式用于 ASP.NET Core 本身。 您认为外部开发人员会使用多少?

@jamesqo感谢您推动设计! 正如@terrajobst所说,我们感谢您的帮助和耐心。 基本的小型 API 有时可能需要一段时间才能迭代:)。

让我们看看最后一条 API 反馈在哪里,然后我们可以继续实施。

是否应该有:

C# public static HashCode Combine<T>(T obj, IEqualityComparer<T> cmp);

?

(如果这已经被驳回,我很抱歉,我在这里想念它)。

@stephentoub说:

写:

c# public override int GetHashCode() { return HashCode.Combine(_field1) .Combine(_field2) .Combine(_field3) .Combine(StringComparer.FromComparison(_comparison).GetHashCode(_field4)); }

是的,它有点长,但它解决了同样的问题,而无需 HashCode 上的两个专门方法(我们刚刚将其提升为 System),并且您将获得一个静态辅助方法,可以在其他不相关的情况下使用。 如果您已经有一个 StringComparer(因为我们不是在谈论比较器重载),它也会保持它类似于您使用它的方式:


嗯,它不仅仅是更长一点,它就像 waaay 超长,并且具有零可发现性。

添加这种方法的阻力是什么? 如果它有用,可以清楚地正确实现,它的作用没有歧义,为什么不添加它?

拥有额外的静态帮助器/转换方法很好 - 虽然我不确定我会使用它 - 但为什么要以牺牲便利方法为代价呢?

为什么要牺牲方便的方法?

因为我不清楚这里真的需要方便的方法。 我知道 ASP.NET 在不同的地方都这样做了。 几个地方? 在这些地方中有多少地方实际上是您拥有的变量 StringComparison 而不是已知值? 在这种情况下,您甚至不需要我提到的助手,可以这样做:

``` C#
.Combine(StringComparer.InvariantCulture.GetHashCode(_field4))

which in no way seems onerous to me or any more undiscoverable than knowing about StringComparison and doing:

``` C#
.Combine(_field4, StringComparison.InvariantCulture);

并且实际上更快,因为我们不必在Combine 内部进行分支来执行开发人员可以编写的完全相同的事情。 额外的代码是不是很不方便,值得为那种情况添加专门的重载? 为什么不重载 StringComparer? 为什么不重载 EqualityComparer? 为什么不使用Func<T, int>重载? 在某些时候,您划清界限并说“这种重载提供的价值不值得”,因为我们添加的一切都是有代价的,无论是维护成本,代码大小的成本,还是任何成本,如果开发人员真的需要这种情况,那么开发人员只需很少的额外代码即可处理较少的特殊情况。 所以我建议画线的正确位置可能是在这些重载之前而不是之后(但正如我在之前的回复开头所说,“我对这个形状没问题”,并建议另一种选择) .

这是我所做的搜索: https :

在大约 100 个匹配项中,即使只是从前几页开始,几乎每个用例都有字符串,并且在某些情况下使用不同类型的字符串比较:

  1. 序数: https :
  2. 序数 + IgnoreCase: https :
  3. 序数: https :
  4. 序数: https :
  5. 序数: https :
  6. 序数: https://github.com/aspnet/Razor/blob/77ed9f22fc8894fbce796bb8a704d6cd03a3b226/src/Microsoft.AspNetCore.Razor.TagHelpers.Testing.Sources/CaseSensitiveTagHelperDescriptorComparer2cs
  7. 序数 + IgnoreCase: https :
  8. 序数: https :
  9. 序数: https :
  10. 序数: https :

(还有其他几十个。)

因此,在 ASP.NET Core 代码库中,这似乎是一种非常常见的模式。 当然,我不能与任何其他系统交谈。

大约 100 个匹配项

您列出的 10 个中的每一个(我没有查看搜索的其余部分)都明确指定了字符串比较,而不是从变量中提取它,所以我们不只是在谈论它们之间的区别,例如:

``` C#
.Combine(Name, StringComparison.OrdinalIgnoreCase)

``` C#
.Combine(StringComparer.OrdinalIgnoreCase.GetHashCode(Name))

? 这不是“waaay 超长”而是更有效率,除非我遗漏了一些东西。

无论如何,正如我所说的,我只是建议我们真正考虑这些重载是否必要。 如果大多数人相信他们是,而且我们不只是考虑我们自己的 ASP.NET 代码库,那很好。

相关的,我们为空输入计划的行为是什么? int==0 呢? 如果我们允许传递 null,我可以开始看到字符串重载的更多好处,因为我相信 StringComparer.GetHashCode 通常会抛出 null 输入,所以如果这真的很常见,如果调用者有特殊情况下的空值。 但这也引出了一个问题,即当提供 null 时行为将是什么。 是否与任何其他值一样将 0 混合到哈希码中? 是否将其视为 nop 而单独留下哈希码?

我认为消除 null 的最佳一般方法是混入零。 对于添加的单个 null 元素,将其作为 nop 会更好,但如果有人按顺序输入,那么将 10 个 null 散列到 20 个不同的散列会更有益。

确实,我的投票是从 ASP.NET Core 代码库的角度来看的,其中具有字符串感知的重载将非常有帮助。 关于行长的事情并不是我真正关心的问题,而是关于可发现性。

如果系统中没有字符串感知重载,我们只需在 ASP.NET Core 中添加一个内部扩展方法并使用它。

如果系统中没有字符串感知重载,我们只需在 ASP.NET Core 中添加一个内部扩展方法并使用它。

我认为目前这将是一个很好的解决方案,直到我们看到更多证据表明通常需要此类 API,并且在 ASP.NET Core 代码库之外。

我不得不说我没有看到删除string重载的价值。 它不会降低任何复杂性,不会使代码更高效,也不会阻止我们改进其他方面,例如提供一种从StringComparison返回StringComparer StringComparison 。 语法糖_确实_很重要,因为 .NET 一直致力于简化常见情况。 我们也想引导开发者做正确的事,掉入成功的坑。

我们需要意识到字符串是特殊的并且非常普遍。 通过添加专门用于它们的重载,我们可以实现两件事:

  1. 我们让@Eilon之类的场景变得更容易。
  2. 我们发现考虑字符串的比较很重要,尤其是大小写。

我们还需要考虑像上面提到的扩展方法@Eilon这样的常见样板助手不是一件好事,而是一件坏事。 这会导致浪费数小时的复制和粘贴辅助方法,并且在未正确完成时可能会导致不必要的代码膨胀和错误。

但是,如果主要关注的是特殊大小写string ,那么这个怎么样:

``` C#
公共结构哈希码:IEquatable
{
公共哈希码组合(T obj, IEqualityComparer比较器);
}

// 用法
返回 HashCode.Combine(_numberField)
.Combine(_stringField, StringComparer.OrdinalIgnoreCase);
``

@terrajobst ,您的妥协很聪明。 我喜欢您不再需要显式调用GetHashCode或使用自定义比较器嵌套一组额外的括号。

(编辑:我想我真的应该把它归功于@JonHanna,因为他在线程前面提到过?😄)

@JonHanna是的,我们还将将空输入散列为 0。

很抱歉在这里打断谈话。 但是,我应该把新类型放在哪里? @mellinoe @ericstj @weshaggard ,您是否建议我为这种类型制作一个新的程序集/包,例如System.HashCode ,还是应该将其添加到现有程序集,例如System.Runtime.Extensions ? 谢谢。

我们最近重构了 .NET Core 中的程序集布局; 我建议把它放在具体的比较器所在的地方,这似乎表明System.Runtime.Extensions

@weshaggard?

@terrajobst关于提案本身,我刚刚发现我们无法同时命名静态和实例重载Combine ,不幸的是。 😢

以下会导致编译器错误,因为实例和静态方法不能具有相同的名称:

using System;
using System.Collections.Generic;

public struct HashCode
{
    public void Combine(int i)
    {
    }

    public static void Combine(int i)
    {
    }
}

现在我们有两个选择:

  • 将静态重载重命名为不同的名称,例如CreateSeed等。
  • 将静态重载移动到另一个静态类:
public static class Hash
{
    public static HashCode Combine(int hash);
}

public struct HashCode
{
    public HashCode Combine(int hash);
}

// Usage:
return Hash.Combine(_field1)
           .Combine(_field2)
           .Combine(_field3);

我倾向于第二种。 不幸的是,我们必须解决这个问题,但是......想法?

将逻辑分成 2 种类型对我来说听起来很奇怪 - 要使用HashCode您必须建立连接并从Hash类开始。

我宁愿添加Create方法(或SeedInit )。
我还要添加无参数重载HashCode.Create().Combine(_field1).Combine(_field2)

@karelz ,我认为我们不应该添加同名的工厂方法。 我们应该只提供无参数构造函数new ,因为它更自然。 此外,e 不能阻止人们写new HashCode().Combine因为它是一个结构体。

public override int GetHashCode()
{
    return new HashCode()
        .Combine(_field1)
        ...
}

这会额外结合 0 和_field1的哈希码,而不是直接从哈希码初始化。 然而,我们使用

提议的 API(更新的规范):

namespace System
{
    public struct HashCode : IEquatable<HashCode>
    {
        public HashCode Combine(int hash);
        public HashCode Combine<T>(T obj);
        public HashCode Combine<T>(T obj, IEqualityComparer<T> comparer);

        public int Value { get; }

        public static implicit operator int(HashCode hashCode);

        public static bool operator ==(HashCode left, HashCode right);
        public static bool operator !=(HashCode left, HashCode right);

        public override bool Equals(object obj);
        public override bool Equals(HashCode other);
        public override int GetHashCode();
    }
}

@redknightlois @JonHanna @stephentoub @Eilon ,您对工厂方法与使用默认构造函数有什么看法吗? 我发现编译器不允许静态Combine重载,因为它与实例方法冲突,所以我们可以选择

HashCode.Create(field1).Combine(field2) // ...

// or, using default constructor

new HashCode().Combine(field1).Combine(field2) // ...

第一个的优点是它更简洁。 第二个的优点是它将具有一致的命名,因此您不必为第一个字段编写不同的内容。

另一种可能性是两种不同的类型,一种带有Combine工厂,一种带有Combine实例(或者第二种作为第一种类型的扩展)。

我不确定我更喜欢哪个 TBH。

@JonHanna ,您将实例重载作为扩展方法的第二个想法听起来很棒。 也就是说,在这种情况下, hc.Combine(obj)会尝试接受静态重载: TryRoslyn

我提议将静态类作为入口点上面的一些评论,这让我想起了... @karelz ,你说

将逻辑分成 2 种类型对我来说听起来很奇怪 - 要使用 HashCode,您必须建立连接并从 Hash 类开始。

人们必须建立什么联系? 难道我们不会先向他们介绍Hash ,然后他们就可以从那里找到HashCode吗? 我认为添加新的静态类不会有问题。

将逻辑分成 2 种类型对我来说听起来很奇怪 - 要使用 HashCode,您必须建立连接并从 Hash 类开始。

我们可以保留顶级类型HashCode并嵌套结构体。 这将允许所需的使用,同时将 API 的“入口点”保持为一种顶级类型,例如:

``` c#
命名空间系统
{
公共静态类 HashCode
{
公共静态 HashCodeValue 组合(整数哈希);
公共静态 HashCodeValue 组合(T 对象);
公共静态 HashCodeValue 组合(T obj, IEqualityComparer比较器);

    public struct HashCodeValue : IEquatable<HashCodeValue>
    {
        public HashCodeValue Combine(int hash);
        public HashCodeValue Combine<T>(T obj);
        public HashCodeValue Combine<T>(T obj, IEqualityComparer<T> comparer);

        public int Value { get; }

        public static implicit operator int(HashCodeValue hashCode);

        public static bool operator ==(HashCodeValue left, HashCodeValue right);
        public static bool operator !=(HashCodeValue left, HashCodeValue right);

        public bool Equals(HashCodeValue other);
        public override bool Equals(object obj);
        public override int GetHashCode();
    }
}

}
``

编辑:虽然,如果我们沿着这条路走下去,嵌套类型可能需要一个比HashCodeValue更好的名称,因为HashCodeValue.Value有点多余,并不是说Value会被非常使用经常。 也许我们甚至不需要一个Value属性-你可以得到Value通过GetHashCode()如果你不想投给int

@justinvp但是,首先有两种不同的类型有什么问题? 例如,该系统似乎适用于LinkedList<T>LinkedListNode<T>

但是,首先有两种不同的类型有什么问题?

两种顶级类型有两个问题:

  1. 哪种类型是 API 的“入口点”? 如果名称是HashHashCode ,您从哪个开始? 从这些名字中看不清楚。 使用LinkedList<T>LinkedListNode<T>很清楚哪个是主要入口点, LinkedList<T> ,哪个是助手。
  2. 污染System命名空间。 它不像 (1) 那样令人担忧,但在我们考虑在System命名空间中公开新功能时要记住一些事情。

嵌套有助于缓解这些问题。

@justinvp

哪种类型是 API 的“入口点”? 如果名称是 Hash 和 HashCode,您从哪一个开始? 从这些名字中看不清楚。 带链表和 LinkedListNode很清楚哪个是主要入口点,LinkedList,这是一个帮手。

好的,足够公平的观点。 如果我们将类型命名为HashHashValue ,而不是嵌套类型呢? 这是否足以表明这两种类型之间的从属关系?

如果我们这样做,那么工厂方法变得更加简洁: Hash.Combine(field1).Combine(field2) 。 另外,使用 struct 类型本身仍然很实用。 例如,有人可能想要收集一个哈希列表,并将其传达给读者,使用List<HashValue>而不是List<int> 。 如果我们使类型嵌套,这可能不会起作用: List<HashCode.HashCodeValue> (即使List<Hash.Value>乍一看也有点令人困惑)。

污染系统命名空间。 它不像 (1) 那样令人担忧,但在我们考虑在 System 命名空间中公开新功能时要记住一些事情。

我同意,但我也认为重要的是我们遵循惯例并且不要牺牲易用性。 例如,我能想到的唯一 BCL API 嵌套类型(不可变集合不算数,它们不是严格的框架的一部分)是List<T>.Enumerator ,我们积极想要隐藏嵌套类型,因为它是供编译器使用的。 在这种情况下,我们不想这样做。

也许我们甚至不需要 Value 属性——如果您不想转换为 int,您可以通过 GetHashCode() 获取 Value。

我很早就想到了这一点。 但是用户如何知道该类型覆盖了GetHashCode ,或者它有一个隐式运算符?

提议的API

public static class Hash
{
    public static HashValue Combine(int hash);
    public static HashValue Combine<T>(T obj);
    public static HashValue Combine<T>(T obj, IEqualityComparer<T> comparer);
}

public struct HashValue : IEquatable<HashValue>
{
    public HashValue Combine(int hash);
    public HashValue Combine<T>(T obj);
    public HashValue Combine<T>(T obj, IEqualityComparer<T> comparer);

    public int Value { get; }

    public static implicit operator int(HashValue hashValue);

    public static bool operator ==(HashValue left, HashValue right);
    public static bool operator !=(HashValue left, HashValue right);

    public override bool Equals(object obj);
    public bool Equals(HashValue other);
    public override int GetHashCode();
}

如果我们将类型命名为 Hash 和 HashValue,而不是嵌套类型呢?

Hash对我来说似乎太笼统了。 我认为我们需要在入口点 API 的名称中包含HashCode ,因为它的预期目的是帮助实现GetHashCode() ,而不是GetHash()

有人可能想要收集哈希列表,并将其传达给读者一个列表用于代替列表. 如果我们使类型嵌套,这可能不起作用:List(即使列表乍一看有点令人困惑)。

这似乎是一个不太可能的用例——不确定我们是否应该为其优化设计。

我能想到的唯一的 BCL API 嵌套类型

TimeZoneInfo.AdjustmentRuleTimeZoneInfo.TransitionTime是 BCL 中有意添加为嵌套类型的示例。

@justinvp

我认为我们需要在入口点 API 的名称中使用 HashCode,因为它的预期目的是帮助实现 GetHashCode(),而不是 GetHash()。

👍 明白了。

我想多了。 嵌套结构体似乎是合理的; 正如你提到的,大多数人永远不会看到实际的类型。 只是一件事:我认为该类型应该被称为Seed ,而不是HashCodeValue 。 其名称的上下文已由包含类隐含。

提议的API

namespace System
{
    public static class HashCode
    {
        public static Seed Combine(int hash);
        public static Seed Combine<T>(T obj);
        public static Seed Combine<T>(T obj, IEqualityComparer<T> comparer);

        public struct Seed : IEquatable<Seed>
        {
            public Seed Combine(int hash);
            public Seed Combine<T>(T obj);
            public Seed Combine<T>(T obj, IEqualityComparer<T> comparer);

            public int Value { get; }

            public static implicit operator int(Seed seed);

            public static bool operator ==(Seed left, Seed right);
            public static bool operator !=(Seed left, Seed right);

            public bool Equals(Seed other);
            public override bool Equals(object obj);
            public override int GetHashCode();
        }
    }
}

@jamesqo使用public readonly int Value有任何异议或实施问题吗? Seed的问题在于它在第一次合并后在技术上不是种子。

也同意@justinvpHash应该保留用于处理散列。 这是引入简化处理HashCode代替。

@redknightlois需要明确的是,我们谈论的是结构名称,而不是属性名称。

        public struct Seed : IEquatable<Seed>
        {
            public Seed Combine(int hash);
            public Seed Combine<T>(T obj);
            public Seed Combine<T>(T obj, IEqualityComparer<T> comparer);

            public int Value { get; }

            public static implicit operator int(Seed seed);

            public static bool operator ==(Seed left, Seed right);
            public static bool operator !=(Seed left, Seed right);

            public bool Equals(Seed other);
            public override bool Equals(object obj);
            public override int GetHashCode();
        }

用法:
c# int hashCode = HashCode.Combine(field1).Combine(name, StringComparison.OrdinalIgnoreCase).Value; int hashCode = (int)HashCode.Combine(field1).Combine(field2);

种子的问题在于它在第一次联合后在技术上不是种子。

它是下一个联合收割机的种子,它产生一个新的种子。

使用 public readonly int Value 有任何异议或实施问题吗?

为什么? int Value { get; }更惯用,可以轻松内联。

它是下一个联合收割机的种子,它产生一个新的种子。

不会是幼苗吧? ;)

@jamesqo根据我的经验,当被复杂的代码属性包围时,往往会生成比字段更糟糕的代码(其中,非内联)。 结构上单个 int 的只读字段也直接在寄存器中转换,最终当 JIT 使用只读进行优化时(在代码生成方面还没有找到它的任何用途); 可以允许进行优化,因为它可以推断它是只读的。 从使用的角度来看,确实和单个getter没有区别。

编辑:此外,它还推动了这些结构确实不可变的想法。

根据我的经验,当被复杂的代码属性包围时,往往会生成比字段更糟糕的代码(其中,非内联)。

如果您发现一个非调试构建,其中自动实现的属性并不总是内联,那么这是一个 JIT 问题,绝对应该修复。

结构上单个 int 的只读字段也直接在寄存器中转换

可以允许进行优化,因为它可以推断它是只读的。

此结构的支持字段将是只读的; API 将是一个访问器。

我认为在这里使用属性不会以任何方式损害性能。

@jamesqo当我找到这些时,我会记住这一点。 对于性能敏感的代码,我只是不再使用属性(此时的肌肉记忆)。

您可能会考虑调用嵌套结构“State”而不是“Seed”?

@ellismg当然,感谢您的建议。 我正在努力为内部结构想出一个好名字。

@karelz我认为这个 API 终于可以使用了; 这一次,我检查以确保一切都编译。 除非有人有任何异议,否则我将开始为此进行实施。

@jamesqo @JonHanna为什么我们需要Combine<T>(T obj)而不是Combine(object o)

为什么我们需要结合(T obj) 而不是 Combine(object o)?

如果实例是结构,后者将分配。

呃,谢谢你的澄清。

我们不喜欢嵌套类型,因为它似乎使设计复杂化。 根本问题是我们不能将静态和非静态命名为相同的名称。 我们有两个选择:摆脱静态或重命名。 我们认为重命名为Create最有意义,因为与使用默认构造函数相比,它创建了相当可读的代码。

除非有一些强烈的反对,这就是我们已经确定的设计:

```C#
命名空间系统
{
公共结构哈希码:IEquatable
{
公共静态哈希代码创建(int hashCode);
公共静态 HashCode 创建(T 对象);
公共静态 HashCode 创建(T obj, IEqualityComparer比较器);

    public HashCode Combine(int hashCode);
    public HashCode Combine<T>(T obj);
    public HashCode Combine<T>(T obj, IEqualityComparer<T> comparer);

    public int Value { get; }

    public static implicit operator int(HashCode hashCode);

    public static bool operator ==(HashCode left, HashCode right);
    public static bool operator !=(HashCode left, HashCode right);

    public bool Equals(HashCode other);
    public override bool Equals(object obj);
    public override int GetHashCode();
}

}
``

让我们等待几天的其他反馈,看看是否有对已批准提案的强烈反馈。 然后我们可以让它“供人抢夺”。

为什么它会使设计复杂化? 我可以理解如果我们实际上必须在代码中使用 HashCode.State(例如定义变量的类型)会有多糟糕,但我们是否期望经常出现这种情况? 大多数情况下,我最终要么直接返回 Value,要么转换为 int 并存储它。

我认为 Create 和 Combine 的组合更糟糕。

请参阅https://github.com/dotnet/corefx/issues/8034#issuecomment -262661653

@terrajobst

我们认为重命名为 Create 最有意义,因为与使用默认构造函数相比,它创建了相当可读的代码。

除非有一些强烈的反对,这就是我们已经确定的设计:

我听到你的声音了,但我在进行实施时最后一刻有了一个想法……我们可以简单地向HashCode添加一个静态的Zero / Empty属性吗,然后有人从那里打电话给Combine吗? 这将使我们不必使用单独的Combine / Create方法。

namespace System
{
    public struct HashCode : IEquatable<HashCode>
    {
        public static HashCode Empty { get; }

        public HashCode Combine(int hashCode);
        public HashCode Combine<T>(T obj);
        public HashCode Combine<T>(T obj, IEqualityComparer<T> comparer);

        public int Value { get; }

        public static implicit operator int(HashCode hashCode);

        public static bool operator ==(HashCode left, HashCode right);
        public static bool operator !=(HashCode left, HashCode right);

        public bool Equals(HashCode other);
        public override bool Equals(object obj);
        public override int GetHashCode();
    }
}

int GetHashCode()
{
    return HashCode.Empty
        .Combine(_1)
        .Combine(_2);
}

还有人认为这是个好主意吗? (我会在此期间提交 PR,如果人们认为是这样,我会在 PR 中更改它。)

@jamesqo ,我喜欢空/零的想法。

我会同意的(在EmptyCreate工厂之间没有强烈的偏好)...... @weshaggard @bartonjs @stephentoub @terrajobst你们怎么看?

我个人认为 Create() 更好; 但我比new HashCode()更喜欢HashCode.Empty new HashCode()

因为它允许一个没有 operator-new 的版本,并且它不排除以后决定我们真的想要 Create 作为引导程序...... ::shrug::。

这就是我的全部阻力(也不是很多)。

FWIW 我会投票给Create而不是Empty / Zero 。 我宁愿从实际价值开始,也不愿把一切都挂在Empty / Zero 。 只是感觉/看起来很奇怪。

它还阻止人们以零播种,这往往是一种糟糕的种子。

我更喜欢 Create 而不是 Empty。 它与我的想法一致:我想创建哈希码并混合其他值。 我也可以使用嵌套方法。

虽然我想说将其命名为 Empty 不是一个好主意(这已经说过了),但经过第三次思考后,我仍然认为这不是一个糟糕的解决方案。 像 Builder 这样的东西怎么样。 虽然仍然可以使用零,但这个词有点不鼓励您立即使用它。

@JonHanna只是为了澄清:你的意思是投票给Create ,对吗?

第四个想法是 With 而不是 Create。

HashCode.With(a).Combine(b)。 结合(c)

基于最新讨论的示例用法(用Create可能替换为替代名称):

```c#
公共覆盖 int GetHashCode() =>
HashCode.Create(_field1).Combine(_field2).Combine(_field3);

We went down the path of this chaining approach, but didn't reconsider earlier proposals when the static & instance `Combine` methods didn't pan out...

Are we sure we don't want something like the existing `Path.Combine` pattern, that was proposed previously, with a handful of generic `Combine` overloads? e.g.:

```c#
public override int GetHashCode() =>
    HashCode.Combine(_field1, _field2, _field3);

@justinvp会导致代码不一致+更多抖动我认为 b/c 是更通用的组合。 如果事实证明这是可取的,我们总是可以在另一个问题中重新讨论这个问题。

就其价值而言,我更喜欢最初提出的版本,至少在使用方面(不确定关于代码大小、抖动等的评论)。 拥有一个额外的结构和 10 多个不同的成员似乎有点矫枉过正,这些成员可以表示为一种具有不同数量重载的方法。 一般来说,我也不喜欢 fluent-style API,所以这可能影响了我的观点。

我不打算提及这一点,因为这有点不寻常,我仍然不确定我对此的感受,但这里有另一个想法,只是为了确保已考虑所有替代方案......

如果我们按照 ASP.NET Core 的可变HashCodeCombiner “builder” 的方式做一些事情,使用类似的Add方法,但还包括对集合初始值设定项语法的支持怎么办?

用法:

```c#
公共覆盖 int GetHashCode() =>
新的哈希码 { _field1, _field2, _field3 };

With a surface area something like:

```c#
namespace System
{
    public struct HashCode : IEquatable<HashCode>, IEnumerable
    {
        public void Add(int hashCode);
        public void Add<T>(T obj);
        public void Add<T>(T obj, IEqualityComparer<T> comparer);

        public int Value { get; }

        public static implicit operator int(HashCode hashCode);

        public static bool operator ==(HashCode left, HashCode right);
        public static bool operator !=(HashCode left, HashCode right);

        public bool Equals(HashCode other);
        public override bool Equals(object obj);
        public override int GetHashCode();

        IEnumerator IEnumerable.GetEnumerator();
    }
}

它必须至少实现IEnumerable以及至少一个Add方法来启用集合初始值设定项语法。 IEnumerable可以显式实现以将其隐藏在智能感知中,并且GetEnumerator可以抛出NotSupportedException或将哈希码值作为可枚举中的单个组合项返回,如果有人碰巧使用它(这种情况很少见)。

@justinvp ,你有一个有趣的想法。 然而,我恭敬地不同意; 我认为HashCode应该保持不变,以避免出现可变结构的问题。 还必须为此实现IEnumerable似乎有点人为/片状; 如果有人在文件中有using System.Linq指令,那么Cast<>OfType<>将显示为扩展方法,如果他们在HashCode旁边放一个点。 我认为我们应该更接近当前的提案。

@jamesqo ,我同意——因此我什至

@MadsTorgersen@jaredpar ,为什么集合初始值设定项需要实现 IEnumerable\@justinvp 上面的第三条评论。

@jamesqo ,我同意最好保持这个不变(而不是 IEnumerable\

@mellinoe我认为这会使简单的情况稍微简单一些,但它也会使除此之外的任何事情变得更加复杂(并且不太清楚应该做什么)。

那包括:

  1. 比你有超载的项目更多
  2. 条件
  3. 循环
  4. 使用比较器

考虑之前发布的有关此主题的 ASP.NET 代码(已更新到当前提案):

```c#
无功哈希码 = 哈希码
.创建(IsMainPage)
.Combine(ViewName, StringComparer.Ordinal)
.Combine(ControllerName, StringComparer.Ordinal)
.Combine(AreaName, StringComparer.Ordinal);

如果(ViewLocationExpanderValues != null)
{
foreach(ViewLocationExpanderValues 中的 var 项目)
{
哈希码 = 哈希码
.Combine(item.Key, StringComparer.Ordinal)
.Combine(item.Value, StringComparer.Ordinal);
}
}

返回哈希码;

How would this look with the original `Hash.CombineHashCodes`? I think it would be:

```c#
var hashCode = Hash.CombineHashCodes(
    IsMainPage,
    StringComparer.Ordinal.GetHashCode(ViewName),
    StringComparer.Ordinal.GetHashCode(ControllerName),
    StringComparer.Ordinal.GetHashCode(AreaName));

if (ViewLocationExpanderValues != null)
{
    foreach (var item in ViewLocationExpanderValues)
    {
        hashCode = Hash.CombineHashCodes(
            hashCode
            StringComparer.Ordinal.GetHashCode(item.Key),
            StringComparer.Ordinal.GetHashCode(item.Value));
    }
}

return hashCode;

即使您忽略为自定义比较器调用GetHashCode() ,我发现必须将hashCode先前值作为第一个参数传递并不简单。

@KrzysztofCwalina根据@ericlippertThe C# Programming Language 1 中注释,这是因为集合初始值设定项(不出所料)是用于集合创建的语法糖,而不是算术(这是名为Add的方法的另一种常见用法

1由于 Google 图书的运作方式,该链接可能不适用于所有人。

@KrzysztofCwalina ,请注意,它需要非通用的IEnumerable ,而不是IEnumerable<T>

@svick ,上面第一个例子中的小问题:对.Combine的第一次调用将是.Create与当前提案。 除非我们使用嵌套方法。

@svic

它还会使除此之外的任何事情变得更加复杂(并且不太清楚应该做什么)

我不知道,第二个例子与第一个总体上几乎没有什么不同,而且它并不是更复杂的 IMO。 使用第二种/原始方法,您只需传入一堆哈希码(我认为第一个参数实际上应该是IsMainPage.GetHashCode() ),所以对我来说似乎很简单。 但在这里我似乎是少数,所以我不会推动原始方法。 我没有强烈的意见; 这两个例子在我看来都足够合理。

@justinvp谢谢,更新了。 (我在第一篇文章中采用了第一个提案,但没有意识到它已经过时了,应该有人更新它。)

@mellinoe问题实际上是第二个会产生微妙的错误。 这是来自我们的一个项目的实际代码。

        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public int GetHashCode(PageFromScratchBuffer obj)
        {
            int v = Hashing.Combine(obj.NumberOfPages, obj.ScratchFileNumber);
            int w = Hashing.Combine(obj.Size.GetHashCode(), obj.PositionInScratchBuffer.GetHashCode());
            return Hashing.Combine(v, w);            
        }

我们忍受它,但我们每天都在处理非常低级的东西; 所以肯定不是一般的开发人员。 然而,这里将 v 与 w 结合起来与 w 与 v 结合起来并不相同……在 v 和 w 结合之间也是如此。 哈希组合不是可交换的,因此一个接一个的链接实际上可以消除 API 级别的一整套错误。

我在第一篇文章中采用了第一个提案,但没有意识到它已经过时了,应该有人更新它。

完毕。
顺便说一句:这个提案很难跟踪,尤其是投票......这么多变化(我认为这很好;-))

@karelz如果我们添加Create API,那么我认为我们仍然可以添加Empty 。 正如@bartonjs所说,它不一定是其中之一。 建议的

namespace System
{
    public struct HashCode : IEquatable<HashCode>
    {
        public HashCode();

        public static HashCode Empty { get; }

        public static HashCode Create(int hashCode);
        public static HashCode Create<T>(T value);
        public static HashCode Create<T>(T value, IEqualityComparer<T> comparer);

        public HashCode Combine(int hashCode);
        public HashCode Combine<T>(T value);
        public HashCode Combine<T>(T value, IEqualityComparer<T> comparer);

        public int Value { get; }

        public static implicit operator int(HashCode hashCode);

        public static bool operator ==(HashCode left, HashCode right);
        public static bool operator !=(HashCode left, HashCode right);

        public bool Equals(HashCode other);
        public override bool Equals(object obj);
        public override int GetHashCode();
        public override string ToString();
    }
}

@乔汉娜

它还阻止人们以零播种,这往往是一种糟糕的种子。

我们选择的散列算法将与今天在HashHelpers使用的相同,其效果为hash(0, x) == xHashCode.Empty.Combine(x)将产生与HashCode.Create(x)完全相同的结果,因此客观上没有区别。

@jamesqo您忘记在上一个提案中包含额外的Zero 。 如果那是遗漏,你能更新吗? 然后我们可以请人们投票支持您的最新提案。 看起来其他替代方案(请参阅我更新的顶部帖子)并没有那么多关注......

@karelz感谢您的发现,已修复。

@KrzysztofCwalina检查您的意思是“添加”是指添加到集合的意义上,而不是其他意义上的。 我不知道我是否喜欢这个限制,但这是我们当时决定的。

public static HashCode Create(int hash);
public HashCode Combine(int hash);

参数是否应该命名为hashCode而不是hash因为传入的值将是可能通过调用GetHashCode()获得的哈希码?

Empty / Zero

如果我们最终保留这个,另一个要考虑的名称是Default

@justinvp

参数是否应该命名为 hashCode 而不是 hash 因为传入的值将是一个可能通过调用 GetHashCode() 获得的哈希码?

我想命名 int 参数hashHashCode参数hashCode 。 然而,再想一想,我确实相信hashCode会更好,因为正如你提到的, hash有点含糊。 我会更新 API。

如果我们最终保留这个,另一个要考虑的名称是 Default。

当我听到Default我认为“当你不知道选择哪个选项时做某事的常规方法”,而不是“结构的默认值”。 例如,像Encoding.Default这样的东西有着完全不同的内涵。

我们选择的散列算法将与今天在 HashHelpers 中使用的相同,其效果是 hash(0, x) == x。 HashCode.Empty.Combine(x) 将产生与 HashCode.Create(x) 完全相同的结果,因此客观上没有区别。

作为一个不太了解内部原理的人,我真的很喜欢HashCode.Create(x).Combine(...)的简单性。 Create很明显,因为很多地方都用到了。

如果Empty / Zero / Default不提供任何算法用法,则 IMO 不应该存在。

PS:非常有趣的线程!! 做得好! 👍

@cwe1ss

如果 Empty / Zero / Default 不提供任何算法用法,则 IMO 不应该存在。

拥有Empty字段确实提供了算法用法。 它代表一个“起始值”,您可以从中组合散列。 例如,如果你想严格使用Create组合一个哈希数组,这是非常痛苦的:

int CombineRange(int[] hashes)
{
    if (hashes.Length == 0)
    {
        return 0;
    }

    var result = HashCode.Create(hashes[0]);

    for (int i = 1; i < hashes.Length; i++)
    {
        result = result.Combine(hashes[i]);
    }

    return result;
}

如果你有Empty ,它会变得更自然:

int CombineRange(int[] hashes)
{
    var result = HashCode.Empty;

    for (int i = 0; i < hashes.Length; i++)
    {
        result = result.Combine(hashes[i]);
    }

    return result;
}

// or

int CombineRange(int[] hashes)
{
    return hashes.Aggregate(HashCode.Empty, (hc, next) => hc.Combine(next));
}

@terrajobst这种类型对我来说与ImmutableArray<T>非常相似。 空数组本身不是很有用,但作为其他操作的“起点”非常有用,这就是我们为它设置Empty属性的原因。 我认为HashCode也有一个是有意义的; 我们保留Create

@jamesqo我注意到你在你的提案https://github.com/dotnet/corefx/issues/8034#issuecomment -262661653 中悄悄地/意外地将 arg 名称obj更改value 。 我将其切换回obj ,IMO 可以更好地捕获您所获得的内容。 在此上下文中,名称value与“int”哈希值本身更相关。
如果需要,我愿意进一步讨论 arg 名称,但让我们有意更改它并跟踪与上次批准的提案的差异。

我已经更新了顶部的提案。 我还针对提案的最后批准版本提出了差异。

我们选择的散列算法将与今天在 HashHelpers 中使用的相同

为什么选择一个应该到处使用的算法是一种很好的算法? 它将对组合的哈希码做出什么假设? 如果它无处不在,它是否会为 DDoS 攻击开辟新的途径? (请注意,过去我们已经被字符串散列烧毁了。)

如果我们按照 ASP.NET Core 的可变 HashCodeCombiner “builder” 做一些事情会怎样

我认为这是正确的使用模式。 良好的通用哈希码组合器通常可以使用比适合哈希码本身的状态更多的状态,但是流畅的模式会崩溃,因为传递更大的结构是一个性能问题。

为什么选择一个应该到处使用的算法是一种很好的算法?

它不应该在任何地方使用。 在https://github.com/dotnet/corefx/issues/8034#issuecomment -260790829 查看我的评论; 它主要针对对哈希不太了解的人。 知道自己在做什么的人可以对其进行评估,看看它是否适合他们的需求。

它将对组合的哈希码做出什么假设? 如果它无处不在,它是否会为 DDoS 攻击开辟新的途径?

当前哈希的一个问题是hash(0, x) == x 。 因此,如果将一系列空值或零值提供给散列,则它将保持为 0。请参阅代码。 这并不是说空值不计算在内,而是初始空值都没有计算在内。 我正在考虑使用更强大(但稍微贵一点)的东西,比如

我认为这是正确的使用模式。 良好的通用哈希码组合器通常可以使用比适合哈希码本身的状态更多的状态,但是流畅的模式会崩溃,因为传递更大的结构是一个性能问题。

我认为不应该有一个具有大结构尺寸的通用组合器来尝试适应每个用例。 相反,我设想了单独的散列代码类型,它们都是整数大小的( FnvHashCode等),并且都有自己的Combine方法。 此外,无论如何,这些“构建器”类型将保留在相同的方法中,而不是传递。

我认为不应该有一个具有大结构尺寸的通用组合器来尝试适应每个用例。

ASP.NET Core 是否能够用这个替换他们自己的哈希码组合器(目前具有 64 位状态)?

我正在设想单独的哈希码类型,它们都是 int 大小的(FnvHashCode 等)

这不会导致组合爆炸吗? 它应该是 API 提案的一部分,以明确这个 API 设计导致什么。

@jkotas我在讨论开始时提出了类似的反对意见。 处理散列函数需要主题知识。 但是我理解并支持解决 2001 年在框架的最根部引入哈希码引起的问题,而不是规定组合哈希的方法。 这种设计旨在解决 99% 的情况(因为散列的统计特性足够好,没有可用甚至不需要的主题知识)。 ASP.Net Core 应该能够将此类组合器包含到非系统程序集上的通用框架中,就像此处提议讨论的那样: https :

我同意在 99% 的情况下使用哈希码组合器是个好主意。 但是,它需要允许更多的内部状态,而不仅仅是 32 位。

BTW: ASP.NET 最初使用 fluent 模式进行 hashcode 组合,但由于它导致容易错过的错误而停止使用: https :

@jkotas关于哈希泛滥安全性。
免责声明:不是专家(您应该咨询一位专家,并且 MS 在这个问题上确实有不少专家)

我一直在环顾四周,虽然在这个问题上没有达成普遍共识,但现在有一种争论越来越受到关注。 哈希码的大小为 32 位,我在一张图表之前发布了一张图表,该图表显示了给定集合大小的冲突概率。 这意味着无论您的算法有多好(例如查看 SipHash),在合理的时间内(大约不到一个小时)生成大量散列并发现冲突都是非常可行的。 这些问题需要在保存散列的数据结构上解决,它们无法在散列函数级别解决。 在不修复底层数据结构的情况下在非加密货币上支付额外的性能以防止散列泛滥并不能解决问题。

编辑:你在我写作的时候发帖。 有鉴于此,64位状态为您带来了什么?

@jkotas我调查了您链接到的问题。 它说:

对 aspnet/Common#40 的反应

https://github.com/aspnet/Common/issues/40 的说明:

发现错误:

public class TagBuilder
{
    private Dictionary<string, string> _attributes;
    private string _tagName;
    private string _innerContent;

    public override int GetHashCode()
    {
        var hash = HashCodeCombiner.Start()
            .Add(_tagName, StringComparer.Ordinal)
            .Add(_innerContent, StringComparer.Ordinal);

        foreach (var kvp in _attributes)
        {
            hash.Add(kvp.Key, StringComparer.Ordinal).Add(kvp.Value, StringComparer.Ordinal);
        }

        return hash.Build();
    }
}

来吧。 这个论点就像说string应该是可变的,因为人们没有意识到Substring返回一个新字符串。 就陷阱而言,可变结构要糟糕得多; 我认为我们应该保持结构不可变。

关于哈希泛滥的安全性。

这有两个方面:按结构正确的设计(健壮的数据结构等); 和缓解现有设计中的问题。 两者都很重要。

@karelz关于参数命名

我注意到你在你的提案 dotnet/corefx#8034(评论)中默默地/意外地将 arg name obj 更改为 value。 我将它切换回 obj,IMO 可以更好地捕捉你得到的东西。 在此上下文中,名称值与“int”哈希值本身更相关。
如果需要,我愿意进一步讨论 arg 名称,但让我们有意更改它并跟踪与上次批准的提案的差异。

我正在考虑在未来的提案中添加 API 以批量组合值。 例如: CombineRange(ReadOnlySpan<T>) 。 如果我们将它命名为obj ,我们将不得不将参数命名为objs ,这听起来很尴尬。 所以我们应该把它命名为item ; 将来,我们可以将 span 参数命名为items 。 更新了提案。

@jkotas同意,但这里的重点是我们没有在组合器级别减轻任何事情......

我们唯一能做的就是有一个随机种子,对于所有状态和目的,我记得在string看到过代码,并且每个构建都是固定的。 (这可能是错误的,因为那是很久以前的事了)。 正确实施随机种子是唯一可以在这里应用的缓解措施。

这是一个挑战,给我你最好的字符串和/或带有固定随机种子的内存哈希函数,我将去构建一个只产生冲突的 32 位哈希码集。 我并不害怕提出这样的挑战,因为它很容易做到,概率论站在我这边。 我什至会去打赌,但我知道我会赢的,所以基本上不再是赌了。

此外……更深入的分析表明,即使缓解是每次运行内置那些“随机种子”的能力,也不需要更复杂的组合器。 因为基本上你从源头上缓解了这个问题。

假设你有M1M2和不同的随机种子rs1rs2 ....
M1将发行h1 = hash('a', rs1)h2=hash('b', rs1)
M2将发行h1' = hash('a', rs2)h2'=hash('b', rs2)
这里的关键是h1h1'将以1/ (int.MaxInt-1)的概率不同(如果hash足够好),对于所有目的来说都是好,因为它会得到。
因此,无论您决定使用什么c(x,y) (如果足够好),都已经考虑到源头内置的缓解措施。

编辑:我找到了代码,您正在使用 Marvin32,它现在在每个域上都发生了变化。 因此,字符串的缓解措施是每次运行使用随机种子。 正如我所说,这足以缓解。

@jkotas

ASP.NET Core 是否能够用这个替换他们自己的哈希码组合器(目前具有 64 位状态)?

绝对地; 它使用相同的散列算法。 我刚刚制作了这个测试应用程序来测量碰撞次数并运行了 10 次。 与使用 64 位没有显着差异。

我正在设想单独的哈希码类型,它们都是 int 大小的(FnvHashCode 等)

这不会导致组合爆炸吗? 它应该是 API 提案的一部分,以明确这个 API 设计导致什么。

@jkotas ,它不会。 此类的设计不会一成不变地为未来的散列 API 设定设计。 那些应该被视为更高级的场景,应该包含在不同的提案中,例如 dotnet/corefx#13757,并且会有不同的设计讨论。 我相信对于正在努力覆盖GetHashCode新手来说,为通用散列算法提供一个简单的 API 更为重要。

我同意在 99% 的情况下使用哈希码组合器是个好主意。 但是,它需要允许更多的内部状态,而不仅仅是 32 位。

我们什么时候需要比 32 位更多的内部状态? 编辑:如果是为了允许人们插入自定义哈希逻辑,我认为(再次)这应该被视为高级场景并在 dotnet/corefx#13757 中讨论。

您正在使用 Marvin32,它现在在每个域上都发生了变化

是的,字符串哈希码随机化缓解在 .NET Core 中默认启用。 由于兼容性原因,在完整的 .NET Framework 中,默认情况下不会为独立应用程序启用它; 它只能通过怪癖启用(例如在高风险环境中)。

我们在 .NET Core 中仍然有非随机散列的代码,但删除它应该没问题。 我不希望我们会再次需要它。 它还会使字符串哈希码计算速度更快,因为不再检查是否再使用非随机化路径。

用于计算随机字符串哈希码的 Marvin32 算法具有 64 位内部状态。 它是由 MS 学科专家挑选的。 我很确定他们有充分的理由使用 64 位内部状态,而且他们没有使用它只是为了让事情变慢。

通用哈希组合器应该不断改进这种缓解措施:它应该使用随机种子和足够强大的哈希码组合算法。 理想情况下,它将使用与随机字符串散列相同的 Marvin32。

用于计算随机字符串哈希码的 Marvin32 算法具有 64 位内部状态。 它是由 MS 学科专家挑选的。 我很确定他们有充分的理由使用 64 位内部状态,而且他们没有使用它只是为了让事情变慢。

@jkotas ,您链接的哈希码组合器不使用 Marvin32。 它使用与非随机化string.GetHashCode相同的朴素 DJBx33x 算法。

通用哈希组合器应该不断改进这种缓解措施:它应该使用随机种子和足够强大的哈希码组合算法。 理想情况下,它将使用与随机字符串散列相同的 Marvin32。

此类型不适用于容易受到散列 DoS 攻击的地方。 这适用于不知道如何添加/异或的人,并将有助于防止诸如https://github.com/dotnet/coreclr/pull/4654 之类的事情

通用哈希组合器应该不断改进这种缓解措施:它应该使用随机种子和足够强大的哈希码组合算法。 理想情况下,它将使用与随机字符串散列相同的 Marvin32。

然后我们应该与 C# 团队交谈,让他们实现一个减轻的ValueTuple哈希算法。 因为该代码也将用于高风险环境。 当然还有Tuple https://github.com/dotnet/coreclr/blob/master/src/mscorlib/src/System/Tuple.cs#L60System.Numerics.HashHelpers (在整个地方)。

现在,在我们决定如何实现它之前,如果支付完全随机的哈希码组合算法的成本是值得的(如果它当然存在),我会调查相同的主题专家,即使它不会改变 API 的方式要么设计(当然,根据提议的 API,您可以使用 512 位状态,并且仍然具有相同的公共 API,当然,如果您愿意支付它的费用)。

这是针对不知道如何添加/异或的人

这正是为什么它的健壮性很重要的原因。 .NET 的关键价值在于它解决了那些不太了解的人的问题。

虽然我们在这里,但不要忘记IntPtr https://github.com/dotnet/coreclr/blob/master/src/mscorlib/src/System/IntPtr.cs#L119
那个特别讨厌,xor 可能是我们那里最糟糕的,因为bad会与dab发生冲突。

实现一个减轻的ValueTuple散列算法

好点子。 我不确定 ValueTuple 是否已经发布,或者现在是否还需要这样做。 打开https://github.com/dotnet/corefx/issues/14046。

让我们不要忘记 IntPtr

这些都是过去的错误……修复它们的门槛要高得多。

@jkotas

这些都是过去的错误……修复它们的门槛要高得多。

我认为 .Net Core 的要点之一是,像这样的“小”变化的门槛应该低得多。 如果有人依赖于IntPtr.GetHashCode (他们真的不应该这样做),他们可以选择不升级他们的 .Net Core 版本。

像这样的“小”变化的门槛应该低得多

是的,它是 - 与完整的 .NET Framework 相比。 但是您仍然需要做一些工作来将更改推送到系统中,并且您可能会发现这不值得痛苦。 最近的例子是Tuple<T>散列算法的变化,因为它破坏了 F#: https :

@jkotas

如果我们将HashCode设为 64 位,您认为不可变设计会在 32 位环境中杀死性能吗? 我同意其他读者的看法,构建器模式似乎要糟糕得多。

杀死性能 - 不。 为语法糖支付的性能损失 - 是的。

为语法糖支付的性能损失 - 是的。

未来是否可以通过 JIT 进行优化?

杀死性能 - 不。
为语法糖支付的性能损失 - 是的。

它不仅仅是语法糖。 如果我们愿意让HashCode成为一个类,那么它就是语法糖。 但是可变值类型是一个错误农场。

引用你之前的话:

这正是为什么它的健壮性很重要的原因。 .NET 的关键价值在于它解决了那些不太了解的人的问题。

我认为对于大多数不太了解的人来说,可变值类型不是一个健壮的 API。

我认为对于大多数不太了解的人来说,可变值类型不是一个健壮的 API。

同意。 思想,我认为不幸的是,可变结构构建器类型就是这种情况。 我用他们所有时间,因为他们是很好紧。 [MustNotCopy]注释有人吗?

MustNotCopy 是结构爱好者的梦想成真。 @jaredpar?

MustNotCopy 就像堆栈一样,但更难使用 😄

我建议不要创建任何类,而是创建扩展方法来组合哈希

static class HashHelpers
{
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static int CombineHash(this int hash1, int hash2);
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static int CombineHash<T>(this int hash, T value);
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static int CombineHash<T>(this int hash, T value, IEqualityComparer<T> comparer);
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static int CombineHash<T>(this int hash, IEnumerable<T> values);
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static int CombineHash<T>(this int hash, IEnumerable<T> values, IEqualityComparer<T> comparer);
}

就这些! 它快速且易于使用。

@AlexRadch我不喜欢污染所有整数的方法列表,而不仅仅是那些意味着散列。

此外,您有继续计算哈希码链的方法,但您如何启动它? 你是否必须做一些不明显的事情,比如从零开始? 即0.CombineHash(this.FirstName).CombineHash(this.LastName)

更新:根据 dotnet/corefx#14046 中的评论,决定将现有的哈希公式保留为ValueTuple

@jamesqo感谢您的帮助。
从上次与@jkotas@VSadov 的讨论推迟采用更昂贵的哈希函数。
如果需要,进行随机化为将来更改散列函数提供了机会。

@jkotas ,我们可以只保留HashCode的当前基于 ROL 5 的哈希,然后将其缩小到 4 个字节吗? 这将消除结构复制的所有问题。 我们可以让HashCode.Empty代表一个随机的哈希值。

@svic
是的,这会污染所有整数的方法,但它可以放置在单独的名称空间中,如果您不使用哈希,则不会包含它,也不会看到。

0.CombineHash(this.FirstName).CombineHash(this.LastName)应该写成this.FirstName.GetHash().CombineHash(this.LastName)

要从种子开始实现,它可以有下一个静态方法

static class HashHelpers
{
    public static int ClassSeed<T>();
}

class SomeClass
{
    int GetHash()
    {
        return HashHelpers.ClassSeed<SomeClass>().CombineHash(value1).CombineHash(value2);
    }
}

所以每个类都有不同的种子来随机化散列。

@jkotas ,那么我们可以为 HashCode 保留当前基于 ROL 5 的哈希值,然后将其缩小到 4 个字节吗?

我认为公共平台哈希码构建助手需要使用 64 位状态才能健壮。 如果它只是 32 位,当它用于散列更多元素、数组或集合时,它很容易产生不好的结果。 您如何编写有关何时使用它和不使用它的好主意的文档? 是的,这是用于混合位的额外指令,但我认为这并不重要。 这些指令执行速度非常快。 我的经验是多做混合比少做要好,因为做太少的混合比做太多的影响严重得多。

此外,我仍然对 API 的提议形状感到担忧。 我认为应该将问题视为哈希码构建,而不是哈希码组合。 将其添加为平台 API 可能还为时过早,我们还应该拭目以待是否会出现更好的模式。 这不会阻止某人使用此 API 发布(源)nuget 包,或使用它作为内部帮助程序的 corefx。

@jkotas具有

看看什么是一个好的散列函数(有些显然像xorhttp : https://research.neustar.biz/2012/02/02/choosing-a-good-hash-function-part-3/

@jamesqo顺便说一句,我刚刚意识到组合器

@jkotas

我认为公共平台哈希码构建助手需要使用 64 位状态才能健壮。 如果它只是 32 位,当它用于散列更多元素、数组或集合时,它很容易产生不好的结果。

什么时候它最终会被浓缩为一个int有关系吗?

@jamesqo并非如此,状态大小仅取决于功能,而不取决于稳健性。 事实上,您实际上可以使您的散列函数变得更糟,因为联合收割机并非旨在以这种方式工作,而且充其量您是在浪费资源,因为您无法从强制中获得随机性。

推论:如果你要特别确定这个函数在统计上是优秀的,否则你几乎可以肯定会让它变得更糟。

这取决于项目之间是否存在相关性。 如果没有相关性,32 位状态和简单的 rotl(甚至 xor)就可以正常工作。 如果有相关性,那就要看情况了。

考虑是否有人使用它从单个字符中构建字符串哈希码。 并不是说有人实际上会为字符串执行此操作,但它说明了问题:

for (int i = 0; i < str.Length; i++)
   hashCodeBuilder.Add(str[i]);

由于现实世界字符串中的字符往往是相关的,因此对于具有 32 位状态和简单 rotl 的字符串,结果会很差。 用于关联的项目多久关联一次,它会产生多糟糕的结果? 很难说,尽管现实生活中的事物往往以意想不到的方式相互关联。

将 next 方法添加到 API 支持哈希随机化会很棒。

namespace System
{
    public struct HashCode : IEquatable<HashCode>
    {
       // add this
       public static HashCode CreateRandomized(Type type);
       // or add this
       public static HashCode CreateRandomized<T>();
    }
}

@jkotas我还没有测试过,所以我相信你做到了。 但这绝对是在说明我们打算使用的功能。 如果您想用速度换取可靠性,那么它根本不够好(没有人可以用它做愚蠢的事情)。 我曾经赞成这样的设计,即这不是一个非加密散列函数,而是一种组合不相关散列代码(它们是随机的)的快速方法。

如果我们的目标是没有人会用它做愚蠢的事情,使用 64 位状态并不能解决任何问题,我们只是在隐藏问题。 仍然可以创建将利用该相关性的输入。 这再次将我们引向了我 18 天前提出的完全相同的论点。 参见: https :

我曾经赞成这样的设计,即这不是一个非加密散列函数,而是一种结合不相关散列码的快速方法

组合不相关哈希码的最快方法是异或...

是的,但我们确实知道上次效果不佳(我想到了 IntPtr)。 旋转和异或(当前)也一样快,如果有人确实放入了某种相关的东西,也不会造成损失。

使用public static HashCode CreateRandomized(Type type);public static HashCode CreateRandomized<T>();方法或两者都添加哈希码随机化。

@jkotas我想我可能为此找到了更好的模式。 如果我们使用 C# 7 ref 返回怎么办? 我们不会每次都返回HashCode ,而是返回一个适合寄存器的ref HashCode

public struct HashCode
{
    private readonly long _value;

    public ref HashCode Combine(int hashCode)
    {
        CombineCore(ref _value, hashCode); // note: modifies the struct in-place
        return ref this;
    }
}

用法和以前一样:

return HashCode.Combine(1)
    .Combine(2).Combine(3);

唯一的缺点是我们又回到了可变结构。 但我认为没有办法同时拥有不可复制和不变性。

ref this还不能工作,但我在 Roslyn 看到一个 PR 来启用它


@AlexRadch我认为将散列与类型更多地结合起来是不明智的,因为获取该类型的散列代码很昂贵。

@jamesqo public static HashCode CreateRandomized<T>();没有得到类型哈希码。 它为这种类型创建随机的 HashCode。

@jamesqoref this还没有工作”。 即使 Roslyn 问题得到修复, ref this也将在一段时间内无法用于 corefx 存储库(我不确定多长时间, @stephentoub可能会设定预期)。

设计讨论并未集中于此。 此外,200 条评论很难理解。
我们计划下周抓住@jkotas,并在下周二的 API 审查中刷新提案。 然后,我们会将提案发回此处以供进一步评论。

一方面:我建议关闭这个问题并在下周我们有“祝福的提案”时创建一个新的问题,以减轻长时间讨论的负担。 如果您认为这是一个坏主意,请告诉我。

@jcouv我还好,它还没有工作,所以只要我们在发布时可以遵循这个设计。 (我也认为可以使用Unsafe暂时解决这个问题。)

@karelz OK :smile: 以后有时间我会关闭这个提案,然后再打开一个新提案。 我同意; 我的浏览器不能很好地处理 200 多条评论。

@karelz我遇到了障碍; 事实证明,有问题的 PR 试图为引用类型启用ref this返回,而不是值类型。 ref this不能安全地从结构体返回; 看看这里为什么。 因此,返回 ref 的妥协将不起作用。

无论如何,我会关闭这个问题。 我在这里打开了另一个问题: https :

尽管我假设 C#vNext 应该能够从值类型扩展方法 post https://github.com/dotnet/roslyn/pull/15650返回 ref "this" ...

@贝纳亚当斯

应该能够从 dotnet/roslyn#15650 后的值类型扩展方法返回引用“this”,尽管我假设 C#vNext ...

正确的。 可以从ref this扩展方法返回this 。 但是,不可能从普通的 struct 实例方法返回this 。 关于为什么会这样,有很多血淋淋的终生细节:(

@redknightlois

如果我们想严格,唯一的哈希值应该是uint ,可以将框架在这种情况下返回int视为疏忽。

CLS 合规性? 无符号整数不符合 CLS。

此页面是否有帮助?
0 / 5 - 0 等级