C#怎么使用Dictionary字典 C#如何用Dictionary存储键值对实现高效查找和遍历【基础】
C# Dictionary必须指定泛型类型,否则编译失败;键需不可变且重写GetHashCode和Equals;取值应优先用TryGetValue避免KeyNotFoundException;遍历用foreach(var kvp in dict)而非Keys/Values分离;多线程须用Concur
C# Dictionary必须指定泛型类型,否则编译失败;键需不可变且重写GetHashCode和Equals;取值应优先用TryGetValue避免KeyNotFoundException;遍历用foreach(var kvp in dict)而非Keys/Values分离;多线程须用ConcurrentDictionary或加锁。

Dictionary 初始化时必须指定泛型类型
直接写 new Dictionary() 是行不通的,编译器会直接报错。C# 的 Dictionary 是个强类型的家伙,TKey 和 TValue 这两个泛型参数一个都不能少。比如,你想建立一个从字符串ID到用户对象的映射,正确的写法是 new Dictionary。可别图省事写成 new Dictionary(),或者用 new Dictionary 来蒙混过关——后者虽然能通过编译,但不仅失去了类型安全,还会引入不必要的装箱开销,得不偿失。
常见的错误提示是编译错误 CS0305(使用了未指定泛型参数的类型),或者在运行时冷不丁地抛出一个 InvalidCastException。
- 键类型的选择有讲究:优先使用不可变、且已经正确重写了
GetHashCode()和Equals()方法的类型,像string、int、Guid这些都是“模范键”。 - 自定义类当键?务必重写那两个方法:如果你用自己的类作为键,却忘了重写
GetHashCode()和Equals(),那么查找操作很可能会失效,因为字典默认使用引用相等来判断。 - 警惕可变键:避免使用内容可变的引用类型(比如一个没有重写相关方法的
List)作为键。因为一旦键对象的内容发生变化,其哈希码也可能改变,导致你再也找不到之前存入的数据。
添加和查找键值对必须注意 KeyNotFoundException
通过索引器直接取值,比如 dict[“abc”],是很多新手踩坑的地方。如果键“abc”不存在,它可不会友好地返回 null 或默认值,而是会直接抛出一个 KeyNotFoundException 异常。这在不确定键是否存在的场景下,尤其危险。
那么,正确的打开方式有哪些呢?主要分三类:
- 场景一:百分百确定键存在。那就直接用
dict[key],简单直接,性能也最好。但前提是你的逻辑必须保证这个键已经被添加了。 - 场景二:不确定键是否存在,但需要获取值。这是最推荐的做法:使用
dict.TryGetValue(key, out value)。它返回一个布尔值告诉你查找是否成功,并通过out参数返回找到的值。不抛异常,性能也优化过,一举两得。 - 场景三:只关心键是否存在,不关心值。可以用
dict.ContainsKey(key)。不过要注意,它通常比TryGetValue略慢一点,因为内部可能多了一次哈希查找。除非你真的不需要那个值,否则TryGetValue通常是更优选择。
来看一个典型的用法示例:
if (users.TryGetValue(“U1001”, out var user))
{
Console.WriteLine(user.Name);
}
else
{
Console.WriteLine(“用户不存在”);
}
遍历 Dictionary 推荐用 foreach + KeyValuePair 而非 Keys/Values 分离
当需要同时遍历键和值时,有一种写法需要避免:foreach (var key in dict.Keys) { var val = dict[key]; ... }。问题在于,这会导致对同一个键进行两次哈希查找(一次在 Keys 集合中,一次通过索引器取值),性能有损耗。在并发修改的场景下,还可能引发错误。
更优雅高效的做法是直接遍历 Dictionary 本身,它会返回一系列的 KeyValuePair 结构体:
- 标准写法:
foreach (var kvp in dict),然后通过kvp.Key和kvp.Value访问。这是最清晰、最常用的方式。 - 解构写法(C# 7及以上):如果想让代码更简洁,可以使用元组解构语法:
foreach (var (id, user) in dict)。这要求键和值的类型是明确的,看起来一目了然。 - 需要避免的写法:不要为了遍历而特意调用
dict.Keys.ToList()。这会产生一个不必要的中间列表,分配额外内存,并且丢失了原始字典的结构语义。
Dictionary 不是线程安全的,多线程读写必须加锁或换 ConcurrentDictionary
这是一个至关重要的安全提示:标准的 Dictionary 并非线程安全。当多个线程同时执行添加、删除甚至某些读取操作时,可能会导致其内部数据结构损坏,结果就是抛出 InvalidOperationException 异常,或者返回错误的数据。即便是“读多写少”的场景,只要存在任何一个写操作,就不能毫无保护地使用普通 Dictionary。
别以为“我只读不写就没事”。要知道,Dictionary 在容量不足需要扩容时,会在内部重建数组,这个过程中并发读取也可能导致问题。
应对多线程访问,主要有两种主流策略:
- 手动同步(加锁):使用
lock语句包裹所有对字典的读写操作。锁对象建议使用一个私有的、只读的字段,避免直接锁this或字典实例本身,以减少死锁风险。 - 使用线程安全集合:直接切换到
ConcurrentDictionary。它在内部采用了更精细的锁机制(如分段锁),使得读操作通常可以无锁进行,写操作的锁粒度也更小,性能更好。但需要注意,其GetOrAdd、AddOrUpdate等方法接收的委托(工厂函数)可能是延迟执行的,并且可能被调用多次,因此不要在委托内编写带有副作用的逻辑。


































