【问题标题】:How to generate a unique hash code for an object, based on its contents?如何根据其内容为对象生成唯一的哈希码?
【发布时间】:2011-07-30 23:38:07
【问题描述】:

我需要为一个对象生成一个唯一的哈希码,基于它的内容,例如。 DateTime(2011,06,04) 应该等于 DateTime(2011,06,04)。

  • 我不能使用 .GetHashCode(),因为它可能会为具有不同内容的对象生成相同的哈希码。
  • 我无法使用 ObjectIDGenerator 中的 .GetID,因为它会为具有相同内容的对象生成不同的哈希码。
  • 如果对象包含其他子对象,则需要递归检查这些。
  • 它需要处理集合。

我需要写这个的原因是什么?我正在使用 PostSharp 编写一个缓存层。

更新

我想我可能一直在问错误的问题。正如 Jon Skeet 指出的那样,为了安全起见,我需要缓存键中的唯一组合与对象中潜在数据的组合一样多。因此,最好的解决方案可能是构建一个长字符串,使用反射对对象的公共属性进行编码。对象不会太大,因此非常快速高效:

  • 构造缓存键的效率很高(只需将对象的公共属性转换为大字符串)。
  • 它可以有效地检查缓存命中(比较两个字符串)。

【问题讨论】:

  • 当您说“内容”时,您到底是什么意思?公共财产?还有什么?
  • 您是说您希望一个值保证是唯一的,并且如果对象或其聚合中的任何内容发生更改,则该值会发生更改。您是否意识到这意味着您想将可能无限量的信息压缩到一个有限长度的数据结构中?
  • 我想要一个基于对象内容的 GUID。我不介意每 10 万亿年左右是否偶尔会出现重复。
  • @Gravitas:我的“无限”评论意味着您很难将这样的 IEnumerable 合并到哈希中:while(true) yield return 1;
  • 将对象序列化为字符串,然后获取字符串值的MD5。唯一的问题是 [Serializable] 属性。

标签: c# .net visual-studio-2010 .net-4.0 hash


【解决方案1】:

来自评论:

我想要一个基于对象内容的 GUID。我不介意每 10 万亿年左右是否偶尔会有重复

这似乎是一个不寻常的要求,但既然这是您的要求,让我们算一下吧。

假设您每年制造 10 亿个独特的物体——每秒 30 个——持续 10 万亿年。这就是您正在创建的 1049 个独特的对象。计算数学很容易。 当哈希的位大小小于 384 时,该时间内至少发生一次哈希冲突的概率高于十分之一18

因此,您至少需要一个 384 位的哈希码才能获得所需的唯一性级别。这是一个方便的大小,为 12 个 int32。如果您要每秒制作超过 30 个对象,或者希望概率小于 1018 中的一个,则需要更多位。

你们为什么有这么严格的要求?

如果我有您提出的要求,我会这样做。第一个问题是将每一个可能的数据转换成一个自描述的比特序列。如果您已经有序列化格式,请使用它。如果没有,请发明一种可以序列化您对散列感兴趣的所有可能对象的方法。

然后,为了散列对象,将其序列化为字节数组,然后通过 SHA-384 或 SHA-512 散列算法运行字节数组。这将产生一个专业的加密级 384 或 512 位哈希,即使面对试图强制碰撞的攻击者,它也被认为是独一无二的。这么多位应该足以确保在您的 10 万亿年时间范围内发生碰撞的可能性很低。

【讨论】:

    【解决方案2】:

    如果您需要创建一个 唯一 哈希码,那么您基本上是在谈论一个数字,它可以代表您的类型可以拥有的尽可能多的状态。对于DateTime,我相信,比意味着采用 Ticks 值和 DateTimeKind

    您可以假设Ticks 属性的前两位为零,并使用它们来存储类型。这意味着据我所知,您在 7307 年之前都还可以:

    private static ulong Hash(DateTime when)
    {
        ulong kind = (ulong) (int) when.Kind;
        return (kind << 62) | (ulong) when.Ticks;
    }
    

    【讨论】:

    • 哇,答案非常快。但是,我需要任何对象的唯一值,而不仅仅是 DateTime。而且,进一步考虑,我不介意奇数对象是否返回相同的哈希码,因为我不介意偶尔的缓存未命中。
    • @Gravitas:您尚未指定哈希码的大小。我已经给出了一个 DateTime 的样本,它是独一无二的,并且适用于相当广泛的值......但它给出了一个 64 位的值。如果您需要 32 位值,我们将需要采用不同的方法。
    • 题外话,但值得一提的是,您可以使用内置的ToBinaryFromBinary 方法在DateTimelong 之间进行序列化/反序列化(封装KindTicks)。
    • 如何获得 32 位值?
    【解决方案3】:

    您在这里不是在谈论哈希码,您需要一个数字表示您的状态 - 为了使其唯一,它可能必须非常大,具体取决于您的对象结构。

    我需要写这个的原因是什么?我是 使用编写缓存层 PostSharp。

    为什么不改用常规哈希码,通过实际比较对象来处理冲突?这似乎是最合理的方法。

    【讨论】:

    • 打败我。 :) 一些超级哈希码无法解决问题。使用更普通的哈希码,当它找到匹配项时,使用 Equals() 来检查它们是否真的相同。
    • 你说得对:我可能会使用 .GetHashCode,因为我并不介意偶尔会出现缓存缺失。
    • +1:这当然是解决方案,也是 .NET 基于哈希的容器的作用。
    • @Gravitas:GetHashCode 的定义确保您永远不会遇到错误的否定问题。这是您必须注意的错误阳性
    • @Jon - 是的,你是对的:我真的不希望为一组不同的参数返回不正确的答案。回到绘图板 - 我真的需要基于对象内容的 GUID。
    【解决方案4】:

    我不能使用 .GetHashCode(),因为它可能会为具有不同内容的对象生成相同的哈希码。

    哈希码发生冲突是很正常的。如果您的哈希码具有固定长度(在标准 .NET 哈希码的情况下为 32 位),那么您肯定会与范围大于此范围的任何值发生冲突(例如,长 64 位;n*64 n 个长数组的位等)。

    事实上,对于任何长度为 N 的哈希码,超过 N 个元素的集合总是会发生冲突。

    您所要求的在一般情况下是不可行的。

    【讨论】:

    • 你能解释一下为什么这是不可行的吗?构成 Web 安全模型基础的整个公钥基础设施都取决于这个问题是否有可行的解决方案;既然有这样的基础设施,那就证明存在可行的低成本解决方案。 32 位哈希码发生冲突是很正常的(正如您正确指出的那样),但 128 位哈希码发生意外冲突的情况极为罕见。显然有超过 2^128 个可能的字符串这一事实是无关紧要的;与散列的文档数量相比,这是一个很大的空间。
    • 另外,我认为你的数学有点偏离。我想你的意思是说“事实上,对于任何有限长度为 n 位的哈希码,超过 2^n 个元素的集合都会发生冲突”。正确的? (更重要的是,当元素超过 2^(n/2) 个时,发生碰撞的概率会变得非常大)
    【解决方案5】:

    对 BrokenGlass 答案的补充,我已投票赞成并认为是正确的:

    使用GetHashCode/Equals 方法意味着如果两个对象散列到相同的值,您将依靠它们的Equals 实现来告诉您它们是否相等。

    除非这些对象覆盖Equals(这实际上意味着它们实现了IEquatable&lt;T&gt;,其中T 是它们的类型),Equals 的默认实现将进行参考比较。这反过来意味着您的缓存会错误地为业务意义上“相等”但已独立构建的对象产生未命中

    仔细考虑缓存的使用模型,因为如果您最终将它用于不是 IEquatable 的类,并且您希望检查非引用相等对象的方式为了平等,缓存将变成完全无用

    【讨论】:

      【解决方案6】:

      我们有完全相同的要求,这是我想出的功能。这对于我们需要缓存的对象类型非常有效

      public static string CreateCacheKey(this object obj, string propName = null)
      {
          var sb = new StringBuilder();
          if (obj.GetType().IsValueType || obj is string)
              sb.AppendFormat("{0}_{1}|", propName, obj);
          else
              foreach (var prop in obj.GetType().GetProperties())
              {
                  if (typeof(IEnumerable<object>).IsAssignableFrom(prop.PropertyType))
                  {
                      var get = prop.GetGetMethod();
                      if (!get.IsStatic && get.GetParameters().Length == 0)
                      {
                          var collection = (IEnumerable<object>)get.Invoke(obj, null);
                          if (collection != null)
                              foreach (var o in collection)
                                  sb.Append(o.CreateCacheKey(prop.Name));
                      }
                  }
                  else
                      sb.AppendFormat("{0}{1}_{2}|", propName, prop.Name, prop.GetValue(obj, null));
      
              }
          return sb.ToString();
      }
      

      例如,如果我们有这样的事情

      var bar = new Bar()
      {
          PropString = "test string",
          PropInt = 9,
          PropBool = true,
          PropListString = new List<string>() {"list string 1", "list string 2"},
          PropListFoo =
              new List<Foo>()
                  {new Foo() {PropString = "foo 1 string"}, new Foo() {PropString = "foo 2 string"}},
          PropListTuple =
              new List<Tuple<string, int>>()
                  {
                      new Tuple<string, int>("tuple 1 string", 1), new Tuple<string, int>("tuple 2 string", 2)
                  }
      };
      
      var cacheKey = bar.CreateCacheKey();
      

      上面方法生成的缓存键是

      PropString_test 字符串|PropInt_9|PropBool_True|PropListString_list 字符串 1|PropListString_list 字符串 2|PropListFooPropString_foo 1 字符串|PropListFooPropString_foo 2 字符串|PropListTupleItem1_tuple 1 字符串|PropListTupleItem2_1|PropListTupleItem1_tuple 2 字符串|PropListTupleItem2_2|

      【讨论】:

      • 我得到了 List 中项目的 TargetParameterCountException(从 AppensFormat 行抛出"{0}{1}_{2}|)
      【解决方案7】:

      您可以从序列化为 json 的对象计算 ex md5 总和(或类似的东西)。 如果您只希望某些属性重要,您可以在途中创建匿名对象:

       public static string GetChecksum(this YourClass obj)
          {
              var copy = new
              {
                 obj.Prop1,
                 obj.Prop2
              };
              var json = JsonConvert.SerializeObject(ob);
      
              return json.CalculateMD5Hash();
          }
      

      我用它来检查是否有人弄乱了我存储基于许可证数据的数据库。您还可以在 json 变量中附加一些种子以使事情复杂化

      【讨论】:

      • 考虑到这既是一个老问题,也是一个有多个好的答案的问题,并且 OP 澄清了要求是什么,那么你的答案就会受到时间的考验。您的答案只适用于最多 128 位或 16 字节的对象,这几乎不是一般的解决方案。代替问题底部的说明,json 字符串比它的 MD5 校验和更合适。
      • 我的意思是,它完全符合 OP 的意思:获取对象的唯一哈希码,当属性更改时更改,并且对于具有相同属性值的两个不同实例是相同的。 MD5 sum 仅适用于恒定长度,即使对于集合也是如此。你能详细说明你的json本身会更好吗?编辑:好的,我明白了 :) 但仍然,来自 json 的更长哈希可能是一种有效的方法
      【解决方案8】:

      这种扩展方法是否适合您的目的?如果对象是值类型,它只返回其哈希码。否则,它递归地获取每个属性的值并将它们组合成一个哈希。

      using System.Reflection;
      
      public static class HashCode
      {
          public static ulong CreateHashCode(this object obj)
          {
              ulong hash = 0;
              Type objType = obj.GetType();
      
              if (objType.IsValueType || obj is string)
              {
                  unchecked
                  {
                      hash = (uint)obj.GetHashCode() * 397;
                  }
      
                  return hash;
              }
      
              unchecked
              {
                  foreach (PropertyInfo property in obj.GetType().GetProperties())
                  {
                      object value = property.GetValue(obj, null);
                      hash ^= value.CreateHashCode();
                  }
              }
      
              return hash;
          }
      }
      

      【讨论】:

      • 这显然不能满足 OP 的第一个要求。
      • public class HaHa { public HaHa Recursive { get{ return this;} } }
      • @smartcaveman:好点。很容易解决类具有返回自身的属性的情况。但是,如果一对类具有相互引用的属性,这并不容易。
      【解决方案9】:

      通用扩展方法

      public static class GenericExtensions
      {
          public static int GetDeepHashCode<T>(this T obj)
          {
              if (obj == null)
                  return 0;
      
              if (typeof(T).IsValueType)
                  return obj.GetHashCode();
      
              var result = 0;
      
              if (typeof(T) is IEnumerable)
              {
                  var enumerable = obj as IEnumerable<T>;
      
                  using (var enumerator = enumerable.GetEnumerator())
                  {
                      var i = 1;
      
                      while (true)
                      {
                          bool moveNextA = enumerator.MoveNext();
      
                          if (!moveNextA)
                              break;
      
                          var current = enumerator.Current;
      
                          result += current.GetDeepHashCode() * i;
      
                          i++;
                      }
      
                      return result;
                  }
              }
      
              foreach (var property in obj.GetType().GetProperties())
              {
                  var value = property.GetValue(obj);
      
                  result += value.GetDeepHashCode();
              }
      
              return result;
          }
      }
      

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2019-11-30
        • 1970-01-01
        • 2019-06-17
        • 1970-01-01
        • 2014-12-31
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多