C#集合——详解Dictionary<K, V>集合

2 篇文章 0 订阅

目录

一、概述

二、对于自定义key类型判断是否相等的问题

1、IEqualityComparer接口

三、Dictionary的内部结构和容量

四、常用构造函数

五、常用属性

六、常用方法

七、示例代码

八、线程安全


一、概述

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>类。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

木林森先生

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值