C# Dictionary使用指南:一篇搞懂增删改查与遍历 – wiki基地


C# Dictionary使用指南:一篇搞懂增删改查与遍历

引言:为什么选择Dictionary?

在C#的编程世界里,数据集合是无处不在的基石。从简单的数组(Array)到灵活的列表(List),每种集合类型都有其特定的应用场景。然而,当我们面临需要通过一个唯一的标识符(即“键”,Key)来快速存取、查找和管理一系列数据(即“值”,Value)时,Dictionary<TKey, TValue> 无疑是最高效、最优雅的选择。

想象一下现实生活中的字典:我们通过拼音或部首(键)来查找汉字的释义(值)。电话簿也是如此,我们通过姓名(键)查找电话号码(值)。C#中的 Dictionary<TKey, TValue> 正是这种“键值对”思想在程序中的完美实现。它是一个泛型集合,存储着唯一的键和与之关联的值。

其核心优势在于 O(1) 的时间复杂度。简单来说,无论字典中有10个元素还是100万个元素,添加、删除或根据键查找一个元素所需的时间几乎是恒定的,这使其在处理大规模数据映射关系时表现得极为出色。

本文将作为一份详尽的指南,带领你从零开始,全面掌握 Dictionary 的创建、增(Add)、删(Remove)、改(Update)、查(Retrieve)等核心操作,深入探讨各种遍历方式及其优劣,并进一步拓展到性能、线程安全、自定义键类型等进阶主题。读完本文,你将对 Dictionary 有一个系统而深刻的理解。

一、 奠定基础:创建与初始化字典

在使用字典之前,我们首先需要创建它。Dictionary<TKey, TValue> 是一个泛型类,意味着在创建时必须明确指定键(TKey)和值(TValue)的数据类型。

1. 标准的创建方式

最基础的创建方式是使用 new 关键字。

csharp
// 创建一个键为 string 类型,值为 int 类型的字典
// 用于存储学生姓名和对应的分数
Dictionary<string, int> studentScores = new Dictionary<string, int>();

此时,我们得到一个空的字典,可以随时向其中添加数据。

2. 使用集合初始化器

为了让代码更简洁,我们可以在创建时直接用大括号 {} 初始化一部分数据。这种方式非常直观,适合在已知初始数据的情况下使用。

“`csharp
// 使用集合初始化器创建并填充字典
var studentScores = new Dictionary
{
{ “Alice”, 95 },
{ “Bob”, 88 },
{ “Charlie”, 76 }
};

// C# 6.0 及以上版本引入了新的索引初始化语法,更加简洁
var cityPopulations = new Dictionary
{
[“Beijing”] = 21540000,
[“Shanghai”] = 24280000,
[“Tokyo”] = 13960000
};
“`

关键点:字典的键必须是 唯一 的。如果在初始化时提供了重复的键,编译器会直接报错(使用 { "key", value } 语法)或在运行时抛出 ArgumentException。同时,键 不能为 null

二、 核心操作:增、查、改、删 (CRUD)

掌握了创建,接下来就是对字典内容进行动态管理的核心——增删改查。

1. 【增】添加元素

向字典中添加新的键值对是基本操作。C# 提供了多种方式,各有其适用场景。

方法一:Add() 方法(最严格)

Add() 方法用于添加一个键值对。如果尝试添加一个已存在的键,它会毫不留情地抛出 System.ArgumentException

“`csharp
var studentScores = new Dictionary();
studentScores.Add(“David”, 92); // 成功添加
studentScores.Add(“Eva”, 85); // 成功添加

try
{
// 尝试添加一个已经存在的键 “David”
studentScores.Add(“David”, 100);
}
catch (ArgumentException ex)
{
Console.WriteLine(ex.Message); // 输出: “An item with the same key has already been added.”
}
“`

使用场景:当你明确知道要添加的键必须是新的,任何重复都应视为程序逻辑错误时,Add() 是最佳选择,因为它能通过异常及时暴露问题。

方法二:索引器 [](最便捷的“增或改”)

索引器提供了一种更灵活的方式。如果键不存在,它会创建新的键值对;如果键已存在,它会 更新 该键对应的值。

“`csharp
var studentScores = new Dictionary();
studentScores[“Frank”] = 99; // “Frank” 不存在,添加新元素
Console.WriteLine(studentScores[“Frank”]); // 输出: 99

studentScores[“Frank”] = 95; // “Frank” 已存在,更新其值
Console.WriteLine(studentScores[“Frank”]); // 输出: 95
“`

使用场景:当你希望实现“如果存在就更新,不存在就添加”的逻辑时,索引器是最简洁、最常用的方法。

方法三:TryAdd() 方法(最安全)

从 .NET Core 2.0 / .NET Standard 2.1 开始,新增了 TryAdd() 方法。它尝试添加一个键值对,如果键已存在,它不会抛出异常,而是返回 false;如果添加成功,则返回 true

“`csharp
var studentScores = new Dictionary { { “Grace”, 89 } };

bool added1 = studentScores.TryAdd(“Heidi”, 91); // “Heidi” 不存在,添加成功
Console.WriteLine($”Added Heidi? {added1}”); // 输出: Added Heidi? True

bool added2 = studentScores.TryAdd(“Grace”, 90); // “Grace” 已存在,添加失败
Console.WriteLine($”Added Grace again? {added2}”); // 输出: Added Grace again? False
Console.WriteLine(studentScores[“Grace”]); // 值仍然是 89,未被修改
“`

使用场景:当你需要添加元素但又不希望因为键重复而中断程序流程(例如在多线程或复杂的业务逻辑中),TryAdd() 是最安全、最高效的选择,避免了 try-catch 的开销。

2. 【查】检索元素

查找是 Dictionary 的核心价值所在。

方法一:索引器 [](最直接,但不安全)

使用索引器 [] 是最直接的取值方式。但它有一个巨大的陷阱:如果键不存在,它会抛出 System.Collections.Generic.KeyNotFoundException

“`csharp
var studentScores = new Dictionary { { “Ivy”, 93 } };
int ivyScore = studentScores[“Ivy”]; // 成功获取值 93

try
{
int jackScore = studentScores[“Jack”]; // “Jack” 不存在
}
catch (KeyNotFoundException)
{
Console.WriteLine(“Key ‘Jack’ was not found.”); // 捕获异常
}
“`

使用场景:只有在你 100% 确定 键一定存在的情况下,才应该使用索引器直接取值。

方法二:TryGetValue() 方法(最推荐,最安全高效)

为了解决 KeyNotFoundException 的问题,Dictionary 提供了 TryGetValue() 方法。这是在不确定键是否存在的情况下进行查找的 最佳实践

TryGetValue() 尝试获取与指定键关联的值。如果找到,它会将值赋给 out 参数并返回 true;如果找不到,它会返回 false,并且 out 参数会被赋为该值类型的默认值(例如,int 的默认值是 0,string 的是 null)。

“`csharp
var studentScores = new Dictionary { { “Mallory”, 82 } };

// 尝试获取 “Mallory” 的分数
if (studentScores.TryGetValue(“Mallory”, out int malloryScore))
{
Console.WriteLine($”Mallory’s score is: {malloryScore}”); // 输出: Mallory’s score is: 82
}
else
{
Console.WriteLine(“Mallory not found.”);
}

// 尝试获取 “Trent” 的分数
if (studentScores.TryGetValue(“Trent”, out int trentScore))
{
Console.WriteLine($”Trent’s score is: {trentScore}”);
}
else
{
// 因为 “Trent” 不存在,此分支被执行
Console.WriteLine($”Trent not found. Score variable is: {trentScore}”); // 输出: Trent not found. Score variable is: 0
}
``
**优势**:
TryGetValue` 只执行一次哈希查找,既安全又高效。它完美地将“检查存在性”和“获取值”两个操作合二为一。

方法三:ContainsKey() + 索引器(传统但略显冗余)

TryGetValue 出现之前,常见的安全做法是先用 ContainsKey() 检查,再用索引器取值。

“`csharp
var studentScores = new Dictionary { { “Walter”, 98 } };

if (studentScores.ContainsKey(“Walter”))
{
int walterScore = studentScores[“Walter”];
Console.WriteLine($”Walter’s score is: {walterScore}”);
}
``
**性能提示**:这种方法会执行 **两次** 哈希查找:一次是
ContainsKey(),一次是索引器[]。因此,在性能敏感的场景下,TryGetValue()` 明显优于此方法。

Dictionary 还提供了 ContainsValue() 方法,用于检查是否存在某个值。但请注意,此操作需要遍历整个字典,其时间复杂度为 O(n),效率远低于基于键的操作。

3. 【改】更新元素

更新操作非常简单,主要依赖索引器。

“`csharp
var studentScores = new Dictionary { { “Peggy”, 75 } };
Console.WriteLine($”Peggy’s original score: {studentScores[“Peggy”]}”); // 输出: 75

// 使用索引器更新 Peggy 的分数
studentScores[“Peggy”] = 80;
Console.WriteLine($”Peggy’s updated score: {studentScores[“Peggy”]}”); // 输出: 80
``
正如前面“增”部分所讲,索引器的行为是“增或改”。如果键
Peggy不存在,上述代码会添加它,而不是更新。如果你的业务逻辑要求必须是更新一个已存在的项,那么在更新前最好先用ContainsKey()` 进行判断。

4. 【删】移除元素

方法一:Remove(TKey key)

Remove() 方法根据键来移除一个键值对。如果成功找到并移除了元素,它返回 true;如果键不存在,它什么也不做,并返回 false。这个方法不会抛出异常。

“`csharp
var studentScores = new Dictionary
{
{ “Victor”, 88 },
{ “Wendy”, 91 }
};

bool removedVictor = studentScores.Remove(“Victor”); // 移除 “Victor”
Console.WriteLine($”Was Victor removed? {removedVictor}”); // 输出: True
Console.WriteLine($”Dictionary contains Victor? {studentScores.ContainsKey(“Victor”)}”); // 输出: False

bool removedZeke = studentScores.Remove(“Zeke”); // 尝试移除不存在的 “Zeke”
Console.WriteLine($”Was Zeke removed? {removedZeke}”); // 输出: False
“`

方法二:Clear()

如果你想一次性清空整个字典,Clear() 方法可以实现。

“`csharp
var studentScores = new Dictionary { { “Xavier”, 78 }, { “Yvonne”, 85 } };
Console.WriteLine($”Count before Clear: {studentScores.Count}”); // 输出: 2

studentScores.Clear();
Console.WriteLine($”Count after Clear: {studentScores.Count}”); // 输出: 0
“`

三、 循环的艺术:遍历字典

遍历字典是另一个高频操作。有多种方式可以实现,每种方式关注点不同。

1. 遍历 KeyValuePair<TKey, TValue>(最常用)

Dictionary 本身实现了 IEnumerable<KeyValuePair<TKey, TValue>> 接口,因此可以直接在 foreach 循环中使用。循环的每一项都是一个 KeyValuePair 对象,它包含了 KeyValue 两个属性。

“`csharp
var studentScores = new Dictionary
{
{ “Alice”, 95 },
{ “Bob”, 88 },
{ “Charlie”, 76 }
};

Console.WriteLine(“— Iterating through KeyValuePairs —“);
foreach (KeyValuePair pair in studentScores)
{
Console.WriteLine($”Student: {pair.Key}, Score: {pair.Value}”);
}

// 使用 var 关键字可以使代码更简洁
Console.WriteLine(“\n— Using ‘var’ for simplicity —“);
foreach (var pair in studentScores)
{
Console.WriteLine($”Student: {pair.Key}, Score: {pair.Value}”);
}
“`
这是遍历字典时获取键和值的 最推荐 的方式,因为它清晰、直接且高效。

2. 单独遍历键集合 (Keys 属性)

如果你只关心字典中的键,可以遍历 Dictionary.Keys 属性。

csharp
Console.WriteLine("\n--- Iterating through Keys ---");
foreach (string name in studentScores.Keys)
{
Console.WriteLine($"Student Name: {name}");
// 如果需要,也可以在循环内部通过键获取值
int score = studentScores[name];
Console.WriteLine($" - Their score is: {score}");
}

3. 单独遍历值集合 (Values 属性)

同理,如果你只关心值,可以遍历 Dictionary.Values 属性。

csharp
Console.WriteLine("\n--- Iterating through Values ---");
foreach (int score in studentScores.Values)
{
Console.WriteLine($"A student scored: {score}");
}

请注意,通过遍历 Values 集合,你无法直接回溯到与该值关联的键。

重要警告:遍历时不能修改字典!

foreach 循环遍历一个集合时,绝对不能 对该集合进行添加或删除操作。这样做会改变集合的内部状态,导致迭代器失效,并立即抛出 System.InvalidOperationException

“`csharp
var studentScores = new Dictionary { { “Alice”, 55 }, { “Bob”, 80 } };

try
{
// 错误示范:尝试在遍历时移除元素
foreach (var pair in studentScores)
{
if (pair.Value < 60)
{
studentScores.Remove(pair.Key); // 这将抛出异常!
}
}
}
catch (InvalidOperationException ex)
{
Console.WriteLine($”\nError: {ex.Message}”); // 输出: Collection was modified…
}
“`

正确做法:如果需要在遍历时删除元素,应该先把要删除的键收集起来,在循环结束后再进行删除。

“`csharp
// 正确示范
var keysToRemove = new List();
foreach (var pair in studentScores)
{
if (pair.Value < 60)
{
keysToRemove.Add(pair.Key);
}
}

foreach (string key in keysToRemove)
{
studentScores.Remove(key);
}
Console.WriteLine($”\nNumber of students after removal: {studentScores.Count}”);
或者,使用 C# 3.0 引入的 LINQ 会更简洁:csharp
// .NET Framework
studentScores = studentScores.Where(pair => pair.Value >= 60)
.ToDictionary(pair => pair.Key, pair => pair.Value);

// .NET 5+ 引入了 Remove(key, out value) 的重载,和 LINQ 结合可以更高效
var removedCount = studentScores.Keys.Where(key => studentScores[key] < 60).ToList()
.RemoveAll(key => studentScores.Remove(key));
“`

四、 进阶话题与最佳实践

1. 性能考量:Dictionary 为何如此之快?

Dictionary 的高效源于其内部的 哈希表(Hash Table)结构。当你添加一个键值对时,Dictionary 会调用键的 GetHashCode() 方法计算出一个哈希码(一个整数),然后通过这个哈希码快速定位到内部数组的一个“桶”(bucket)中来存储数据。查找时,它会重复这个过程,直接跳到对应的桶,从而避免了逐个元素比较的线性搜索。这就是 O(1) 复杂度的来源。

2. 线程安全问题

Dictionary<TKey, TValue> 不是线程安全的。如果在多个线程中同时对同一个 Dictionary 实例进行读写操作,可能会导致数据损坏或引发异常。

  • 解决方案一:手动加锁
    使用 lock 关键字可以确保在任何时候只有一个线程能访问字典。

    “`csharp
    private readonly object _lock = new object();
    private Dictionary _myDict = new Dictionary();

    public void AddOrUpdate(string key, int value)
    {
    lock (_lock)
    {
    _myDict[key] = value;
    }
    }
    “`

  • 解决方案二:使用 ConcurrentDictionary<TKey, TValue>
    .NET Framework 4.0 引入了 System.Collections.Concurrent.ConcurrentDictionary<TKey, TValue>。这是一个专门为多线程环境设计的字典,它内部实现了精细的锁机制,允许多个线程并发读取,以及在不阻塞整个集合的情况下进行写入。它提供了如 TryAdd, TryUpdate, TryRemove, AddOrUpdate 等原子操作,是多线程场景下的首选。

    csharp
    var concurrentDict = new System.Collections.Concurrent.ConcurrentDictionary<string, int>();
    // 在多线程中可以安全地调用
    concurrentDict.TryAdd("key1", 123);

3. 自定义类型作为键

当使用自定义的类或结构体作为 Dictionary 的键时,必须正确地重写 Equals()GetHashCode() 方法。

  • Equals(object obj): 用于判断两个对象是否相等。
  • GetHashCode(): 用于计算对象的哈希码。

规则
1. 如果两个对象通过 Equals() 判断是相等的,那么它们的 GetHashCode() 返回值 必须 相同。
2. 如果 GetHashCode() 返回值不同,那么 Equals() 一定 返回 false
3. 对象的哈希码在其生命周期内应保持不变。如果一个对象的哈希码会改变(例如,基于一个可变的属性),那么它不适合做字典的键。

“`csharp
public class Person
{
public int Id { get; set; }
public string Name { get; set; }

// 重写 Equals
public override bool Equals(object obj)
{
    if (obj is Person other)
    {
        return this.Id == other.Id && this.Name == other.Name;
    }
    return false;
}

// 重写 GetHashCode
public override int GetHashCode()
{
    // 使用元组或 HashCode.Combine ( .NET Core) 来组合哈希码
    return HashCode.Combine(Id, Name);
    // 或者老式方法:
    // unchecked { return (Id * 397) ^ (Name != null ? Name.GetHashCode() : 0); }
}

}

var people = new Dictionary();
var person1 = new Person { Id = 1, Name = “John” };
var person2 = new Person { Id = 1, Name = “John” };

people[person1] = “Developer”;

// 因为重写了 Equals 和 GetHashCode,person2 被认为是与 person1 相同的键
Console.WriteLine(people.ContainsKey(person2)); // 输出: True
Console.WriteLine(people[person2]); // 输出: Developer
``
如果不重写这两个方法,
Dictionary` 将会使用对象的引用地址来判断相等性,导致即使两个对象内容完全相同,也会被视为不同的键。

总结

Dictionary<TKey, TValue> 是C#程序员工具箱中一把锋利的瑞士军刀。它通过键值对的形式,为我们提供了无与伦比的数据查找和关联能力。

核心要点回顾
创建:使用 new 关键字,并可通过集合初始化器快速填充。
Add() (严格), 索引器 [] (增或改), TryAdd() (安全)。
:索引器 [] (快捷但不安全), TryGetValue() (安全高效的最佳实践), ContainsKey() (仅检查存在性)。
:主要使用索引器 []
Remove() (按键移除), Clear() (清空)。
遍历foreach (var pair in dict) 是最常用方式,注意遍历时不能修改集合。
进阶Dictionary 非线程安全,多线程请用 ConcurrentDictionary;自定义类型作键需重写 EqualsGetHashCode

通过深入理解并熟练运用这些知识点,你将能更加自信和高效地在项目中利用 Dictionary 解决各种实际问题,编写出更健壮、更优雅的代码。希望这篇详尽的指南能成为你掌握 Dictionary 道路上的得力助手。

发表评论

您的邮箱地址不会被公开。 必填项已用 * 标注

滚动至顶部