3

我最近用几种不同的方式问过这个问题,但没有得到一个答案来告诉我<T,U>当我持有对发生变化的事物的引用时需要如何处理字典T.GetHashCode()。对于这个问题,“状态”是指在检查时也会检查的属性和字段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();
            }
        }
    }

}

基于Jon Skeet 的这个回答(对我之前的问题)

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

.

“基本上你已经被塞满了。你不会(或至少可能不会)再次在字典中找到那个键。你应该尽可能小心地避免这种情况。就我个人而言,我通常会发现那些是字典键的良好候选者,也是不变性的良好候选者。” - JS

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

4

2 回答 2

2

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

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

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

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

testDictionary.Add(keyValue, "some data");
// Do whatever...

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

但总的来说,仅使用不可变数据结构作为字典键的风险要小得多

另请注意,目前您的Equals方法依赖于所涉及的两个列表的引用相等 - 这真的是您想要的吗?此外,您没有覆盖GetHashCodein TrustedEntity,因此即使您确实使用相同的列表创建了一个新TrustedEntity列表,它也不会给您想要的结果。基本上,不清楚您想要哪种相等操作 - 您需要自己澄清这一点,然后理想地创建所涉及数据的不可变表示。

于 2012-10-17T22:31:01.967 回答
1

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

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

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

于 2013-06-16T02:51:23.573 回答