首页 / C# 入门教程 / Dictionary<K,V> 字典

C# 入门教程

Dictionary<K,V> 字典

本教程共 100 篇 · 第 24 篇 · 更新于 2026-07-31 · 约 12 分钟阅读

C#C# 入门教程编程语言Dictionary字典

24. Dictionary<K,V> 字典

本节目标:用字典以”键”查”值”,掌握 Add、索引器、TryGetValue,并能用它做词频统计与去重。

数组和 List 靠”位置”存取,第几位就拿第几位。但很多时候我们想按”名字”取东西:用学号查姓名、用单词查释义、用配置名查配置值。这种”按键找值”的需求,靠字典(dictionary)最合适。

什么是字典

字典存的是键值对(key-value pair)。给一个键(key),立刻拿到对应的值(value),速度极快。它位于 System.Collections.Generic 命名空间,底层用哈希表(hash table)实现。

Dictionary<int, string> idToName = new Dictionary<int, string>();
Note

键必须唯一。往字典里加重复键会抛运行时异常。这正是字典适合做”去重”和”按编号查名字”的原因。值(value)可以重复,但键(key)不能重复。

哈希表:为什么查得这么快

理解字典有多快,先看它怎么存。Dictionary 内部维护一组”桶(bucket)“。当你放入 dict[key] = value 时,它会拿 key 算出一个哈希码(hash code),再用哈希码决定这一对数据落到哪个桶里。

查找时,同样用 key 算哈希码,直接跳到对应的桶,几乎不用逐个比较。所以无论字典里有一百个还是一百万个键,“按键取值”的耗时都差不多(平均 O(1))。对比之下,在 List 里找某个值要挨个扫(O(n)),数据越多越慢。这就是字典的核心价值:用空间换时间。

Tip

哈希查找像”按学号直接翻到对应抽屉”,而 List 查找像”从第一排座位挨个问过去”。人多了,前者优势就出来了。

添加元素:Add 与索引器

两种方式:用 Add 方法,或直接用索引器 dict[key] = value

Dictionary<int, string> idToName = [];
idToName.Add(1, "小明");
idToName.Add(2, "小红");
idToName[3] = "小刚";   // 索引器也能添加
Tip

Add 遇到重复键会报错;索引器 dict[key] = value 遇到已有键则是”覆盖”。想保险地加,先用 ContainsKey 判断。一句话区分:想新增用 Add(重复就报错提醒你),想”有则改、无则加”用索引器。

读取元素:索引器与 TryGetValue

最直白的是 dict[key],但键不存在会抛 KeyNotFoundException。更稳妥的是 TryGetValue

Dictionary<int, string> idToName = [];
idToName.Add(1, "小明");

// 不安全:键不存在会抛异常
// Console.WriteLine(idToName[99]);

// 安全:找不到返回 false,不会崩溃
if (idToName.TryGetValue(1, out string? name))
{
    Console.WriteLine($"查到:{name}");
}
Note

TryGetValueout 变量在找不到时会被赋成默认值(这里 string 是 null)。它把”查找”和”判断”合并成一步,是字典读值的推荐写法。配合 C# 的 out 变量声明,不用提前定义变量,干净利落。

修改已有的值

用索引器对已有键赋值,就是更新它的值。

Dictionary<int, int> scores = [];
scores[1] = 90;
scores[1] = 95;   // 同一键,值被更新
Console.WriteLine(scores[1]); // 95

判断是否含键、含值

ContainsKey 检查键在不在;ContainsValue 检查值在不在(它要全表扫描,相对慢,别在热路径频繁用)。

Dictionary<int, string> idToName = [];
idToName.Add(1, "小明");
Console.WriteLine($"含键 1:{idToName.ContainsKey(1)}");
Console.WriteLine($"元素个数:{idToName.Count}");
Warning

ContainsValue 内部要遍历所有键值对去找值,复杂度是 O(n)。如果经常需要”按值反查键”,说明你的数据模型可能更适合把关系反过来建,或改用别的集合。高频查询请优先基于 key。

遍历字典

foreach 遍历,每一项是个 KeyValuePair<K,V>,带 KeyValue 两个属性。

Dictionary<int, string> idToName = [];
idToName.Add(1, "小明");
idToName.Add(2, "小红");
foreach (var pair in idToName)
{
    Console.WriteLine($"键:{pair.Key},值:{pair.Value}");
}

也可以只遍历键或只遍历值:foreach (var k in idToName.Keys)foreach (var v in idToName.Values)

Note

字典的遍历顺序不保证固定。你放入 1、2、3,遍历出来未必是 1、2、3 的顺序,它取决于内部哈希分布。如果业务要求”按 key 排序输出”,遍历后对 key 排个序即可,别依赖字典自带的顺序。

删除元素

Remove(键) 删掉整对;Clear 清空全部。

idToName.Remove(1);

实战场景:词频统计

字典最经典的用法是计数。下面统计一段文字里每个字符出现了几次。

string text = "hello";
Dictionary<char, int> counts = [];
foreach (char c in text)
{
    if (counts.TryGetValue(c, out int n))
    {
        counts[c] = n + 1;
    }
    else
    {
        counts[c] = 1;
    }
}
foreach (var pair in counts)
{
    Console.WriteLine($"'{pair.Key}' 出现 {pair.Value} 次");
}
Tip

这段”先 TryGetValue,有就加一、没有就置 1”是字典计数的固定套路,面试和实战都常见。也可以写成 counts.TryAdd(c, 0)counts[c]++,语义一样,看个人口味。

去重场景

把一批数据塞进字典的”键”,天然就去重了,因为键不允许重复。

int[] raw = [1, 2, 2, 3, 3, 3];
Dictionary<int, bool> seen = [];
foreach (var x in raw) seen[x] = true;
foreach (var key in seen.Keys) Console.Write($"{key} "); // 1 2 3
Note

只想去重、不关心值,下一章的 HashSet<T> 更合适,它就是专门为去重而生的,写起来也更短。字典去重适合”顺手”的场景,专门去重请用 HashSet。

键的讲究:能用什么当键

键不一定是 int 或 string,任何类型都能当键,但有个前提:它必须正确实现了 EqualsGetHashCodestringint 这些内置类型都已经做好了这个工作,所以开箱即用。

如果你用自定义的 class 当键,要特别当心:默认的 object 相等是按”引用”比较的,两个内容相同但不同实例的对象会被当成不同键。想让”内容相同即同键”,要么重写 Equals/GetHashCode,要么改用 record(C# 9 起 record 默认按内容比较,天然适合当键)。

// record 按内容相等,适合做字典键
record ProductKey(int Id, string Region);

Dictionary<ProductKey, decimal> prices = [];
prices[new ProductKey(1, "CN")] = 9.9m;
Console.WriteLine(prices[new ProductKey(1, "CN")]); // 9.9,因为内容相同
Warning

当键用的对象,不要在放入字典后修改会影响哈希码和相等的字段。否则它”搬家”到了错误的桶,之后就查不到了,这是字典最隐蔽的 bug 之一。键最好用不可变(immutable)的类型。

字典与 List 怎么选

需求选哪个
按编号/名字快速查Dictionary<K,V>
按位置顺序访问、下标操作List<T>
需要去重集合HashSet<T>(下一章)
既要顺序又要偶尔按键查List<T> + 维护一个索引字典

真实用途举例

字典在业务里无处不在:用用户 ID 查用户资料(缓存)、用配置名查配置值、用状态码查提示语、用单词查翻译。它的核心价值就是”用 key 秒级定位 value”,是性能和可读性兼得的利器。

实战:用字典做配置查表

字典最接地气的一种用法,是把”编码 → 文案”的映射表固化下来,避免写一长串 if/else。比如把状态码翻译成提示语:

Dictionary<int, string> messages = new()
{
    [200] = "成功",
    [404] = "未找到",
    [500] = "服务器错误",
};

int code = 404;
if (messages.TryGetValue(code, out string? text))
{
    Console.WriteLine($"状态 {code}{text}");
}
else
{
    Console.WriteLine($"未知状态码 {code}");
}

用字典替代 if/else if 链,好处是”数据和逻辑分离”——以后加状态码只改这个表,不动判断代码。当分支超过五六个,switch 或字典都比长串 if 清爽。

扩容与性能

和 List 类似,Dictionary 内部桶数组满到一定程度也会扩容(通常是翻倍并重新分布)。频繁放入已知数量的元素时,可以在构造时给个初始容量:new Dictionary<int, string>(1000),减少扩容带来的重新哈希开销。

Tip

字典的读取速度不随元素增多而明显下降,这正是哈希的威力。但代价是占用更多内存(桶数组会有空位),以及不保证顺序。需要”按 key 排序”时就遍历排序,别和字典的顺序较劲。

最佳实践清单

  1. 读值优先 TryGetValue:安全又不抛异常,还能顺手拿到结果。
  2. 新增用 Add,有则改无则加用索引器:语义分清,避免误覆盖。
  3. 键要唯一且尽量不可变:用 record 或内置类型当键最省心。
  4. 别用 ContainsValue 做高频查询:它要全表扫,慢。
  5. 已知规模就给初始容量,减少扩容。
  6. 遍历顺序不保证,需要有序就显式排序。

小结

字典用空间换时间,靠哈希让”按键取值”接近瞬间完成。记住优先用 TryGetValue 读取值,别直接 dict[key] 冒险;键必须唯一且最好不可变;遍历顺序不保证。计数、查表、去重是它最亮眼的三类用法。下一章看 HashSet、队列与栈这三种特殊集合。