【问题标题】:Should GetHashCode() return value be based on original object's state or the modified object's state?GetHashCode() 返回值应该基于原始对象的状态还是修改后的对象的状态?
【发布时间】:2012-10-08 06:39:04
【问题描述】:

我最近以几种不同的方式问过这个问题,但没有得到一个答案来告诉我当我持有对更改 T.GetHashCode() 的某些内容的引用时,需要如何处理 <T,U> 的字典。对于这个问题,“状态”指的是在检查Equals() 时也会检查的属性和字段。假设包括所有公共、内部和受保护的成员。

鉴于我有一个 C# 对象

  • 覆盖 GetHashCode 和 Equals

  • 这个对象作为Key值保存到Dictionary中(注意我的理解是Dictionary此时会读取GetHashCode值)

  • 我通过 Key 搜索对象并修改一个值。 (修改这个值会修改我的自定义 equals 函数和可能的 gethashcode)

我的问题是,GetHashCode 应该反映什么?这个函数的返回应该反映对象的原始状态还是修改后的状态?

示例代码

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace TrustMap
{
    class Program
    {
       static  Dictionary<Model.TrustedEntityReference, string> testDictionary = new Dictionary<Model.TrustedEntityReference, string>();

        static void Main(string[] args)
        {
            Model.TrustedEntity te = new Model.TrustedEntity();

            te.BackTrustLink = null;
            te.ForwardTrustLink = null;
            te.EntryName = "test1";

            var keyValue =  new Model.TrustedEntityReference()
            {
                HierarchyDepth = 1,
               trustedEntity = te 
            };

            testDictionary.Add(keyValue, "some data");

            // Now that I have a reference to the Key outside the object
            te.EntryName = "modified data";

            // Question: how should TE respond to the change, considering that it's a part of a dictionary now?
            //           If this is a implementation error, how should I track of objects that are stored as Keys that shouldn't be modified like I did in the previous line?

        }
    }

}

namespace Model
{
    public class TrustedEntity
    {
        public TrustedEntity()
        {
            this.BackTrustLink = new List<TrustedEntityReference>();
            this.ForwardTrustLink = new List<TrustedEntityReference>();
        }

        public List<TrustedEntityReference> BackTrustLink { get; set; }

        public string EntryName { get; set; }

        public List<TrustedEntityReference> ForwardTrustLink { get; set; }

    }

    public class TrustedEntityReference 
    {
        public int HierarchyDepth { get; set; }
        public TrustedEntity trustedEntity {get; set; }

        public override bool Equals(object obj)
        {
            if (obj.GetType() != trustedEntity.GetType())
                return false;

            TrustedEntity typedObj = (TrustedEntity)obj;

            if (typedObj.BackTrustLink != null)
            { 
                if (trustedEntity.BackTrustLink != typedObj.BackTrustLink)
                    return false;
            }

            if (typedObj.ForwardTrustLink != null)
            {
                if (trustedEntity.ForwardTrustLink != typedObj.ForwardTrustLink)
                    return false;
            }

            if (trustedEntity.EntryName != typedObj.EntryName)
                return false;

            return true;
        }

        /// <summary>
        /// If the hash-code for two items does not match, they may never be considered equal
        /// Therefore equals may never get called.
        /// </summary>
        /// <returns></returns>
        public override int GetHashCode()
        {

            // if two things are equal (Equals(...) == true) then they must return the same value for GetHashCode()
            // if the GetHashCode() is equal, it is not necessary for them to be the same; this is a collision, and Equals will be called to see if it is a real equality or not.
           // return base.GetHashCode();
            return StackOverflow.System.HashHelper.GetHashCode<int, TrustedEntity>(this.HierarchyDepth, this.trustedEntity);
        }
    }


}

namespace StackOverflow.System
{
    /// <summary>
    /// Source https://stackoverflow.com/a/2575444/328397
    /// 
    /// Also it has extension method to provide a fluent interface, so you can use it like this:
///public override int GetHashCode()
///{
///    return HashHelper.GetHashCode(Manufacturer, PartN, Quantity);
///}
///or like this:

///public override int GetHashCode()
///{
///    return 0.CombineHashCode(Manufacturer)
///        .CombineHashCode(PartN)
///        .CombineHashCode(Quantity);
///}
    /// </summary>
    public static class HashHelper
    {
        public static int GetHashCode<T1, T2>(T1 arg1, T2 arg2)
        {
            unchecked
            {
                return 31 * arg1.GetHashCode() + arg2.GetHashCode();
            }
        }

        public static int GetHashCode<T1, T2, T3>(T1 arg1, T2 arg2, T3 arg3)
        {
            unchecked
            {
                int hash = arg1.GetHashCode();
                hash = 31 * hash + arg2.GetHashCode();
                return 31 * hash + arg3.GetHashCode();
            }
        }

        public static int GetHashCode<T1, T2, T3, T4>(T1 arg1, T2 arg2, T3 arg3,
            T4 arg4)
        {
            unchecked
            {
                int hash = arg1.GetHashCode();
                hash = 31 * hash + arg2.GetHashCode();
                hash = 31 * hash + arg3.GetHashCode();
                return 31 * hash + arg4.GetHashCode();
            }
        }

        public static int GetHashCode<T>(T[] list)
        {
            unchecked
            {
                int hash = 0;
                foreach (var item in list)
                {
                    hash = 31 * hash + item.GetHashCode();
                }
                return hash;
            }
        }

        public static int GetHashCode<T>(IEnumerable<T> list)
        {
            unchecked
            {
                int hash = 0;
                foreach (var item in list)
                {
                    hash = 31 * hash + item.GetHashCode();
                }
                return hash;
            }
        }

        /// <summary>
        /// Gets a hashcode for a collection for that the order of items 
        /// does not matter.
        /// So {1, 2, 3} and {3, 2, 1} will get same hash code.
        /// </summary>
        public static int GetHashCodeForOrderNoMatterCollection<T>(
            IEnumerable<T> list)
        {
            unchecked
            {
                int hash = 0;
                int count = 0;
                foreach (var item in list)
                {
                    hash += item.GetHashCode();
                    count++;
                }
                return 31 * hash + count.GetHashCode();
            }
        }

        /// <summary>
        /// Alternative way to get a hashcode is to use a fluent 
        /// interface like this:<br />
        /// return 0.CombineHashCode(field1).CombineHashCode(field2).
        ///     CombineHashCode(field3);
        /// </summary>
        public static int CombineHashCode<T>(this int hashCode, T arg)
        {
            unchecked
            {
                return 31 * hashCode + arg.GetHashCode();
            }
        }
    }

}

基于this answer from Jon Skeet(我之前的问题)

“如果我更改了最终更改键值的属性,我该怎么办?” - 我

.

“基本上你已经吃饱了。你不会(或至少可能不会) 能够在您的字典中再次找到该键。你应该避免这种情况 尽可能小心。就个人而言,我通常会发现 适合作为字典键候选的类好 不变性的候选者。” - J.S.

这是否意味着我需要从字典中删除对象并重新添加它?这是正确/最好的方法吗?

【问题讨论】:

  • 您的问题非常不清楚,因为您谈论键和值就好像它们是同一件事一样。没有像 Dictionary&lt;T&gt; 这样的东西 - 有 Dictionary&lt;TKey, TValue&gt;,如果您修改与键关联的值,那么 不会 修改键本身......一个例子会让你的问题更容易理解。
  • @JonSkeet 我试图回答所有这些请求,如果我还有其他需要做的,请告诉我
  • 基本上,您的实体不应实现相等成员。它们是实体,而不是值。不要将它们用作值。
  • @usr 感谢您的提示!
  • 我建议,如果可能的话,您应该设计对象,以便对于任何对象 XY,如果曾经调用过 X.Equals(Y),它将始终返回与第一个相同的值时间;同样适用于X.GetHashCode()。如果您想使用可变类型作为字典键,请将其包装在一个构造该类型实例的类中,并且永远不要将该实例暴露给任何可能改变它的东西(拥有基类型和包装类可能会有所帮助实现一个只读接口)。

标签: c# generics dictionary gethashcode iequatable


【解决方案1】:

GetHashCode() 是否应该改变的问题有点牵强附会。我建议六个公理:

  1. 应始终观察到每个对象都与自身相等。
  2. 如果曾经观察到一个对象与另一个对象相等,则两个对象应该永远更多地报告自己彼此相等。
  3. 如果曾经观察到一个对象与另一个对象不相等,则两个对象都应该永远报告自己不等于另一个对象。
  4. 如果曾经观察到一个对象与另一个对象相等,并且观察到其中一个对象等于第三个对象,则两者都应该永远报告自己等于第三个对象。
  5. 如果曾经观察到一个对象与另一个对象相等,并且观察到其中一个对象不等于第三个对象,则两者都应该永远报告自己不等于第三个对象。
  6. 观察对象的哈希码表示观察到它与曾经返回不同哈希码的每个对象都不相等。

对象的哈希码不变的要求不是公理之一,而是源自点#1和#6;对一个对象的哈希码的观察与之前的观察不同将构成一个观察,即该对象不等于自身。

【讨论】:

    【解决方案2】:

    好的,澄清一下:您正在修改键/值对的 key 部分。

    既然问题已经清楚了,答案就比较简单了:

    这是否意味着我需要从字典中删除对象并重新添加?

    是的。 但是 - 你必须在修改它之前删除它。所以你会写:

    testDictionary.Add(keyValue, "some data");
    // Do whatever...
    
    testDictionary.Remove(keyValue);
    te.EntryName = "modified data";
    testDictionary.Add(keyValue, "some data"); // Or a different value...
    

    不过,一般而言,仅使用不可变数据结构作为字典键的风险会更小。

    还请注意,目前您的 Equals 方法依赖于所涉及的两个列表的 reference 相等性 - 这真的是您想要的吗?此外,您不会在TrustedEntity 中覆盖GetHashCode,因此即使您确实创建了具有相同列表的新TrustedEntity,它也不会给您想要的结果。基本上,不清楚你想要什么样的相等操作 - 你需要自己澄清一下,然后理想地创建所涉及数据的不可变表示。

    【讨论】:

      猜你喜欢
      • 2013-08-06
      • 2010-10-07
      • 2020-10-13
      • 1970-01-01
      • 2021-03-15
      • 2023-01-09
      • 1970-01-01
      • 2017-01-25
      • 1970-01-01
      相关资源
      最近更新 更多