Struct vs Class 作为HashTable或者Dictionary的Key

本文探讨了在使用Struct和Class作为HashTable或Dictionary的Key时的差异。Struct在成员相同的情况下,GetHashCode值相同,但反射导致效率较低;而Class需实现GetHashCode和Equals方法,推荐实现IEquatable接口,通过质数相乘避免溢出。文章提供了测试用例代码。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

1. 使用Struct.

     1) 使用Struct,如果成员都是简单类型,默认情况下,第一个成员相同的时候,GetHashCode 值相同。如果成员值全部相同,则两个Struct相等。

      2) Struct 因为要使用到反射,GetHashCode 和Equals效率比较低


2. 使用Class

    1)  必须实现GetHashCode,重载Equals方法(不然每次new 一个Class作为查找Key, HashCode都不一样)。最好是实现IEquatable<ConfigCacheKeyClass>接口,以避免装箱。

     2)多个属性个可以分别调用GetHashCode(),得到一个值乘以一个质数,然后相加。避免溢出加上uncheck.


测试用例代码


using System;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using System.Diagnostics;
using System.Collections;
using System.Collections.Generic;

namespace AllTests
{
    [TestClass]
    public class UnitTest1
    {
        /// <summary>
        /// 1. 对于一个值类型,如果其本身存在某个数据可以唯一标明此类型对象,有点儿像数据库中的Key字段,那么用它作为类型的第一个元素
        /// 2. 要保证这个类型的对象通过GetHashCode能自始至终一样,就要防止第一个成员被修改,比较好的做法就是给它加上readonly标示
        /// 3. 不同版本的.netframework对于值类型的GetHashCode的实现可能实现不同
        /// </summary>
        [TestMethod]
        public void TestStructHash()
        {
            var t1 = new KeyStruct("AA1", "1");
            var t2 = new KeyStruct("AA1", "2");
            var t3 = new KeyStruct("AA1", "1");
            var t4 = new KeyStruct("BB1", "1");

            Debug.WriteLine(t1.GetHashCode());
            Debug.WriteLine(t2.GetHashCode());
            Debug.WriteLine(t3.GetHashCode());
            Debug.WriteLine(t4.GetHashCode());

            // 不同的.netframework可能实现不同。有的版本下面test返回True. 当前版本False.
            Debug.WriteLine(t1.TestHashCode());
            Hashtable ht = new Hashtable();

            ht.Add(t1, "v1");
            ht.Add(t2, "v2");
            // exceptpion
            //ht.Add(t3, "test3");

            Assert.AreEqual(t1, t2);
        }


        [TestMethod]
        public void TestClassHash1()
        {
            var t1 = new KeyClass("AA1", "1");
            var t2 = new KeyClass("AA1", "2");
            var t3 = new KeyClass("AA1", "1");

            Debug.WriteLine(t1.GetHashCode());
            Debug.WriteLine(t2.GetHashCode());
            Debug.WriteLine(t3.GetHashCode());

            #region 

            // 不会先调用GetHashCode函数, 再调用Equals函数判断相等。
            // 如果重写了Equals函数,一定要重写GetHashCode函数来达到一致
            // 如果实现了IEquatable<ConfigCacheKeyClass>,下面的Equals会调用bool Equals(ConfigCacheKeyClass other)
            Debug.WriteLine(t1.Equals(t3));

            #endregion

            #region Hashtable的Add, Contains会调用GetHashCode函数和   bool Equals(object obj)

            Hashtable ht = new Hashtable();

            ht.Add(t1, "v1");
            ht.Add(t2, "v2");
            ht.Add(t3, "v3");

            Debug.WriteLine(ht.Contains(t3));

            #endregion

            #region Dictionary的Add, Contains会调用GetHashCode函数和  bool Equals(ConfigCacheKeyClass other)

            IDictionary<KeyClass, string> dic = new Dictionary<KeyClass, string>();

            dic.Add(t1, "v1");
            dic.Add(t2, "v2");
            dic.Add(t3, "v3");

            Debug.WriteLine(dic.Keys.Contains(t3));

            #endregion

            //  Assert.AreEqual(t1, t2);
            Assert.AreSame(t1, t3);
        }
    }


    /// <summary>
    /// new KeyStruct(), 只要p1相同, 它们的HashCode就一样了
    /// </summary>
    public struct KeyStruct
    {
        public readonly string P1;
        public readonly string P2;

        public KeyStruct(string p1, string p2)
        {
            this.P1 = (p1 == null ? null : p1.ToLowerInvariant());
            this.P2 = (p2 == null ? null : p2.ToLowerInvariant());
        }

        public bool TestHashCode()
        {
            return this.GetHashCode() == this.P1.GetHashCode();
        }
    }

    /// <summary>
    /// 如果重写了Equals函数,一定要重写GetHashCode函数来达到一致
    /// </summary>
    public class KeyClass : IEquatable<KeyClass>
    {
        public readonly string P1;
        public readonly string P2;

        public KeyClass(string p1, string p2)
        {
            this.P1 = (p1 == null ? null : p1.ToLowerInvariant());
            this.P2 = (p2 == null ? null : p2.ToLowerInvariant());
        }

        public override bool Equals(object obj)
        {
            // return this.ServerEnvironmentName == ((ConfigCacheKeyClass)obj).ServerEnvironmentName;
            return base.Equals(obj);
        }

        public override int GetHashCode()
        {
            return this.P1.GetHashCode();
            //return base.GetHashCode();
        }

        public bool Equals(KeyClass other)
        {
            return this.P1 == other.P1
                && this.P2 == other.P2;
            // throw new NotImplementedException();
        }
    }
}


<think>好的,我现在需要帮助用户理解CLR键值容器的含义用法。首先,用户提到了之前的引用,但可能这些引用当前问题关联不大,需要仔细分析。用户之前的问题涉及结构体的内存布局,现在转向了CLR的键值容器,可能是指字典或哈希表这样的数据结构。首先,我需要确定CLR键值容器的定义。CLR(公共语言运行时)中的键值容器通常指的是像Dictionary<TKey,TValue>这样的泛型集合,它们以键值对的形式存储数据。用户可能想了解这些容器的实现原理使用方法。 接下来,我需要结合用户提供的引用。引用[2]提到泛型在CLR中的处理方式,特别是值类型实例化时CLR会生成具体化的类型,这可能在性能方面有影响。引用[1]指出使用非泛型容器(如System.Collections中的Hashtable)在处理值类型时会有装箱拆箱的开销,导致性能下降。这可能用户的问题相关,因为键值容器的选择会影响性能。 用户的问题还包括实现,所以需要解释Dictionary<TKey,TValue>的内部结构,比如使用数组链表处理冲突(可能是指拉链法),以及哈希函数的作用。同时,要提到如何正确使用,比如键的哈希码相等性比较的重要性。 此外,用户可能关心与其他容器的对比,比如HashtableConcurrentDictionary,需要说明泛型与非泛型的区别,以及线程安全的选项。还要提到性能优化,比如避免值类型键的装箱,使用合适的初始容量等。 最后,生成相关问题时要覆盖实现细节、性能优化、线程安全选项、与其他容器的对比等。确保引用正确,比如引用[1][2]在适当的地方标注。</think>### CLR键值容器的含义与使用详解 --- #### 一、CLR键值容器的核心定义 CLR键值容器是用于存储**键值对(Key-Value Pair)**的数据结构,其核心特征是**通过键快速定位值**,时间复杂度通常为$O(1)$。在.NET框架中,主要体现为以下类型: 1. **泛型容器**:`Dictionary<TKey, TValue>`(高效类型安全) 2. **非泛型容器**:`Hashtable`(存在装箱拆箱开销)[^1] 3. **线程安全容器**:`ConcurrentDictionary<TKey, TValue>` --- #### 二、核心实现原理 1. **哈希表结构** CLR键值容器底层采用**哈希表+冲突解决链**的混合结构: - 哈希桶数组:通过哈希函数$h(key) \mod n$计算索引 - 冲突处理:拉链法(每个桶挂载链表)或开放寻址法 - 动态扩容:当负载因子超过阈值(默认0.72)时,容量翻倍并重新哈希[^2] 2. **泛型优化机制** ```csharp public class Dictionary<TKey, TKey> { private struct Entry { public int hashCode; // 缓存哈希值 public TKey key; public TValue value; } private int[] buckets; // 哈希桶索引 private Entry[] entries; // 实体数组 } ``` 泛型实现避免了值类型的装箱拆箱操作,显著提升性能[^1][^2]。 3. **哈希函数要求** - `GetHashCode()`需满足: $$ \forall k_1 \neq k_2 \Rightarrow h(k_1) \neq h(k_2) $$ - 默认使用对象内存地址,但可通过重写实现自定义哈希 --- #### 三、典型使用场景与示例 1. **基础使用** ```csharp var dict = new Dictionary<int, string>(); dict.Add(1, "Apple"); // 添加元素 if (dict.TryGetValue(1, out var value)) { Console.WriteLine(value); // 输出"Apple" } ``` 2. **自定义键类型** ```csharp public class Product { public int Id { get; set; } public override int GetHashCode() => Id.GetHashCode(); public override bool Equals(object obj) => obj is Product p && p.Id == Id; } var productDict = new Dictionary<Product, decimal>(); ``` 3. **性能优化实践** - 设置初始容量避免扩容: $$ \text{初始容量} = \lceil \text{预期元素数} / 0.72 \rceil $$ - 使用值类型作为键时优先选择`struct`而非`class` --- #### 四、与其他容器的对比分析 | 特性 | Dictionary<T> | Hashtable | ConcurrentDictionary<T> | |---------------------|---------------------|--------------------|-------------------------| | 线程安全 | 否 | 否 | 是 | | 装箱拆箱 | 无 | 有 | 无 | | 空值支持 | 允许null值 | 不允许null键 | 允许null值 | | 性能(读操作) | $O(1)$ | $O(1)$ | $O(1)$带锁 | | 内存占用 | 较低 | 较高 | 最高 | --- #### 五、高级应用场景 1. **配置信息存储** ```csharp var config = new Dictionary<string, object> { ["Timeout"] = 5000, ["MaxConnections"] = 100 }; ``` 2. **缓存系统实现** ```csharp public class LruCache<TKey, TValue> { private readonly Dictionary<TKey, LinkedListNode<CacheItem>> _dict; private readonly LinkedList<CacheItem> _list = new(); private readonly int _capacity; public void Add(TKey key, TValue value) { if (_dict.Count >= _capacity) { var last = _list.Last; _dict.Remove(last.Value.Key); _list.RemoveLast(); } var node = new LinkedListNode<CacheItem>(new CacheItem(key, value)); _dict.Add(key, node); _list.AddFirst(node); } } ``` 3. **依赖属性系统(WPF)** WPF的依赖属性系统本质上是通过`Dictionary<DependencyObject, object>`存储属性值[^3],结合高效的哈希算法实现属性继承变更通知。 --- ### 内存分配优化公式 假设字典包含$n$个元素,最佳初始容量计算为: $$ \text{初始容量} = \left\lceil \frac{n}{0.72} \right\rceil $$ 例如当$n=1000$时: $$ \text{初始容量} = \left\lceil \frac{1000}{0.72} \right\rceil = 1389 $$ ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值