Langchain系列文章目录
01-玩转LangChain:从模型调用到Prompt模板与输出解析的完整指南
02-玩转 LangChain Memory 模块:四种记忆类型详解及应用场景全覆盖
03-全面掌握 LangChain:从核心链条构建到动态任务分配的实战指南
04-玩转 LangChain:从文档加载到高效问答系统构建的全程实战
05-玩转 LangChain:深度评估问答系统的三种高效方法(示例生成、手动评估与LLM辅助评估)
06-从 0 到 1 掌握 LangChain Agents:自定义工具 + LLM 打造智能工作流!
07-【深度解析】从GPT-1到GPT-4:ChatGPT背后的核心原理全揭秘
PyTorch系列文章目录
Python系列文章目录
C#系列文章目录
01-C#与游戏开发的初次见面:从零开始的Unity之旅
02-C#入门:从变量与数据类型开始你的游戏开发之旅
03-C#运算符与表达式:从入门到游戏伤害计算实践
04-从零开始学C#:用if-else和switch打造智能游戏逻辑
05-掌握C#循环:for、while、break与continue详解及游戏案例
06-玩转C#函数:参数、返回值与游戏中的攻击逻辑封装
07-Unity游戏开发入门:用C#控制游戏对象移动
08-C#面向对象编程基础:类的定义、属性与字段详解
09-C#封装与访问修饰符:保护数据安全的利器
10-如何用C#继承提升游戏开发效率?Enemy与Boss案例解析
11-C#多态性入门:从零到游戏开发实战
12-C#接口王者之路:从入门到Unity游戏开发实战 (IAttackable案例详解)
13-C#静态成员揭秘:共享数据与方法的利器
14-Unity 面向对象实战:掌握组件化设计与脚本通信,构建玩家敌人交互
15-C#入门 Day15:彻底搞懂数组!从基础到游戏子弹管理实战
16-C# List 从入门到实战:掌握动态数组,轻松管理游戏敌人列表 (含代码示例)
17-C# 字典 (Dictionary) 完全指南:从入门到游戏属性表实战 (Day 17)
文章目录
前言
大家好!欢迎来到我们 “C# 学习与游戏开发实践 - 50 天精通之旅” 的第 17 天!在前两天,我们学习了如何使用数组(Array
)和列表(List
)来存储和管理一组数据。它们在很多场景下都非常有用,但当我们需要根据某个“标识符”快速查找对应信息时,数组和列表的效率就不那么理想了(需要遍历查找)。
想象一下查字典或者通讯录,我们不会一页一页翻,而是根据首字母或姓名直接定位。今天,我们就来学习 C# 中实现这种高效查找的关键数据结构 —— 字典(Dictionary)。字典以**键值对(Key-Value Pair)**的形式存储数据,能够让你像查字典一样,通过唯一的“键”(Key)快速找到对应的“值”(Value)。
本文将带你深入理解 C# Dictionary
的核心概念、基本操作、应用场景,并通过一个游戏开发中常见的“玩家属性表”案例,让你掌握如何在实际项目中运用字典来高效管理数据。无论你是 C# 新手还是希望巩固数据结构知识的开发者,本文都将为你提供清晰、实用的指导。
一、什么是字典(Dictionary)?
1.1 键值对(Key-Value Pair)的核心思想
字典的核心思想非常直观,就是存储成对的信息:一个键(Key)和一个值(Value)。
- 键(Key): 用于唯一标识和查找对应值的“标签”。就像字典里的单词、通讯录里的人名。在一个字典中,键必须是唯一的,不能重复。
- 值(Value): 与键相关联的具体信息。就像字典里单词的释义、通讯录里的电话号码。值可以重复。
类比:
- 现实生活中的字典: 键是“单词”,值是“单词的解释”。
- 手机通讯录: 键是“联系人姓名”,值是“电话号码”或更详细的联系信息。
- 储物柜: 键是“柜子编号”,值是“柜子里存放的物品”。
这种“键 -> 值”的映射关系使得我们可以通过键快速定位到值,而不需要像遍历数组或列表那样逐个检查元素。
1.2 C# 中的 Dictionary<TKey, TValue>
在 C# 中,字典由泛型类 System.Collections.Generic.Dictionary<TKey, TValue>
实现。
Dictionary<TKey, TValue>
: 这是一个泛型类,意味着你可以在创建字典时指定键和值的具体数据类型。TKey
: 代表键的数据类型。例如,string
(字符串)、int
(整数)、Guid
等。TKey
类型必须是可哈希的(通常意味着它需要正确实现GetHashCode()
和Equals()
方法,内置类型如int
,string
等都已满足)。TValue
: 代表值的数据类型。可以是任何 C# 类型,如int
,float
,string
,bool
, 甚至是你自己定义的类或结构体。
要使用 Dictionary
,通常需要在代码文件顶部添加 using System.Collections.Generic;
指令。
1.3 字典与数组/列表的对比
字典、数组和列表都是常用的集合类型,但它们在存储方式、访问效率和使用场景上有所不同。
特性 | 数组 (Array) | 列表 (List) | 字典 (Dictionary<TKey, TValue>) |
---|---|---|---|
存储结构 | 固定大小的连续内存块 | 动态大小的连续内存块 (内部是数组) | 基于哈希表 (Hash Table) |
元素访问 | 通过索引 (整数, 从 0 开始) | 通过索引 (整数, 从 0 开始) | 通过唯一的键 (TKey) |
查找效率 | 索引访问: O(1) 值查找: O(n) |
索引访问: O(1) 值查找: O(n) |
键查找: 平均 O(1), 最坏 O(n) |
添加/删除 | 不支持动态增删 (大小固定) | 添加/末尾删除: 平均 O(1) 中间插入/删除: O(n) |
平均 O(1), 最坏 O(n) |
元素顺序 | 有序 (按索引) | 有序 (按添加顺序) | 通常无序 (不保证迭代顺序) |
主要用途 | 存储固定数量、类型统一的数据 | 存储可变数量、类型统一的数据 | 快速查找、映射、关联数据 |
核心优势:字典最大的优势在于其平均 O(1) 的查找、添加和删除效率。这是通过内部的哈希表实现的。简单来说,字典会计算键的哈希码(一个整数),并使用这个哈希码来快速定位到值存储的大致位置,从而避免了逐个比较。(注意:哈希冲突可能导致最坏情况下的 O(n) 效率,但在实际应用中很少发生,尤其对于内置类型作为键)。
二、字典的基本操作
掌握字典的基本 CRUD (Create, Read, Update, Delete) 操作是使用它的基础。
(请确保在使用以下代码示例前,已在文件顶部添加 using System;
和 using System.Collections.Generic;
)
2.1 创建字典
你可以创建一个空字典,或者在创建时就初始化一些键值对。
// 1. 创建一个空字典,键是字符串(string),值是整数(int)
Dictionary<string, int> playerScores = new Dictionary<string, int>();
Console.WriteLine("Created an empty dictionary. Count: " + playerScores.Count); // 输出: 0
// 2. 使用集合初始化器创建并填充字典
// 键是字符串(string),值是字符串(string)
Dictionary<string, string> itemDescriptions = new Dictionary<string, string>
{
{
"Potion", "Restores 50 HP" },
{
"Sword", "A basic weapon" },
{
"Key", "Opens the old gate" }
};
Console.WriteLine($"Created a dictionary with {
itemDescriptions.Count} items."); // 输出: 3
2.2 添加键值对
向字典中添加新的键值对主要有两种方法:
2.2.1 使用 Add() 方法
Add()
方法用于添加一个键值对。如果尝试添加一个已经存在的键,它会抛出 ArgumentException
异常。
playerScores.Add("Alice", 100);
playerScores.Add("Bob", 95);
Console.WriteLine($"Added Alice and Bob. Count: {
playerScores.Count}"); // 输出: 2
try
{
playerScores.Add("Alice", 110); // 尝试再次添加 "Alice"
}
catch (ArgumentException ex)
{
Console.WriteLine($"Error adding duplicate key: {
ex.Message}"); // 会捕获到异常
}
2.2.2 使用索引器 []
索引器 []
提供了一种更简洁的方式来添加或更新键值对。
- 如果键不存在,它会添加新的键值对。
- 如果键已存在,它会覆盖该键对应的旧值。
// 使用索引器添加 Charlie
playerScores["Charlie"] = 88;
Console.WriteLine($"Added Charlie using indexer. Score: {
playerScores["Charlie"]}"); // 输出: 88
// 使用索引器更新 Alice 的分数 (因为 "Alice" 已存在)
playerScores["Alice"] = 105;
Console.WriteLine($"Updated Alice's score using indexer: {
playerScores["Alice"]}"); // 输出: 105
Add()
vs 索引器 []
的关键区别:
Add()
: 只用于添加新键,重复会报错。- 索引器
[]
: 可用于添加新键,或更新现有键的值。
2.3 访问值
同样,访问与特定键关联的值也有两种主要方式:
2.3.1 使用索引器 []
最直接的方式,但如果键不存在于字典中,它会抛出 KeyNotFoundException
异常。
// 直接访问 Bob 的分数
int bobScore = playerScores["Bob"];
Console.WriteLine($"Bob's score (using indexer): {
bobScore}"); // 输出: 95
try
{
int davidScore = playerScores["David"]; // 尝试访问不存在的键 "David"
}
catch (KeyNotFoundException ex)
{
Console.WriteLine($"Error accessing non-existent key: {
ex.Message}"); // 会捕获到异常
}
2.3.2 使用 TryGetValue() 方法
TryGetValue()
是更安全、推荐的方式。它尝试获取键对应的值,如果键存在,返回 true
并通过 out
参数输出值;如果键不存在,返回 false
,不会抛出异常。
// 安全地尝试获取 Alice 的分数
if (playerScores.TryGetValue("Alice", out int aliceScore))
{
Console.WriteLine($"Successfully got Alice's score using TryGetValue: {
aliceScore}"); // 输出: 105
}
else
{
Console.WriteLine("Key 'Alice' not found.");
}
// 尝试获取不存在的键 "David"
if (playerScores.TryGetValue("David", out int davidScore))
{
Console.WriteLine($"David's score: {
davidScore}");
}
else
{
Console.WriteLine("Key 'David' not found using TryGetValue. (Expected)"); // 会执行这里
}
建议优先使用 TryGetValue()
来访问字典元素,以避免潜在的异常。
2.4 修改值
修改现有键对应的值,最常用的方法是使用索引器 []
。
// 假设 Bob 完成了一个任务,分数增加
if (playerScores.ContainsKey("Bob")) // 最好先检查键是否存在
{
playerScores["Bob"] = playerScores["Bob"] + 10; // 读取旧值,计算新值,再写回
// 或者直接 playerScores["Bob"] = 105;
Console.WriteLine($"Updated Bob's score after task: {
playerScores["Bob"]}"); // 输出: 105 (假设原为95)
}
else
{
Console.WriteLine("Cannot update score for Bob, key not found.");
}
2.5 删除键值对
2.5.1 删除指定键值对:Remove()
Remove()
方法根据键来删除一个键值对。如果成功删除,返回 true
;如果键不存在,返回 false
。
// 删除 Charlie
bool removed = playerScores.Remove("Charlie");
if (removed)
{
Console.WriteLine("Successfully removed Charlie.");
}
else
{
Console.WriteLine("Charlie not found, could not remove.");
}
Console.WriteLine($"Dictionary count after removing Charlie: {
playerScores.Count}"); // 输出: 2 (假设之前有 Alice, Bob, Charlie)
2.5.2 清空字典:Clear()
Clear()
方法会移除字典中所有的键值对。
playerScores.Clear();
Console.WriteLine($"Dictionary count after Clear(): {
playerScores.Count}"); // 输出: 0
2.6 检查键/值是否存在
2.6.1 检查键是否存在:ContainsKey()
ContainsKey()
是检查字典中是否包含特定键的最高效方法 (接近 O(1))。
// 重新填充 itemDescriptions 用于演示
itemDescriptions = new Dictionary<string, string>
{
{
"Potion", "Restores 50 HP" }, {
"Sword", "A basic weapon" }
};
if (itemDescriptions.ContainsKey("Potion"))
{
Console.WriteLine("Dictionary contains the key 'Potion'."); // 会输出
}
if (!itemDescriptions.ContainsKey("Shield"))
{
Console.WriteLine("Dictionary does not contain the key 'Shield'."); // 会输出
}
2.6.2 检查值是否存在:ContainsValue()
ContainsValue()
用于检查字典中是否存在某个值。注意:这个操作效率较低,因为它需要遍历字典中的所有值 (O(n))。
if (itemDescriptions.ContainsValue("A basic weapon"))
{
Console.WriteLine("Dictionary contains the value 'A basic weapon'."); // 会输出
}
2.7 遍历字典
有多种方式可以遍历字典中的所有条目:
2.7.1 遍历键值对 (KeyValuePair)
这是最常用的方式,可以同时访问键和值。
Console.WriteLine("\nIterating through KeyValuePairs:");
foreach (KeyValuePair<string, string> kvp in itemDescriptions)
{
Console.WriteLine($"Key: {
kvp.Key}, Value: {
kvp.Value}");
}
// 输出:
// Key: Potion, Value: Restores 50 HP
// Key: Sword, Value: A basic weapon
2.7.2 遍历键 (Keys)
如果你只关心键,可以遍历 Keys
属性。
Console.WriteLine("\nIterating through Keys:");
foreach (string itemKey in itemDescriptions.Keys)
{
Console.WriteLine($"Key: {
itemKey}");
// 如果需要值,可以通过键再次查找:
// Console.WriteLine($"Value: {itemDescriptions[itemKey]}");
}
// 输出:
// Key: Potion
// Key: Sword
2.7.3 遍历值 (Values)
如果你只关心值,可以遍历 Values
属性。
Console.WriteLine("\nIterating through Values:");
foreach (string itemValue in itemDescriptions.Values)
{
Console.WriteLine($"Value: {
itemValue}");
}
// 输出:
// Value: Restores 50 HP
// Value: A basic weapon
重要提示:不要依赖字典的迭代顺序。虽然在较新的 .NET 版本中,Dictionary
通常会保持元素的插入顺序,但这并非所有 .NET Framework 或 .NET Core/5+ 版本的保证行为。如果需要保证顺序,应考虑使用 SortedDictionary<TKey, TValue>
或在迭代前对键或键值对进行排序。
三、键值对的核心用途
字典之所以如此重要,是因为键值对的结构天然适用于解决很多编程问题:
3.1 快速查找(Lookups)
这是字典最核心的用途。当你需要根据一个唯一标识符(键)快速找到相关信息(值)时,字典是理想选择。
- 场景:
- 根据用户 ID (
int
) 查找用户的详细信息对象 (UserObject
)。 - 根据物品 ID (
string
) 获取物品的属性 (ItemStats
)。 - 根据 HTTP 状态码 (
int
) 查找对应的描述文本 (string
)。
- 根据用户 ID (
- 优势: 平均 O(1) 的查找速度远胜于数组或列表的 O(n) 遍历查找。
3.2 数据关联(Mapping/Association)
字典天然地表示了“从 A 到 B”的映射关系。
- 场景:
- 将英文单词 (
string
) 映射到中文翻译 (string
)。 - 将文件扩展名 (
string
) 映射到对应的处理程序 (ActionDelegate
)。 - 将配置项名称 (
string
) 映射到配置值 (string
或object
)。
- 将英文单词 (
- 优势: 清晰地表达了两个数据集之间的关联。
3.3 计数与频率统计(Counting/Frequency)
字典非常适合用来统计集合中各项出现的次数。
- 场景:
- 统计一篇文章中每个单词出现的频率(键:单词
string
,值:次数int
)。 - 统计日志文件中各种错误类型出现的次数(键:错误码
int
或string
,值:次数int
)。 - 统计游戏中玩家拾取各种道具的数量(键:道具 ID
string
,值:数量int
)。
- 统计一篇文章中每个单词出现的频率(键:单词
- 实现: 遍历数据源,如果字典中已存在该项的键,则将其值加一;否则,添加该项作为新键,并将值设为 1。
// 示例:统计字符频率
string message = "hello world, hello csharp!";
Dictionary<char, int> charFrequency = new Dictionary<char, int>();
foreach (char c in message)
{
if (char.IsLetterOrDigit(c)) // 只统计字母和数字
{
char lowerChar = char.ToLower(c); // 不区分大小写
if (charFrequency.TryGetValue(lowerChar, out int count))
{
charFrequency[lowerChar] = count + 1; // 已存在,计数加 1
}
else
{
charFrequency.Add(lowerChar, 1); // 不存在,添加新条目,计数为 1
}
}
}
Console.WriteLine("\nCharacter Frequency:");
foreach (var kvp in charFrequency)
{
Console.WriteLine($"'{
kvp.Key}': {
kvp.Value}");
}
3.4 缓存(Caching)
对于那些计算成本高昂且结果可能重复使用的操作,可以用字典来缓存结果。
- 场景:
- 缓存数据库查询结果(键:查询参数
string
或object
,值:查询结果DataSet
或List<object>
)。 - 缓存复杂计算的结果,如路径规划(键:起点终点
Tuple<Vector2, Vector2>
,值:路径点列表List<Vector2>
)。 - 缓存网络请求的响应(键:URL
string
,值:响应内容string
或byte[]
)。
- 缓存数据库查询结果(键:查询参数
- 实现: 在执行计算或请求前,先检查字典中是否已存在对应键的结果。如果存在,直接返回缓存的值;如果不存在,执行计算/请求,将结果存入字典,然后再返回。
四、游戏案例:玩家属性表
在游戏开发中,管理玩家或角色的各种属性(如生命值、魔法值、力量、敏捷度等)是一项常见任务。这些属性种类繁多,数据类型也可能不同。使用字典可以提供一种灵活的方式来存储和访问这些属性。
4.1 场景描述
假设我们需要为一个 RPG 游戏的角色存储以下属性:
- 姓名 (Name): 字符串
- 等级 (Level): 整数
- 生命值 (Health): 浮点数
- 魔法值 (Mana): 浮点数
- 力量 (Strength): 整数
- 敏捷 (Agility): 整数
- 是否存活 (IsAlive): 布尔值
我们希望能够通过属性名称(字符串)来快速获取或修改对应的属性值。
4.2 使用字典实现
我们可以使用 Dictionary<string, object>
来实现这个需求。键是属性名称(string
),值是属性值(object
)。使用 object
作为值类型可以容纳不同的数据类型,但需要注意类型转换。
// 创建玩家属性字典
Dictionary<string, object> playerAttributes = new Dictionary<string, object>();
// 添加初始属性
playerAttributes.Add("Name", "Arin the Brave");
playerAttributes.Add("Level", 1);
playerAttributes.Add("Health", 100.0f);
playerAttributes.Add("Mana", 50.0f);
playerAttributes.Add("Strength", 10);
playerAttributes.Add("Agility", 8);
playerAttributes.Add("IsAlive", true);
Console.WriteLine("\n--- Initial Player Attributes ---");
foreach (var attribute in playerAttributes)
{
Console.WriteLine($"{
attribute.Key}: {
attribute.Value} (Type: {
attribute.Value.GetType().Name})");
}
4.3 属性的读取与修改
访问和修改属性时,需要使用属性名称作为键。由于值是 object
类型,取出后通常需要进行类型转换才能使用。推荐使用 TryGetValue
结合类型检查或模式匹配。
// 读取并可能修改生命值
string healthKey = "Health";
if (playerAttributes.TryGetValue(healthKey, out object healthValue) && healthValue is float currentHealth)
{
Console.WriteLine($"\nCurrent Health: {
currentHealth}");
// 模拟受到伤害
float damage = 15.5f;
currentHealth -= damage;
// 检查是否存活
if (currentHealth <= 0)
{
currentHealth = 0;
playerAttributes["IsAlive"] = false; // 更新存活状态
Console.WriteLine("Player has fallen!");
}
// 更新字典中的生命值
playerAttributes[healthKey] = currentHealth;
Console.WriteLine($"Health after taking {
damage} damage: {
playerAttributes[healthKey]}");
Console.WriteLine($"Is player alive? {
playerAttributes["IsAlive"]}");
}
else
{
Console.WriteLine($"Attribute '{
healthKey}' not found or has incorrect type.");
}
// 升级:增加等级和力量
if (playerAttributes.TryGetValue("Level", out object levelValue) && levelValue is int currentLevel &&
playerAttributes.TryGetValue("Strength", out object strValue) && strValue is int currentStrength)
{
playerAttributes["Level"] = currentLevel + 1;
playerAttributes["Strength"] = currentStrength + 2; // 升级加 2 点力量
Console.WriteLine($"\nPlayer Leveled Up! New Level: {
playerAttributes["Level"]}, New Strength: {
playerAttributes["Strength"]}");
}
4.4 优缺点与替代方案
使用 Dictionary<string, object>
管理属性的优点:
- 灵活性高: 可以动态地添加、删除属性,无需修改类结构。
- 按名称访问: 通过直观的字符串名称来存取属性,易于理解。
缺点:
- 类型不安全: 从字典中取出的值是
object
,需要手动进行类型检查和转换,容易出错(运行时错误)。 - 性能开销: 值类型(如
int
,float
,bool
)存入object
会发生装箱(Boxing),取出时需要拆箱(Unboxing),这会带来一定的性能损耗。 - 代码冗余: 每次访问都需要
TryGetValue
和类型检查/转换。 - 不利于 Unity Inspector:
Dictionary<string, object>
不能直接在 Unity 编辑器的 Inspector 面板中方便地查看和编辑。
替代方案:
-
创建专门的
PlayerStats
类或结构体:public class PlayerStats { public string Name; public int Level; public float Health; public float Mana; public int Strength; public int Agility; public bool IsAlive; // ... 其他方法,如 TakeDamage(), LevelUp() ... } // 使用: PlayerStats stats = new PlayerStats(); stats.Health -= 10;
- 优点: 类型安全,性能好(无装箱/拆箱),易于在 Inspector 中编辑。
- 缺点: 不够灵活,添加新属性需要修改类定义。
-
使用枚举作为键(如果值类型统一):
Dictionary<AttributeTypeEnum, float>
public enum AttributeType { Health, Mana, Strength, Agility } Dictionary<AttributeType, float> numericAttributes = new Dictionary<AttributeType, float>(); // numericAttributes.Add(AttributeType.Health, 100.0f); // float health = numericAttributes[AttributeType.Health];
- 优点: 键是类型安全的(枚举),代码更清晰,避免了字符串拼写错误。
- 缺点: 通常只适用于值类型都相同的情况(例如都是
float
或int
)。
-
Unity 的 ScriptableObject: 可以创建 ScriptableObject 资源来定义属性模板或存储具体角色的属性集,方便在编辑器中管理和复用。
结论: 对于属性集合相对固定、性能和类型安全要求较高的场景(这在游戏开发中很常见),通常推荐使用专门的类或结构体。Dictionary<string, object>
更适用于属性集非常动态、或者需要快速原型设计的场景,但要留意其缺点。
五、常见问题与注意事项
5.1 KeyNotFoundException
- 原因: 使用索引器
[]
访问一个不存在于字典中的键。 - 避免:
- 在访问前使用
ContainsKey(key)
检查键是否存在。 - 推荐: 使用
TryGetValue(key, out value)
进行安全的访问。
- 在访问前使用
5.2 键的唯一性与覆盖
Add(key, value)
: 要求key
必须是新的,否则抛出ArgumentException
。dictionary[key] = value
:- 如果
key
已存在,会覆盖原有的值。 - 如果
key
不存在,会添加新的键值对。
- 如果
- 注意: 理解这两种添加/更新方式的区别,根据需要选择合适的方法。
5.3 值类型与引用类型作为值
字典的值 (TValue
) 可以是值类型(如 int
, float
, struct
)或引用类型(如 string
, class
)。这会影响修改值的方式:
-
值类型 (Value Types): 当你从字典中获取一个值类型的值时,你得到的是该值的副本。修改这个副本不会影响字典中存储的原始值。必须将修改后的副本重新赋值给字典中的对应键。
Dictionary<string, int> counters = new Dictionary<string, int> { { "A", 1 } }; int countA = counters["A"]; // countA 是 1 (副本) countA = countA + 1; // countA 变成 2 Console.WriteLine(counters["A"]); // 输出 1 (字典中的值未变) counters["A"] = countA; // 必须写回 Console.WriteLine(counters["A"]); // 输出 2 (字典中的值已更新)
-
引用类型 (Reference Types): 当你从字典中获取一个引用类型的值时,你得到的是指向该对象的引用。通过这个引用修改对象的内部状态(如类的属性),会直接反映在字典存储的对象上,无需将引用重新赋值回字典。
public class MyData { public string Name; } Dictionary<int, MyData> dataMap = new Dictionary<int, MyData> { { 1, new MyData { Name = "Initial" } } }; MyData data1 = dataMap[1]; // data1 是指向 MyData 对象的引用 data1.Name = "Modified"; // 修改了引用所指向对象的 Name 属性 Console.WriteLine(dataMap[1].Name); // 输出 "Modified" (字典中的对象状态已改变)
5.4 性能考量(哈希冲突)
Dictionary
的高效依赖于 TKey
类型的 GetHashCode()
和 Equals()
方法。GetHashCode()
用于快速定位,Equals()
用于处理哈希冲突(多个不同的键算出了相同的哈希码)。
- 对于 C# 内置类型(
int
,string
,float
等),它们的哈希实现通常很好,性能有保障。 - 如果你使用自定义的类或结构体作为键 (
TKey
),你需要确保正确地重写 (override)GetHashCode()
和Equals()
方法。GetHashCode()
应尽可能地为不同的对象生成不同的哈希码,且对于同一个对象多次调用应返回相同的值。Equals()
用于精确比较两个对象是否相等。- 规则: 如果
a.Equals(b)
为true
,那么a.GetHashCode()
必须等于b.GetHashCode()
。 - 错误的实现会导致字典性能下降(接近 O(n))甚至行为不正确。这是一个相对高级的主题,但使用自定义类型作键时必须注意。
5.5 迭代顺序
再次强调:不要依赖 Dictionary
的迭代顺序。如果你需要按键排序,请使用 SortedDictionary<TKey, TValue>
,或者在迭代前获取 Keys
或 KeyValuePairs
,将它们排序后再进行遍历。
// 按键排序遍历
var sortedKeys = itemDescriptions.Keys.ToList(); // 获取键并转为列表
sortedKeys.Sort(); // 对键进行排序
Console.WriteLine("\nIterating by sorted keys:");
foreach (string key in sortedKeys)
{
Console.WriteLine($"Key: {
key}, Value: {
itemDescriptions[key]}");
}
六、总结
今天我们深入学习了 C# 中的 Dictionary<TKey, TValue>
,这是一个极其有用的数据结构,尤其是在需要高效查找和管理关联数据的场景中。
以下是本文的核心要点:
- 核心概念: 字典以键值对 (Key-Value Pair) 存储数据,其中键 (Key) 必须唯一,用于快速查找对应的值 (Value)。其内部基于哈希表实现,平均查找、添加、删除操作的时间复杂度为 O(1)。
- 基本操作: 我们掌握了字典的创建、添加 (
Add
,[]
)、安全访问 (TryGetValue
)、不安全访问 ([]
)、更新 ([]
)、删除 (Remove
,Clear
)、检查存在性 (ContainsKey
,ContainsValue
) 以及多种遍历方式 (KeyValuePair
,Keys
,Values
)。 - 核心用途: 字典非常适用于快速查找 (Lookups)、数据映射/关联 (Mapping)、计数/频率统计 (Counting) 以及缓存 (Caching) 等多种编程场景。
- 游戏开发应用: 通过“玩家属性表”的案例,我们看到了如何使用
Dictionary<string, object>
灵活管理动态属性,但也认识到其在类型安全和性能上的缺点,并了解了替代方案(如专用类/结构体、枚举作键、ScriptableObject)。 - 注意事项: 警惕
KeyNotFoundException
(优先使用TryGetValue
),理解Add
与索引器[]
在处理重复键时的不同行为,区分值类型和引用类型作为值时的修改方式差异,了解自定义键类型时正确实现GetHashCode/Equals
的重要性,并且不要依赖字典的迭代顺序。
字典是 C# 集合框架中的利器,熟练掌握它将大大提升你处理数据的能力。希望通过今天的学习,你对 Dictionary
有了更深入的理解!明天,我们将继续探索 C# 中其他有用的数据结构:队列(Queue)与栈(Stack)。
感谢阅读,如果你有任何疑问或想法,欢迎在评论区留言交流!