目录
一、概述
Dictionary<K, V>是表示一种键值对的集合。键值对中,key不可重复。Key不可为null,但Value可以为null。内部使用哈希索引进行存储和检索!
Dictionary<K, V>泛型类提供了一组键到一组值的映射。每次添加到Dictionary中的元素都包含一个值和与其相关联的键。使用键检索值是非常快的,时间复杂度接近O(1)。而Dictionary<K, V>类以HashTable的形式实现,正因为如此,在使用键检索的时候,速度非常快。
只要一个对象在Dictionary<K, V>中被用作key,那么它的哈希值就不能以任何的方式进行更改!Dictionary<K, V>中的每个key在比较时,必须是唯一的。key不能为空,但value可以为空,如果value类型是引用类型。
二、对于自定义key类型判断是否相等的问题
1、IEqualityComparer<T>接口
Dictionary<K, V>集合在向其中插入新的键值对时,需要先判断集合中是否已经存在该key,这就要求在创建集合时,需要使用带有comparer参数的构造函数,这个comparer参数是一种实现了IEqualityComparer<T>接口的对象。如果key是int、double、float、string这样的数据类型,可以不使用带comparer参数的构造函数。
1、对于实现IComparable<T>接口,和重写Equals(Object obj)方法,等两种方式,本人已经测试过,均无法保证Key的唯一性。因为Key可以被继承,并且它们的行为可以改变,所以不能通过使用Equals方法进行比较来保证它们的绝对唯一性。
2、在向集合中添加,已经存在相同key的 键值对时,会报错:System.ArgumentException:“已添加了具有相同键的项。”
关于IEqualityComparer<T>具体代码,见示例:
using System;
using System.Collections.Generic;
static class Example
{
static void Main()
{
BoxEqualityComparer comparer = new();
Dictionary<Box, string> boxes = new(comparer);
AddBox(new Box(4, 3, 4), "red");
AddBox(new Box(4, 3, 4), "blue");
AddBox(new Box(3, 4, 3), "green");
Console.WriteLine($"The dictionary contains {boxes.Count} Box objects.");
void AddBox(Box box, string name)
{
try
{
//在添加已存在key的时候,会报错,所以这里使用try-catch
boxes.Add(box, name);
}
catch (ArgumentException e)
{
Console.WriteLine($"Unable to add {box}: {e.Message}");
}
}
}
}
class Box
{
public int Height { get; }
public int Length { get; }
public int Width { get; }
public Box(int height, int length, int width)
{
Height = height;
Length = length;
Width = width;
}
public override string ToString() => $"({Height}, {Length}, {Width})";
}
class BoxEqualityComparer : IEqualityComparer<Box>
{
public bool Equals(Box b1, Box b2)
{
if (ReferenceEquals(b1, b2))
return true;
if (b2 is null || b1 is null)
return false;
return b1.Height == b2.Height
&& b1.Length == b2.Length
&& b1.Width == b2.Width;
}
public int GetHashCode(Box box) => box.Height ^ box.Length ^ box.Width;
}
// The example displays the following output:
// Unable to add (4, 3, 4): An item with the same key has already been added.
// The dictionary contains 2 Box objects.
三、Dictionary的内部结构和容量
Dictionary<K, V>内部是通过一个Entry类型的数组来保存键值对的。而Entry类型是一个结构体变量类型,内部有四个变量,分别为hashCode、next、key、value。
Dictionary<K, V>的容量是Dictionary<K, V>所能容纳的元素个数。当向Dictionary<K, V>添加元素时,通过重新分配内部数组,容量会根据需要自动增加。
对于非常大的Dictionary<K, V>对象,通过在运行时环境中将<gcAllowVeryLargeObjects>配置元素的enabled属性设置为true,可以将64位系统上的最大容量增加到20亿个元素。实际项目有这么多元素的可能不大!
可以使用foreach语句来枚举Dictionary集合中的每个元素。由于Dictionary<T, K>是键和值的集合,因此元素类型不可以简单的认为是key的类型或value的类型。相反,元素类型是KeyValuePair<K, V>,表示一个键值对组成的组合,这也为了能够枚举Dictionary中的每一个键值对。枚举时返回项的顺序不明确,也就是返回项的顺序与写入的顺序不一定相同。使用foreach语句来枚举Dictionary集合的实例代码如下,myDictionary是Dictionary<T, K>类型的集合。
foreach( KeyValuePair<string, string> kvp in myDictionary )
{
Console.WriteLine("Key = {0}, Value = {1}", kvp.Key, kvp.Value);
}
四、常用构造函数
(1)、Dictionary<K, V>():初始化Dictionary<K, V>类的新实例,该实例为空,具有默认的初始容量,并对键类型使用默认的相等比较器。
(2)、Dictionary<K, V>(IDictionary<K, V>):初始化Dictionary<K, V>类的新实例,从另一个Dictionary<K, V>实例中复制所有元素到新创建的Dictionary<K, V>实例,并对TKey类型使用默认的相等比较器。
(3)、Dictionary<K, V>(IDictionary<K, V>, IEqualityComparer<TKey>):初始化Dictionary<K, V>类的新实例,该实例包含从指定的Dictionary<K, V>集合中复制的元素,并使用指定的IEqualityComparer<T>。
(4)、Dictionary<K, V>(IEqualityComparer<TKey>):初始化Dictionary<K, V>类的新实例,该实例为空,具有默认初始容量,并使用指定的IEqualityComparer<T>。
(5)、Dictionary<K, V>(Int32):初始化Dictionary<K, V>类的新实例,该实例为空,具有指定的初始容量,并对键类型使用默认的相等比较器。
(6)、Dictionary<K, V>(Int32, IEqualityComparer<TKey>):初始化Dictionary<K, V>类的新实例,该实例为空,具有指定的初始容量,并对键类型使用指定的IEqualityComparer<T>。
(7)、Dictionary<K, V>(SerializationInfo, StreamingContext):用序列化的数据初始化Dictionary<K, V>类的新实例。
五、常用属性
(1)、Comparer:获取用于确定Dictionary的键是否相等的IEqualityComparer<T>。
(2)、Count:获取Dictionary<TKey,TValue>中包含的键/值对的个数。
(3)、Item[TKey]:获取或设置与指定键关联的值。
(4)、Keys:获取一个集合,其中包含Dictionary<TKey,TValue>中的键。
(5)、Values:获取一个集合,其中包含Dictionary<TKey,TValue>中的值。
六、常用方法
(1)、Add(K, V):将指定的键和值添加到字典中。
(2)、Clear():从字典中删除所有的键和值<K, V>。
(3)、ContainsKey(K):判断Dictionary<K, V>是否包含指定的键。
(4)、ContainsValue(V):判断Dictionary<K, V>是否包含特定值。
(5)、GetEnumerator():返回一个枚举数,遍历Dictionary<K, V>。
(6)、GetObjectData(SerializationInfo, StreamingContext):实现可序列化接口并返回序列化Dictionary<K, V>实例所需的数据。
(7)、OnDeserialization(Object):实现ISerializable接口,并在反序列化完成时引发反序列化事件。
(8)、Remove(K):从Dictionary<K, V>中删除具有指定键的值。
(9)、TryGetValue(K, V):获取与指定键关联的值。
七、示例代码
下面的代码示例创建一个包含key为字符串类型,value为字符串类型的空Dictionary<TKey,TValue>,并使用Add方法添加一些元素。该示例演示了Add方法在尝试添加重复键时抛出ArgumentException。
该示例使用Item[]属性(c#中的索引器)来检索值,演示了当请求的键不存在时抛出KeyNotFoundException,并显示与键关联的值可以被替换。
这个例子展示了一个程序如果经常必须尝试Dictionary中没有的key值,如何使用TryGetValue()方法作为一种更有效的方法来检索值。它还展示了如何在调用Add()方法之前使用ContainsKey方法来测试一个键是否存在。
该示例展示了如何枚举Dictionary中的键和值,以及如何使用keys属性和values属性单独枚举键和值。
// Create a new dictionary of strings, with string keys.
//
Dictionary<string, string> openWith =
new Dictionary<string, string>();
// Add some elements to the dictionary. There are no
// duplicate keys, but some of the values are duplicates.
openWith.Add("txt", "notepad.exe");
openWith.Add("bmp", "paint.exe");
openWith.Add("dib", "paint.exe");
openWith.Add("rtf", "wordpad.exe");
// The Add method throws an exception if the new key is
// already in the dictionary.
try
{
openWith.Add("txt", "winword.exe");
}
catch (ArgumentException)
{
Console.WriteLine("An element with Key = \"txt\" already exists.");
}
// The Item property is another name for the indexer, so you
// can omit its name when accessing elements.
Console.WriteLine("For key = \"rtf\", value = {0}.",
openWith["rtf"]);
// The indexer can be used to change the value associated
// with a key.
openWith["rtf"] = "winword.exe";
Console.WriteLine("For key = \"rtf\", value = {0}.",
openWith["rtf"]);
// If a key does not exist, setting the indexer for that key
// adds a new key/value pair.
openWith["doc"] = "winword.exe";
// The indexer throws an exception if the requested key is
// not in the dictionary.
try
{
Console.WriteLine("For key = \"tif\", value = {0}.",
openWith["tif"]);
}
catch (KeyNotFoundException)
{
Console.WriteLine("Key = \"tif\" is not found.");
}
// When a program often has to try keys that turn out not to
// be in the dictionary, TryGetValue can be a more efficient
// way to retrieve values.
string value = "";
if (openWith.TryGetValue("tif", out value))
{
Console.WriteLine("For key = \"tif\", value = {0}.", value);
}
else
{
Console.WriteLine("Key = \"tif\" is not found.");
}
// ContainsKey can be used to test keys before inserting
// them.
if (!openWith.ContainsKey("ht"))
{
openWith.Add("ht", "hypertrm.exe");
Console.WriteLine("Value added for key = \"ht\": {0}",
openWith["ht"]);
}
// When you use foreach to enumerate dictionary elements,
// the elements are retrieved as KeyValuePair objects.
Console.WriteLine();
foreach( KeyValuePair<string, string> kvp in openWith )
{
Console.WriteLine("Key = {0}, Value = {1}",
kvp.Key, kvp.Value);
}
// To get the values alone, use the Values property.
Dictionary<string, string>.ValueCollection valueColl =
openWith.Values;
// The elements of the ValueCollection are strongly typed
// with the type that was specified for dictionary values.
Console.WriteLine();
foreach( string s in valueColl )
{
Console.WriteLine("Value = {0}", s);
}
// To get the keys alone, use the Keys property.
Dictionary<string, string>.KeyCollection keyColl =
openWith.Keys;
// The elements of the KeyCollection are strongly typed
// with the type that was specified for dictionary keys.
Console.WriteLine();
foreach( string s in keyColl )
{
Console.WriteLine("Key = {0}", s);
}
// Use the Remove method to remove a key/value pair.
Console.WriteLine("\nRemove(\"doc\")");
openWith.Remove("doc");
if (!openWith.ContainsKey("doc"))
{
Console.WriteLine("Key \"doc\" is not found.");
}
/* This code example produces the following output:
An element with Key = "txt" already exists.
For key = "rtf", value = wordpad.exe.
For key = "rtf", value = winword.exe.
Key = "tif" is not found.
Key = "tif" is not found.
Value added for key = "ht": hypertrm.exe
Key = txt, Value = notepad.exe
Key = bmp, Value = paint.exe
Key = dib, Value = paint.exe
Key = rtf, Value = winword.exe
Key = doc, Value = winword.exe
Key = ht, Value = hypertrm.exe
Value = notepad.exe
Value = paint.exe
Value = paint.exe
Value = winword.exe
Value = winword.exe
Value = hypertrm.exe
Key = txt
Key = bmp
Key = dib
Key = rtf
Key = doc
Key = ht
Remove("doc")
Key "doc" is not found.
*/
八、线程安全
Dictionary<K, V>可以同时支持多个线程对其进行读操作,只要集合不被修改。即便如此,在集合中枚举本质上也不是线程安全的过程。在枚举与写访问竞争的极少数情况下,必须在整个枚举期间锁定集合。要允许多个线程访问集合进行读写,您必须实现自己的同步。
对于线程安全的替代方法,可以使用:ConcurrentDictionary<TKey,TValue>类或ImmutableDictionary<TKey,TValue>类。