前言:
在 C# 中,键值对是一种常见的数据结构,可以使用不同的集合类实现。以下是常用的键值对集合类:
- Dictionary<TKey, TValue>:一种使用哈希表实现的键值对集合。它通过将键哈希为桶号,然后将值存储在桶中进行快速查找。
- SortedList<TKey, TValue>:一种基于数组实现的键值对集合。它会将键值对按照键排序并存储在数组中,以支持快速访问、查找和枚举。
- SortedDictionary<TKey, TValue>:一种使用红黑树实现的键值对集合。它能够按照键的排序进行快速查找,也可以快速地插入和删除键值对,并且该树具备自平衡的特性,使得插入、删除和搜索性能都非常优秀。
- ConcurrentDictionary<TKey, TValue>:一种线程安全的键值对集合,它支持在高并发场景下的快速查找、插入和删除,并且保证线程安全。
- ImmutableDictionary<TKey, TValue>:一种不可变的键值对集合,它提供了对集合进行修改时创建新集合的效率优化,同时也是线程安全的。
- KeyValuePair<TKey, TValue>:一个用于表示键值对的结构体类型。它将一个键和一个值组合在一起,并提供了多种方法来处理这个组合。
在工作中比较经常用到Dictionary 和SortedList。
SortedList
var list = new SortedList<string, int>(); //使用[]方式添加键值对时若该键不参加就添加,存在就修改对应的值 list["芋泥波波"] = 16; list["黄桃果霸"] = 16; //使用Add添加键值对时,键不能重复,否则报错 list.Add("杨枝甘露", 19); list.Add("满杯烧仙草", 21);
遍历获取键值,元素被检索为keyvaluePair对象
foreach (var VARIABLE in list) { Console.WriteLine(VARIABLE.Key + " 价格:" + VARIABLE.Value); } //输出: 黄桃果霸 价格:16 满杯烧仙草 价格:21 杨枝甘露 价格:19 芋泥波波 价格:16
获取值
//通过键获取值 Console.WriteLine("杨枝甘露的价格:" + list["杨枝甘露"]); //通过索引获取值 Console.WriteLine("芋泥波波的价格:" + list.Values[0]); //输出: 杨枝甘露的价格:19 芋泥波波的价格:16 //获取集合中的Values列表(若要获取所有的键则Values改成Keys) IList<int> values = list.Values; foreach (var value in values) { Console.WriteLine(value); } 输出: 16 21 19 16
如果在通过键获取值时,不确定该键值对是否存在,那么就使用TryGetValue,这样能避免键不存在时报异常
list.TryGetValue("黑糖珍珠", out int result); Console.WriteLine(result); 输出: 0
判断
//判断是否包含某个键 if (list.ContainsKey("芋泥波波")) { Console.WriteLine("包含"); } else { Console.WriteLine("不包含"); } 输出: 包含
移除
//通过键移除元素 list.Remove("芋泥波波");
HashTable
var hashtable = new Hashtable(); hashtable.Add("杨枝甘露", 19); hashtable.Add(21,"满杯烧仙草");
可以看出,使用HashTable时不需要设置类型,因此它是非泛型的
- 缺点:非泛型,需要装箱拆箱,效率较低,类型非安全,
- 好处:线程安全,当线程并发时,只有一个线程会执行这段代码
字典Dictionary
Dictionary与hashtable相反
- 缺点:线程非安全
- 优点:类型安全,效率高
IDictionary<string, int> dictionary = new Dictionary<string, int>(); dictionary["芋泥波波"] = 16; dictionary["黄桃果霸"] = 16; dictionary.Add("杨枝甘露", 19); dictionary.Add("满杯烧仙草", 21); foreach (var VARIABLE in dictionary) { Console.WriteLine(VARIABLE.Key + " 价格:" + VARIABLE.Value); } 输出: 芋泥波波 价格:16 黄桃果霸 价格:16 杨枝甘露 价格:19 满杯烧仙草 价格:21
可以看出IDictionary和SortedList的方法用法都大致相同,虽然它们提供的方法有些相似,但它们在实现上有所不同,比如 IDictionary 使用哈希表进行实现,而 SortedList 则使用了基于数组排序的实现。因此在某些场景下,它们的运行效率和行为可能有所不同。
这里列举一下IDictionary和SortedList方法上的差异
- 添加方法
IDictionary 提供了 Add(TKey, TValue) 方法来添加一个键值对。如果已经存在一个具有相同键的元素,则会抛出异常。
SortedList 提供了 Add(TKey, TValue) 方法来添加一个键值对。如果键已经存在,则会用新值替换旧值。
- 删除方法
IDictionary 提供了 Remove(TKey) 方法来移除一个键值对。如果不存在具有指定键的元素,则不会进行任何操作。
SortedList 提供了 Remove(TKey) 方法来移除具有指定键的键值对。
- 更新方法
IDictionary 提供了一种索引器,允许使用键指定元素来更新值。如果指定键不存在,则将添加新元素。
SortedList 提供了同样的索引器,以便根据键更新值。
- 访问方法
IDictionary 提供了 Keys 和 Values 属性,分别返回一个包含所有键和值的集合。
SortedList 提供了 Keys 和 Values 属性,分别返回一个按键或值进行排序的数组。
IDictionary和SortedList的区别
- 数据结构
IDictionary 是一种键值对集合,其中每个元素都是由一个键和一个值组成的。可以使用键来访问集合中的值。键在字典中需要是唯一的。
SortedList 基于一个排序列表实现,其中每个元素都是一个键值对。所有元素按照键进行排序,使得可以根据键进行快速的查找和访问。如同一个数组,键必须是唯一的。
- 访问
通过键访问 IDictionary 中的元素比通过索引访问 SortedList 中的元素要慢一些。因为需要使用哈希表来查找键值对。
SortedList 中的元素可以通过索引(其键在列表中的顺序)进行访问。因为元素已经按键排序,访问仅仅需要进行集合中的索引操作。
- 效率
IDictionary 的查找和修改操作通常比 SortedList 更快,因为 IDictionary 使用哈希表来快速查找(使用 Dictionary 类时)和修改。
SortedList 中的所有元素都必须按键进行排序,因此插入和删除操作通常比哈希表慢,但与数组相比较快。
- 内存使用
IDictionary 存储的数据结构通常使用哈希表,哈希表需要更多的内存用于存储哈希值和链表的指针。因此,IDictionary 需要比 SortedList 更多的内存。
SortedList 使用一个数组来存储元素,所以内存使用相对较少。
- 用途
IDictionary 适用于在集合中查找特定的值。每个键都映射到一个值,可以通过键来快速查找并获取值。它也可以被用来存储和管理任何需要一个唯一标识的对象集合。
SortedList 适用于需要按照键的顺序进行访问的集合。所有元素都按照键排序,允许按键顺序进行快速的访问。它也可用于需要实现一个简单的排序算法来排序对象集合的情况,比如一个按名称排序的集合。
ConcurrentDictionary
针对于Dictionary的线程非安全,官方提供了ConcurrentDictionary,ConcurrentDictionary是线程安全的,但如果只是进行读取操作推荐使用Dictionary,涉及到读写才使用ConcurrentDictionary ,因为ConcurrentDictionary的效率会更慢
ConcurrentDictionary<string, int> dict = new(); dict["杨枝甘露"] = 16; //尝试添加,字段如果存在就不做处理,也不会抛异常 dict.TryAdd("杨枝甘露", 19); Console.WriteLine(dict["杨枝甘露"]); //16
方法文档:
向字典添加新键(如果字典中尚不存在) |
如果字典中当前不存在键,此方法将添加指定的键/值对。 方法返回 true 或 false ,具体取决于是否添加了新对。 |
|
更新字典中现有键的值(如果该键具有特定值) |
此方法检查键是否具有指定的值,如果具有指定值,则使用新值更新密钥。 它类似于 CompareExchange 方法,只不过它用于字典元素。 |
|
无条件地将键/值对存储在字典中,并覆盖已存在的键的值 |
索引器资源库: dictionary[key] = newValue |
|
将键/值对添加到字典,或者如果键已存在,请根据键的现有值更新键的值 |
AddOrUpdate(TKey, Func<TKey,TValue>, Func<TKey,TValue,TValue>) - 或 - |
AddOrUpdate(TKey, Func<TKey,TValue>, Func<TKey,TValue,TValue>) 接受密钥和两个委托。 如果字典中不存在键,则使用第一个委托;它接受密钥并返回应为键添加的值。 如果键确实存在,则使用第二个委托;它接受键及其当前值,并返回应为键设置的新值。 AddOrUpdate(TKey, TValue, Func<TKey,TValue,TValue>) 接受键、要添加的值和更新委托。 这与上一个重载相同,只不过它不使用委托来添加键。 |
获取字典中某个键的值,将该值添加到字典中,如果该键不存在,则返回该值 |
- 或 - |
这些重载为字典中的键/值对提供延迟初始化,仅当值不存在时才添加该值。 GetOrAdd(TKey, TValue) 如果键不存在,则采用要添加的值。 GetOrAdd(TKey, Func<TKey,TValue>) 采用一个委托,该委托将在键不存在时生成值。 |
所有这些操作都是原子操作,对于 类上的 ConcurrentDictionary<TKey,TValue> 所有其他操作都是线程安全的。 唯一的例外是接受委托的方法,即 AddOrUpdate 和 GetOrAdd。 对于对字典的修改和写入操作, ConcurrentDictionary<TKey,TValue> 请使用精细锁定来确保线程安全。 (以无锁方式对字典执行读取操作。) 但是,这些方法的委托在锁外部调用,以避免在锁下执行未知代码时可能出现的问题。 因此,这些委托执行的代码不受操作原子性的约束。
如下是接送委托的线程非安全方法👇
//增加或修改,如果不存在,增加键值对的值为10,存在则修改为99 dict.AddOrUpdate("满杯烧仙草", 10,(k,v)=>{return 99;}); Console.WriteLine(dict["满杯烧仙草"]); //10 dict.AddOrUpdate("满杯烧仙草", 10,(k,v)=>{return 99;}); Console.WriteLine(dict["满杯烧仙草"]); //99 //如果存在则获取值,不存在就增加 dict.GetOrAdd("黄桃果霸", 17); Console.WriteLine(dict["黄桃果霸"]); //17 dict.GetOrAdd("黄桃果霸", 99); Console.WriteLine(dict["黄桃果霸"]); //17