Dictionary 学习笔记

一、基础概念与用法

1.1 什么是 Dictionary?

Dictionary<TKey, TValue> 是 C# 中非常常用的键值对集合,属于 System.Collections.Generic 命名空间。

  • 特点

    • 每个元素由一个键(Key) 和一个值(Value) 组成。
    • 键必须唯一,不能重复;值可以重复。
    • 查询速度快(基于哈希表实现)。
    • 类似于其他语言中的“Map”或“Hash”。

💡 类比理解:想象一个电话簿 —— “姓名”是 Key,“电话号码”是 Value,通过姓名快速找到号码。


1.2 如何定义和初始化?

using System;
using System.Collections.Generic;

// 定义一个字典:Key 是 string,Value 是 int
Dictionary<string, int> studentScores = new Dictionary<string, int>();

// 初始化时赋值(C# 6+ 推荐写法)
var scores = new Dictionary<string, int>
{
    { "张三", 85 },
    { "李四", 92 },
    { "王五", 78 }
};

1.3 常用方法与操作

✅ 添加元素

scores.Add("赵六", 90); // 添加新键值对
// 或者直接赋值(如果键已存在会覆盖!)
scores["赵六"] = 95; // 覆盖原值

⚠️ 注意:Add() 方法如果键已存在会抛出异常!推荐使用 TryAdd() 或直接赋值。


✅ 获取元素

int score = scores["张三"]; // 直接通过键获取值(若键不存在会报错!)

// 更安全的做法:使用 TryGetValue
if (scores.TryGetValue("张三", out int foundScore))
{
    Console.WriteLine($"张三的分数是:{foundScore}");
}
else
{
    Console.WriteLine("找不到该学生!");
}

✅ 修改与删除

// 修改值
scores["张三"] = 88;

// 删除指定键
scores.Remove("李四");

// 清空所有元素
scores.Clear();

✅ 遍历字典

foreach (KeyValuePair<string, int> kvp in scores)
{
    Console.WriteLine($"{kvp.Key}: {kvp.Value}");
}

// 或者使用解构语法(C# 7+)
foreach (var (name, score) in scores)
{
    Console.WriteLine($"{name}: {score}");
}

✅ 判断是否包含键或值

bool hasZhangSan = scores.ContainsKey("张三");     // true
bool has90 = scores.ContainsValue(90);             // false(已被覆盖)

二、进阶知识点

2.1 Dictionary vs 其他集合对比

集合类型 特点
Dictionary<TKey, TValue> 键值对结构,查询快,键唯一,适合查找场景
List<T> 有序列表,按索引访问,适合顺序遍历或频繁插入/删除中间元素
HashSet<T> 无序、无重复元素,适合去重和快速判断是否存在
SortedDictionary 保持按键排序,查询稍慢但遍历时有序(适合需要排序的场景)

选择建议

  • 需要“通过名字找数据” → 选 Dictionary
  • 需要“保存一堆不重复的名字” → 选 HashSet
  • 需要“按添加顺序或排序展示” → 选 ListSortedDictionary

2.2 性能特点

  • 🔍 查找性能:平均 O(1),非常快!因为底层是哈希表。
  • 📈 插入/删除性能:平均 O(1),但在哈希冲突严重时可能退化。
  • ⚖️ 内存开销:比数组或 List 略高,因为要存储键和哈希信息。

📌 提示:如果你知道最终容量,可以提前指定容量减少扩容开销:

var dict = new Dictionary<string, int>(100); // 预分配空间

2.3 线程安全问题

重要提醒Dictionary<TKey, TValue> 不是线程安全的

多个线程同时读写会导致不可预知错误(如 InvalidOperationException)。

解决方案:

方案一:手动加锁

private static readonly object _lock = new object();

lock (_lock)
{
    scores["张三"] = 85;
}

方案二:使用线程安全版本 ConcurrentDictionary

using System.Collections.Concurrent;

var concurrentDict = new ConcurrentDictionary<string, int>();
concurrentDict.TryAdd("张三", 85);
concurrentDict["李四"] = 90; // 支持直接赋值

💡 ConcurrentDictionary 是专门为多线程设计的,内部做了并发控制,推荐在多线程环境中使用。


2.4 自定义键类型注意事项

如果键是自定义类,必须重写 Equals()GetHashCode() 方法,否则可能无法正确查找!

public class Student
{
    public string Name { get; set; }
    public int Id { get; set; }

    public override bool Equals(object obj)
    {
        if (obj is Student other)
            return Id == other.Id && Name == other.Name;
        return false;
    }

    public override int GetHashCode()
    {
        return HashCode.Combine(Id, Name);
    }
}

📌 否则可能出现:明明对象相同,却查不到的情况!


三、实际工作中的使用场景

3.1 场景一:缓存数据(提升性能)

比如缓存用户信息,避免重复查询数据库:

private static readonly Dictionary<int, User> _userCache = new();

public User GetUser(int userId)
{
    if (!_userCache.TryGetValue(userId, out User user))
    {
        user = LoadUserFromDatabase(userId); // 从数据库加载
        _userCache[userId] = user;           // 缓存起来
    }
    return user;
}

✅ 优点:首次加载后,后续访问几乎瞬间返回,大幅提升响应速度。


3.2 场景二:配置管理 / 映射关系

比如根据国家代码映射国家名称:

var countryMap = new Dictionary<string, string>
{
    { "CN", "中国" },
    { "US", "美国" },
    { "JP", "日本" }
};

string name = countryMap["CN"]; // 输出:中国

✅ 适合处理“枚举型”或“映射型”数据,清晰直观。


3.3 场景三:统计频率(计数器)

比如统计单词出现次数:

var wordCount = new Dictionary<string, int>();

string[] words = { "apple", "banana", "apple", "orange", "banana", "apple" };

foreach (string word in words)
{
    wordCount[word] = wordCount.GetValueOrDefault(word, 0) + 1;
}

foreach (var kvp in wordCount)
{
    Console.WriteLine($"{kvp.Key}: {kvp.Value}次");
}

GetValueOrDefault(key, defaultValue) 是处理“键不存在时默认值”的好帮手!


3.4 场景四:状态机 / 事件分发

比如根据不同指令执行不同方法:

var commandHandlers = new Dictionary<string, Action>
{
    { "start", () => Console.WriteLine("开始游戏") },
    { "pause", () => Console.WriteLine("暂停游戏") },
    { "quit",  () => Console.WriteLine("退出游戏") }
};

string input = "start";
if (commandHandlers.TryGetValue(input, out Action handler))
{
    handler();
}

✅ 优雅替代大量 switch-caseif-else,代码更简洁可扩展!


🎯 总结速查表

操作 方法/语法
创建字典 new Dictionary<TKey, TValue>()
添加元素 .Add(key, value)dict[key] = val
获取值 dict[key].TryGetValue(...)
删除元素 .Remove(key)
判断是否存在 .ContainsKey(key)
遍历 foreach(var kvp in dict)
多线程安全 使用 ConcurrentDictionary
自定义键类型 必须重写 EqualsGetHashCode

📚 学习小贴士

  • ✅ 初学阶段先掌握增删改查和遍历,别急着看源码。
  • ✅ 实际编码中多用 TryGetValue 避免异常。
  • ✅ 多线程环境务必使用 ConcurrentDictionary
  • ✅ 自定义键类一定要重写 EqualsGetHashCode
posted @ 2025-10-31 13:32  恨水长秋  阅读(12)  评论(0)    收藏  举报