首页
看点啥
插画图片
首页 科技看点 C# Dictionary字典增删改查、哈希原理与高频踩坑指南实用指南

C# Dictionary字典增删改查、哈希原理与高频踩坑指南实用指南

2026-10-28 0

平时做技术实践时,很多问题不是概念不会,而是细节没串起来。拿“C# Dictionary字典增删改查、哈希原理与高频踩坑指南”来说,它看着像小点,放到项目里常会牵出环境、配置、兼容性和维护成本。下面按实际采用顺序,把思路、关键写法和容易踩坑的地方讲清楚,便于大家直接对照操作。

导语:Dictionary(字典)就像一本真实的字典书,每个"词目"(键 Key)对应一段"释义"(值 Value)。你不用从头翻到尾,只要报出词目,就能直接定位到释义,这就是它最大的本事——按键更快查找。日常开发中只要遇到"借助一个东西找另一个东西"的场景(学号找学生、单词计数、缓存数据),基本都该用它。本文从新建、增删改查一直讲到哈希表原理和高频踩坑,适合初学者入门和复习。

一、Dictionary 是什么

Dictionary 是一个泛型键值对集合,位于 System.Collections.Generic 命名空间,两个泛型参数分别是键的类型和值的类型:

using System.Collections.Generic;

// 键是 string 类型,值是 int 类型
Dictionary ageDict = new Dictionary();

它有三个核心特点:

  1. 键唯一:同一个键只能存一个值,重复添加会报错。
  2. 按键查找快:平均查找时间接近 O(1),不会因为数据变多就明显变慢。
  3. 类型安全:因为是泛型,键和值的类型在编译期就定死了,不会出现装箱拆箱和类型转换问题。

【插图 1:Dictionary 成员知识地图】

二、新建与初始化

2.1 最常用的新建方式

// 方式一:new 出来再逐个添加
Dictionary dict1 = new Dictionary();

// 方式二:集合初始化器,创建时就把数据写好(最常用)
Dictionary dict2 = new Dictionary
{
    { "张三", 18 },
    { "李四", 20 }
};

// 方式三:索引器初始化(C# 6 及以上),写法更短
Dictionary dict3 = new Dictionary
{
    ["张三"] = 18,
    ["李四"] = 20
};

2.2 指定初始容量

事先知道大概要存多少条数据,能够在新建时指定容量,避免后面频繁扩容:

// 预计要放 100 条左右,提前把空间留好
Dictionary dict = new Dictionary(100);

2.3 忽略大小写的字符串键

键是字符串时默认区分大小写,"Apple" 和 "apple" 会被当成两个键。需忽略大小写时,传入系统提供的比较器:

Dictionary dict = new Dictionary(StringComparer.OrdinalIgnoreCase);
dict["Apple"] = 5;
Console.WriteLine(dict["apple"]); // 5:被当成同一个键

三、增删改查(核心)

3.1 增加:Add 和索引器

Dictionary dict = new Dictionary();

// Add:键不存在就添加,键已存在会抛 ArgumentException
dict.Add("张三", 18);

// 索引器:键不存在时自动添加,键已存在时覆盖旧值
dict["李四"] = 20;
dict["李四"] = 21; // 不报错,值被改成 21

两种方式的区别:

方式键已存在时适用场景
Add(key, value)抛异常确定键不应该重复,重复说明逻辑有问题
dict[key] = value覆盖旧值有则更新、无则新增

3.2 读取:索引器和 TryGetValue

Dictionary dict = new Dictionary
{
    ["张三"] = 18
};

// 索引器读取:键存在正常返回,键不存在抛 KeyNotFoundException
Console.WriteLine(dict["张三"]); // 18

// TryGetValue:安全的读取方式,键不存在返回 false,不抛异常
if (dict.TryGetValue("王五", out int age))
{
    Console.WriteLine(age);
}
else
{
    Console.WriteLine("没找到这个人");
}

【插图 2:索引器与 TryGetValue 两种读取方式对比】

读取一个"不一定存在"的键,优先用 TryGetValue实际处理时,,一次调用同时完成判断和取值,比先 ContainsKey 再读索引器少查一次。

3.3 判断键和值是否存在

Dictionary dict = new Dictionary
{
    ["张三"] = 18
};

Console.WriteLine(dict.ContainsKey("张三")); // True
Console.WriteLine(dict.ContainsValue(18)); // True

注意:ContainsKey 很快(哈希查找),而 ContainsValue 是从头到尾逐个比对,数据量大时比较慢,不要滥用。

3.4 删除和清空

dict.Remove("张三");   // 键存在就删除并返回 true,键不存在返回 false,不抛异常
dict.Clear(); // 清空所有键值对

3.5 常用成员速查表

成员作用
Add(key, value)添加,键重复抛异常
dict[key]读取/赋值,读不存在的键抛异常
TryGetValue(key, out value)安全读取,得到是否找到
ContainsKey(key)判断键是否存在
ContainsValue(value)判断值是否存在(线性查找,较慢)
Remove(key)删除指定键
Clear()清空
Count当前键值对数量
Keys / Values所有键 / 所有值的集合

四、遍历字典

4.1 foreach 遍历键值对

Dictionary dict = new Dictionary
{
    ["张三"] = 18,
    ["李四"] = 20
};

// 每一项都是 KeyValuePair
foreach (KeyValuePair pair in dict)
{
    Console.WriteLine($"{pair.Key}:{pair.Value}");
}

4.2 只遍历键或只遍历值

foreach (string name in dict.Keys)
{
    Console.WriteLine(name);
}

foreach (int age in dict.Values)
{
    Console.WriteLine(age);
}

需特别注意:字典不保证遍历顺序实际处理时,,不要写依赖"先添加的先被遍历到"的逻辑;需有序就用后面讲到的 SortedDictionary。

五、底层原理:为什么按键查找这么快

5.1 哈希表的基本思路

Dictionary 内部用的是哈希表(Hash Table)。查找过程能够理解为三步:

  1. 对键调用 GetHashCode(),得到一个整数(哈希码)。
  2. 落到代码里,用哈希码做一次取模运算,算出这个键应该放在哪个"桶"(bucket,能够理解为编号的格子)。
  3. 直接去那个桶里找,不用翻遍整个集合。

【插图 3:哈希表查找原理】

5.2 哈希冲突

两个不同的键可能算出同一个桶,这叫哈希冲突从实现思路看,。Dictionary 的处理办法是让同一个桶里的元素串成一条链,先定位到桶,再在链上逐个比较键。所以:

  • 平均查找复杂度是 O(1),一次定位。
  • 实际处理时,极端情况下(大量冲突)会退化到 O(n),这也是为什么键的哈希算法要尽量分散。

5.3 扩容

落到代码里,元素不断增加、桶变得拥挤时,字典会自动扩容(容量取更大的质数)同时重新计算所有元素的位置,这一步有一定开销。所以能预估数据量时,新建时就带上容量参数。

六、相关集合对比

集合特点适用场景
Dictionary哈希表,按键 O(1) 查找,无序绝大多数键值查找场景
Hashtable非泛型老版本,键值都是 object老代码维护,新项目不用
SortedDictionary按键自动排序,底层是二叉树需按键有序输出
List>保持插入顺序,查找靠遍历顺序重要、数据量小
ConcurrentDictionary线程安全多线程同时读写

Dictionary 本身不是线程安全的结合项目来看,,多线程环境下同时读写可能出问题,这种场景应采用 ConcurrentDictionary。

七、实战:统计字符出现次数

落到代码里,字典最经典的应用之一就是"计数"。下面统计一个字符串里每个字符出现了多少次:

string text = "hello dictionary";
Dictionary countDict = new Dictionary();

foreach (char c in text)
{
    // 键已存在就加 1,不存在就先初始化为 1
    if (countDict.ContainsKey(c))
    {
        countDict[c]++;
    }
    else
    {
        countDict[c] = 1;
    }
}

foreach (KeyValuePair pair in countDict)
{
    Console.WriteLine($"字符 {pair.Key} 出现 {pair.Value} 次");
}

【插图 4:字符计数程序运行结果】(发布时在此插入图片 fig4-console-result.png)

八、常用坑

  1. 实际处理时,用索引器直接读不确定存在的键,抛 KeyNotFoundException。 落到代码里,读取外部数据、用户输入对应的键时,统一用 TryGetValue,或者先 ContainsKey 判断,不要赌键一定存在。
  2. 用 Add 添加重复键,抛 ArgumentException。 实际处理时,想实现"有则更新、无则新增",直接用索引器赋值 dict[key] = value;坚持用 Add 时先判断 ContainsKey。
  3. 从实现思路看,自定义类当键,却没有重写 GetHashCode 和 Equals,导致"内容相同"的键查不到。 默认按引用比较,两个 new 出来的对象即使字段完全一样也是不同的键。解决办法是在键类里重写这两个方法,或者直接用 record 类型(自带基于内容的相等比较),也能够在构造字典时传入自定义的 IEqualityComparer。
  4. 理解这一步时,foreach 遍历过程中添加或删除元素,抛 InvalidOperationException。 在这个场景下,遍历中不能修改集合。需删除时,先把要删的键收集到一个 List 里,遍历结束后再统一 Remove。
  5. 依赖字典的遍历顺序。 从实现思路看,字典的契约里没有"有序"这一条,不同 .NET 版本的内部实现也可能不同,依赖顺序属于埋雷。需按插入顺序就用 List,需按键排序就用 SortedDictionary。另外引用类型的键不能为 null,传入 null 键会抛 ArgumentNullException。

九、小结

  1. Dictionary 是泛型键值对集合结合项目来看,,键唯一、按键平均 O(1) 查找,是"借助一个东西找另一个东西"的首选。
  2. 记住增删改查的正确姿势:新增用 Add 或索引器,不确定的读取用 TryGetValue,删除用 Remove,遍历用 KeyValuePair。
  3. 底层是哈希表:键的哈希码决定桶位置,哈希冲突在桶内挂链处理;能预估数据量就指定初始容量。
  4. 理解这一步时,自定义类型当键要重写 GetHashCode 和 Equals(或用 record);多线程场景用 ConcurrentDictionary;需有序用 SortedDictionary。

实际处理时,总的来说,C# Dictionary增删改查、哈希原理与踩坑适合结合实际项目边做边理解。先抓住核心思路,再逐步补上细节和边界处理,最后效果会更稳定,也更容易复用。

喜欢(0)

上一篇

Ollama本地大模型配置与实践指南实用指南

下一篇

Ollama最新版安装包下载和安装步骤(开源,简单,小白5分钟学会)实用指南

Ollama最新版安装包下载和安装步骤(开源,简单,小白5分钟学会)实用指南
猜你喜欢