C# 系列 -- 集合

333 阅读21分钟

集合

集合分类

C# 包括存储一系列值或对象的专用类,称为集合。 C# 中有三种类型的集合:非泛型集合,泛型集合和并发集合。 System.Collections 命名空间包含非泛型集合类型,System.Collections.Generic 命名空间包含泛型集合类型。 System.Collections.Concurrent 下面包含并发集合(都是泛型了)

在大多数情况下,建议使用泛型集合,因为它们比非泛型集合执行得更快,还可以得到强类型的好处。(打点,编译出错提示)

另外,在有并发多线程,Task的场景下可以使用 并发集合

非泛型集合

非泛型集合描述
ArrayListArrayList 存储任何类型的对象,如数组。但是,不需要像数组一样指定 ArrayList 的大小,因为它会自动增长。
HashtableHashtable 存储键值对。它通过比较键的哈希值来检索值。
SortedListSortedList 存储键值对。默认情况下,它会自动按键的升序排列元素。
QueueQueue以 FIFO 样式(先进先出)存储值。它保持添加值的顺序。它提供了一个 Enqueue() 方法来添加值和一个 Dequeue() 方法来从集合中检索值。 C# 包括泛型和非泛型队列。
StackStack 以 LIFO 样式(后进先出)存储值。它提供了 Push() 方法来添加值和 Pop() 和 Peek() 方法来检索值。 C# 包括泛型和非泛型堆栈。
BitArrayBitArray 管理一个紧凑的位值数组,这些值表示为布尔值,其中 true 表示该位打开 (1),false 表示该位关闭 (0)。

泛型集合

C# 在 System.Collections.Generic 命名空间中包含以下泛型集合类。

泛型集合描述
List泛型 List 包含指定类型的元素。 当您在其中添加元素时,它会自动增长。
Dictionary<TKey,TValue>Dictionary<TKey,TValue> 包含键值对。
SortedList<TKey,TValue>SortedList<TKey,TValue> 存储键值对。 默认情况下,它会自动按键的升序添加元素。
QueueQueue 以 FIFO 样式(先进先出)存储值。 它保持添加值的顺序。 它提供了一个 Enqueue() 方法来添加值和一个 Dequeue() 方法来从集合中检索值。
StackStack 将值存储为 LIFO(后进先出)。 它提供了 Push() 方法来添加值和 Pop() 和 Peek() 方法来检索值。
HashsetHashset 包含非重复元素。 它消除了重复的元素。

并发集合

并发集合描述
ConcurrentBag<List 的并发版本
ConcurrentDictionary<TKey,TValue>Dictionary<TKey,TValue> 的并发版本
ConcurrentQueueQueue 的并发版本
ConcurrentStackStack 的并发版本
BlockingCollection先进先出,主要用来做生产消费者模式,类似于 Goang channel
Channel先进先出,主要用来做生产消费者模式,跟 Goang channel更像了

列表 List

List 是一个强类型对象的集合,可以通过索引访问,并具有排序、搜索和修改列表的方法。 它是 System.Collection.Generic 命名空间下的 ArrayList 的泛型版本。 有了这个之后我们基本不在用ArrayList了。

特征

  • List 等效于 ArrayList
  • List 可以包含 指定类型 的元素。 它提供编译时类型检查并且不执行装箱拆箱,因为它是通用的。
  • 可以使用 Add()、AddRange() 方法或集合初始值设定项语法 添加元素
  • 可以通过传递索引来访问元素,例如 我的列表[0]。 索引从零开始
  • List 比 ArrayList 执行得 更快,更不容易出错

构造函数

构造函数描述
List()初始化 List 类的新实例,该实例为空并且具有默认初始容量。
List(IEnumerable)初始化 List 类的新实例,该实例包含从指定集合复制的元素并且具有足够的容量来容纳所复制的元素。
List(Int32)初始化 List 类的新实例,该实例为空并且具有指定的初始容量。

属性

属性描述
Capacity获取或设置该内部数据结构在不调整大小的情况下能够容纳的元素总数。
Count获取 List 中包含的元素数。
Item[Int32]获取或设置指定索引处的元素。

方法

方法功能描述
Add(T)将对象添加到 List 的结尾处。
AddRange(IEnumerable)将指定集合的元素添加到 List 的末尾。
AsReadOnly()返回当前集合的只读 ReadOnlyCollection 包装器。
BinarySearch(Int32, Int32, T, IComparer)使用指定的比较器在已排序 List 的某个元素范围中搜索元素,并返回该元素从零开始的索引。
BinarySearch(T)使用默认的比较器在整个已排序的 List 中搜索元素,并返回该元素从零开始的索引。
BinarySearch(T, IComparer)使用指定的比较器在整个已排序的 List 中搜索元素,并返回该元素从零开始的索引。
Clear()从 List 中移除所有元素。
Contains(T)确定某元素是否在 List 中。
ConvertAll(Converter<T,TOutput>)将当前 List 中的元素转换为另一种类型,并返回包含已转换元素的列表。
CopyTo(Int32, T[], Int32, Int32)从目标数组的指定索引处开始,将元素的范围从 List 复制到兼容的一维数组。
CopyTo(T[])从目标数组的开头开始,将整个 List 复制到兼容的一维数组。
CopyTo(T[], Int32)从目标数组的指定索引处开始,将整个 List 复制到兼容的一维数组。
Equals(Object)确定指定对象是否等于当前对象。(继承自 Object)
Find(Predicate)搜索与指定谓词所定义的条件相匹配的元素,并返回整个 List 中的第一个匹配元素。
FindAll(Predicate)检索与指定谓词定义的条件匹配的所有元素。
FindIndex(Int32, Int32, Predicate)搜索与指定谓词所定义的条件相匹配的一个元素,并返回 List 中从指定的索引开始、包含指定元素个数的元素范围内第一个匹配项的从零开始的索引。
FindIndex(Int32, Predicate)搜索与指定谓词所定义的条件相匹配的元素,并返回 List 中从指定索引到最后一个元素的元素范围内第一个匹配项的从零开始的索引。
FindIndex(Predicate)搜索与指定谓词所定义的条件相匹配的元素,并返回整个 List 中第一个匹配元素的从零开始的索引。
FindLast(Predicate)搜索与指定谓词所定义的条件相匹配的元素,并返回整个 List 中的最后一个匹配元素。
FindLastIndex(Int32, Int32, Predicate)搜索与指定谓词所定义的条件相匹配的元素,并返回 List 中包含指定元素个数、到指定索引结束的元素范围内最后一个匹配项的从零开始的索引。
FindLastIndex(Int32, Predicate)搜索与由指定谓词定义的条件相匹配的元素,并返回 List 中从第一个元素到指定索引的元素范围内最后一个匹配项的从零开始的索引。
FindLastIndex(Predicate)搜索与指定谓词所定义的条件相匹配的元素,并返回整个 List 中最后一个匹配元素的从零开始的索引。
ForEach(Action)对 List 的每个元素执行指定操作。
GetEnumerator()返回循环访问 List 的枚举数。
GetHashCode()作为默认哈希函数。(继承自 Object)
GetType()获取当前实例的 Type。(继承自 Object)
IndexOf(T, Int32)搜索指定对象并返回 List 中从指定索引到最后一个元素这部分元素中第一个匹配项的从零开始索引。
IndexOf(T, Int32, Int32)搜索指定对象并返回 List 中从指定索引开始并包含指定元素数的这部分元素中第一个匹配项的从零开始索引。
Insert(Int32, T)将元素插入 List 的指定索引处。
InsertRange(Int32, IEnumerable)将集合中的元素插入 List 的指定索引处。
LastIndexOf(T)搜索指定对象并返回整个 List 中最后一个匹配项的从零开始索引。
LastIndexOf(T, Int32)搜索指定对象并返回 List 中从第一个元素到指定索引这部分元素中最后一个匹配项的从零开始的索引。
LastIndexOf(T, Int32, Int32)搜索指定对象并返回 List 中到指定索引为止包含指定元素数的这部分元素中最后一个匹配项的从零开始索引。
MemberwiseClone()创建当前 Object 的浅表副本。(继承自 Object)
RemoveAll(Predicate)移除与指定的谓词所定义的条件相匹配的所有元素。
RemoveAt(Int32)移除 List 的指定索引处的元素。
RemoveRange(Int32, Int32)从 List 中移除一系列元素。
Reverse()将整个 List 中元素的顺序反转。
Reverse(Int32, Int32)将指定范围中元素的顺序反转。
Sort()使用默认比较器对整个 List 中的元素进行排序。
Sort(Comparison)使用指定的 Comparison,对整个 List 中的元素进行排序。
Sort(IComparer)使用指定的比较器对整个 List 中的元素进行排序。
Sort(Int32, Int32, IComparer)使用指定的比较器对 List 中某个范围内的元素进行排序。
ToArray()将 List 的元素复制到新数组中。
ToString()返回表示当前对象的字符串。(继承自 Object)
TrueForAll(Predicate)确定 List 中的每个元素是否都与指定谓词定义的条件匹配。

实践

创建一个 List

List<T> 是一个泛型集合,因此您需要为其可以存储的数据类型指定一个类型参数。 以下示例展示如何创建列表和添加元素。

static void Main(string[] args)
{
    var numberList = new List<int>();
    numberList.Add(1); // 通用 add添加元素
    numberList.Add(3);
    numberList.Add(5);
    numberList.Add(7);

    var cities = new List<string>();
    cities.Add("北京");
    cities.Add("广州");
    cities.Add("上海");
    cities.Add("厦门");
    cities.Add(null); // null是允许的

    // 使用 集合初始值设定项 语法来添加元素 
    var bigCities = new List<string>()
        {
            "北京",
            "广州",
            "上海",
            "厦门"
        };
}

在上面的例子中,我们使用 var numberList = new List<int>(); 来创建一个 int 类型的集合。 同样,我们也创建了cities 和 bigCities 都是字符串类型集合。另外,可以使用 Add() 方法或集合初始值设定项语法在列表中添加元素。

我们还可以使用集合初始值设定项语法添加自定义类的元素。 下面在 List 中添加 Student 类的对象。

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

    public string Name { get; set; }

}
class Program
{
    static void Main(string[] args)
    {
        var students = new List<Student>() 
        {
            new Student(){ Id = 1, Name="Ma"},
            new Student(){ Id = 2, Name="Le"},
            new Student(){ Id = 3, Name="Ma2"},
            new Student(){ Id = 4, Name="Hello"}
        };

    }
}

添加一组数据到 List 当中

使用 AddRange(IEnumerable<T> collection) 方法将数组或其他集合中的所有元素添加到 List。

static void Main(string[] args)
{
    var names = new List<string> { "baidu", "qq", "malema" };
    var popularNames = new List<string>();
    popularNames.AddRange(names);
}

访问 List 元素

可以通过索引、for/foreach 循环和使用 LINQ 查询访问List。 List的索引从零开始。 与数组相同可以通过索引器来访问单个元素。 也可以使用 foreach 或 for 循环迭代 List 集合。

var names = new List<string> { "baidu", "qq", "malema" };
var popularNames = new List<string>();
popularNames.AddRange(names);
Console.WriteLine(names[0]); //baidu
Console.WriteLine(names[1]); //qq
foreach (var item in popularNames)
{
    Console.WriteLine(item);
}
popularNames.ForEach(it => Console.WriteLine(it));

for (int i = 0; i < names.Count; i++)
    Console.WriteLine(names[i]); //省略了大括号,不推荐

使用 Linq 语法来检索

例子 1

static void Main(string[] args)
{
    var names = new List<string> { "baidu", "qq", "malema", "tencent", "alibaba" };

    var hasAList = names.Where(x => x.Contains("a"));
    //string.Join是可以把集合合拼起来。
    Console.WriteLine(string.Join(",", hasAList)); // baidu,malema,alibaba

    var query = from it in hasAList where it.Contains('a') select it; // 跟sql有类似的语法
    Console.WriteLine(string.Join(",", hasAList)); // baidu,malema,alibaba
}

例子 2

static void Main(string[] args)
{
    var students = new List<Student>()
    {
        new Student(){ Id = 1, Name="Ma"},
        new Student(){ Id = 2, Name="Le"},
        new Student(){ Id = 3, Name="Ma2"},
        new Student(){ Id = 4, Name="Hello"},
        new Student(){ Id = 5, Name="linq"}
    };

    var list = students.Where(it => it.Id > 3).Select(x => x.Name);
    Console.WriteLine(string.Join(",", list));  // Hello,linq
}

在任意位置插入一个新的元素

下面的代码就将 student 插入到了第一个 student Ma 之前了

static void Main(string[] args)
{
    var students = new List<Student>()
    {
        new Student(){ Id = 1, Name="Ma"},
        new Student(){ Id = 2, Name="Le"},
        new Student(){ Id = 3, Name="Ma2"},
        new Student(){ Id = 4, Name="Hello"},
        new Student(){ Id = 5, Name="linq"}
    };

    var student = new Student() { Name = "m-a-l-e-m-a" };
    students.Insert(0, student);
}

从 List 当中移除一个元素

我们可以使用RemoveAt(index) Remove(T item) 如下我们通过两种方式来移除元素

static void Main(string[] args)
{
    var students = new List<Student>()
    {
        new Student(){ Id = 1, Name="Ma"},
        new Student(){ Id = 2, Name="Le"},
        new Student(){ Id = 3, Name="Ma2"},
        new Student(){ Id = 4, Name="Hello"},
        new Student(){ Id = 5, Name="linq"}
    };

    students.RemoveAt(0); //移除第一个元素
    Console.WriteLine(string.Join(",", students.Select(x => x.Name)));//Le,Ma2,Hello,linq
    students.Remove(students[0]);// 再次移除第一个元素
    Console.WriteLine(string.Join(",", students.Select(x => x.Name)));//Ma2,Hello,linq 

    //先找出要移除的元素
    var linqStudent = students.FirstOrDefault(it => it.Name == "linq");
    students.Remove(linqStudent);
    Console.WriteLine(string.Join(",", students.Select(x => x.Name)));//Ma2,Hello
}

传统方法 移除所有符合条件的

static void Main(string[] args)
{
    var students = new List<Student>()
    {
        new Student(){ Id = 1, Name="Ma"},
        new Student(){ Id = 2, Name="Le"},
        new Student(){ Id = 3, Name="Ma2"},
        new Student(){ Id = 4, Name="Hello"},
        new Student(){ Id = 5, Name="linq"}
    };

    foreach (var item in students.ToList())
    {
        // 为什么要在students后面加上.ToList因为不加的话会报错
        // System.InvalidOperationException:“Collection was modified; enumeration operation may not execute.”
        if (item.Name == "linq")
        {
            students.Remove(item);
        }
    }
}

可以用 students.RemoveAll(it => it.Name == "linq"); 代替上面的移除代码

判断元素是否存在

  • 简单类型
var numbers = new List<int>() { 10, 20, 30, 40 };
numbers.Contains(10); // returns true
numbers.Contains(11); // returns false
numbers.Contains(20); // returns true
  • 复杂类型
static void Main(string[] args)
{
    var students = new List<Student>()
    {
        new Student(){ Id = 1, Name="Ma"},
        new Student(){ Id = 2, Name="Le"},
        new Student(){ Id = 3, Name="Ma2"},
        new Student(){ Id = 4, Name="Hello"},
        new Student(){ Id = 5, Name="linq"}
    };

    var has = students.Any(x => x.Name == "linq"); //true
}

有序列表 SortedList

特征

  • SortedList<TKey, TValue> 是按键排序的键值对数组。
  • 添加元素后立即对其进行排序。 根据 IComparer 按升序对原始类型键和对象键进行排序。
  • 必须是唯一的,不能为空。
  • 值可以为 重复
  • 可以通过在索引器 mySortedList[key] 中传递关联的键来访问值
  • 包含 KeyValuePair<TKey, TValue> 类型的元素
  • 它比 SortedDictionary<TKey,TValue> 使用更少的内存
  • 排序后的数据 检索速度更快,而 SortedDictionary<TKey, TValue> 在插入和删除键值对方面更快。

属性

属性描述
Capacity获取或设置排序列表的容量
Count获取包含在排序列表元素的数量
IsFixedSize获取一个值,指示排序列表是否具有固定大小
IsReadOnly获取一个值,指示排序列表是否为只读
Item获取和设置与SortedList中的特定键关联的值
Keys获取的排序列表的键
Values获取的排序列表(SortedList)中的值

方法

方法描述
Add( object key, object value )将带有指定键和值到排序列表的元素
Clear()将删除 SortedList 的所有元素
ContainsKey( object key )确定 SortedList 中是否包含一个特定的键
ContainsKey( object key )确定 SortedList 中是否包含一个特定的键
ContainsValue( object value )确定 SortedList 是否包含特定的值
GetByIndex( int index )获取 SortedList 中指定索引处的值
GetKey( int index )获取 SortedList 中指定索引处的键
GetKeyList()获取 SortedList 的键
GetValueList()获取 SortedList 中的值
IndexOfKey( object key )返回在排序列表中指定键从零开始的索引
IndexOfValue( object value )返回在 SortedList 中指定的值第一次出现的从零开始的索引
Remove( object key )删除从 SortedList 表中指定键的元素
RemoveAt( int index )删除 SortedList 中指定索引处的元素
TrimToSize()设置在 SortedList 元素的实际数

实践

创建一个 SortedList

以下示例演示如何创建泛型 SortedList<TKey, TValue>,并在其中添加键值对。

static void Main(string[] args)
{
    var sortedList = new SortedList<int, string>(); 
    sortedList.Add(22, "value22");
    sortedList.Add(11, "value11");
    sortedList.Add(2, "value2");
    sortedList.Add(1, "value1");
    sortedList.Add(13, "value13");

    Console.WriteLine(string.Join(",", sortedList.Select(it => it.Value)));
    // value1,value2,value11,value13,value22
    
    // 使用 foreach 循环 列出所有的 key 和 value
    foreach (var item in sortedList) // item 是 KeyValuePair<int, string> 类型 
    {
        Console.WriteLine($"{item.Key}={item.Value}");
    }
    // 1=value1
    // 2=value2
    // 11=value11
    // 13=value13
    // 22=value22
}

访问 SortedList

在索引器 sortedList[key] 中指定一个键,以获取或设置 SortedList 中的值。

static void Main(string[] args)
{
    var sortedList = new SortedList<int, string>()
    {
        [22] = "value22",
        [23] = "value23",
        [3] = "value3",
    };

    Console.WriteLine(sortedList[22]);//"value22"
    sortedList[3] = "value3 new"; // 更改了值
    sortedList[33] = "value33"; // 添加了一个新元素
    // Console.WriteLine(sortedList[65]);
    // 报错:System.Collections.Generic.KeyNotFoundException:“The given key '65' was not present in the dictionary.”

}

上面, sortedList[65] 将抛出 KeyNotFoundException 因为指定的键 65 在排序列表中不存在。 为防止出现此异常,请使用 ContainsKey() 或 TryGetValue() 方法,如下所示。

static void Main(string[] args)
{
    var sortedList = new SortedList<int, string>()
    {
        [22] = "value22",
        [23] = "value23",
        [3] = "value3",
    };

    if (sortedList.ContainsKey(65)) Console.WriteLine(sortedList[65]); // 不会执行到
    if (sortedList.TryGetValue(3, out string value)) Console.WriteLine(value); // value3
}

sortedList还支持Keys和Values。

static void Main(string[] args)
{
    var sortedList = new SortedList<int, string>()
    {
        [22] = "value22",
        [23] = "value23",
        [3] = "value3",
    };

    foreach (var item in sortedList.Keys) //.Values 也是可以输出所有的 Values
    {
        Console.Write(item + ",");
    }
    // 3,22,23,
}

从 SortedList 中移除元素

使用 Remove(key) 和 RemoveAt(index) 方法从 SortedList 中删除键值对。

static void Main(string[] args)
{
    var sortedList = new SortedList<int, string>()
    {
        [22] = "value22",
        [23] = "value23",
        [3] = "value3",
        [4] = "value4",
        [5] = "value5",
        [6] = "value5",
    };

    sortedList.RemoveAt(2); // 移掉了 [5] = "value5",
    sortedList.Remove(23); // 移掉了 [23] = "value23",
    Console.WriteLine(string.Join(",", sortedList.Keys)); // 3,4,6,22
}

字典 Dictionary

Dictionary<TKey, TValue> 是一个通用字典集合,它以没有特定顺序存储键值对。 (跟SortedList比它不排序)

特征

  • Dictionary<TKey, TValue> 存储键值对
  • 实现 IDictionary<TKey, TValue> 接口。
  • 必须是唯一的,不能为空。
  • 值可以为重复
  • 可以通过在索引器中传递关联的键来访问值,例如 我的字典[键]
  • 元素存储为 KeyValuePair<TKey, TValue> 对象。
  • 内部使用 hash 算法

属性

属性描述
Keys字典键集合
Values字典值集合
Count字典内元素数量

方法

属性描述
Add()新增字典元素,当字典键存在时,抛出异常
TryAdd()尝试将指定的键和值添加到字典中,字典键存在时返回false表示新增失败且不会抛出异常
Remove()移除字典集合内指定键数据
Reverse()将字典集合反序排序
ContainsKey()搜索字典键是否存在于字典集合中,存在返回true不存在返回false
ContainsValue()搜索字典值是否存在于字典集合中,存在返回true不存在返回false
First()获取字典集合内第一个键、值元素;若字典集合长度为0会抛出异常Sequence contains no matching element,支持lambda表达式查询
FirstOrDefault()获取字典集合内第一个键、值元素;若字典集合长度为0不抛出异常并且返回null值,支持lambda表达式查询
Where()按条件搜索字典集合中符合条件的结果集,支持lambda表达式查询
ElementAt()获取字典集合中指定下标键、值元素
Except()与另一个同类型结构的字典结果集比较并获取两个字典结果集之间的差集
Intersect()与另一个同类型结构的字典结果集比较并获取两个字典结果集之间的交集
Union()与另一个同类型结构的字典结果集比较并获取两个字典结果集之间的并集

Dictionary常用的属性、方法已经列举完毕,接下来附上常用属性、方法的使用代码供大家参考。

实践

创建 Dictionary

您可以通过传递符合要求的键值对来创建 Dictionary<TKey, TValue> 对象。 以下示例显示如何创建字典并添加键值对

static void Main(string[] args)
{
    var dict0 = new Dictionary<int, string>()
    {
        [1] = "a",
        [2] = "b"
    };

    // C#6.0 之后 提供的 Index Initializers 索引初始化器
    var dict1 = new Dictionary<string, string>()
    {
        ["xm"] = "厦门",
        ["bj"] = "北京",
        ["sh"] = "上海",
    };

    var dict2 = new Dictionary<string, string>()
    {
        { "xm" ,"厦门"},
        { "bj" ,"北京"},
        { "sh" ,"上海" },
    };

    var dict3 = new Dictionary<string, string>();
    dict3.Add("xm", "厦门");
    dict3.Add("bj", "北京");
    dict3.Add("sh", "上海");

    foreach (var kvp in dict3)
        Console.WriteLine("Key: {0}, Value: {1}", kvp.Key, kvp.Value);

    // Key: xm, Value: 厦门
    // Key: bj, Value: 北京
    // Key: sh, Value: 上海
}

在上面的例子中,dict0 是一个 Dictionary<int, string> 类型的字典,所以它可以存储 int 键和字符串值。 同样的,dict1,dict2,dict3是一个 Dictionary<string, string> 类型的字典,所以它可以存储字符串键和字符串值。 字典不能包含重复键或空键,而值可以是重复键或空键。 键必须是唯一的,否则会抛出运行时异常。

访问 Dictionary

我们可以使用索引器访问字典。 通过一个Key来获取相关联的值。 也还可以使用 ElementAt() 方法从指定的索引中获取 KeyValuePair。

static void Main(string[] args)
{
    var dict1 = new Dictionary<string, string>()
    {
        ["xm"] = "厦门",
        ["bj"] = "北京",
        ["sh"] = "上海",
    };

    Console.WriteLine(dict1["xm"]);  // 输出 厦门
    Console.WriteLine(dict1["bj"]);  // 输出 北京
    // Console.WriteLine(dict1["malema"]);
    // 报错:System.Collections.Generic.KeyNotFoundException

    if (dict1.ContainsKey(".net"))
    {
        // 没有这个 key 不会输出
        Console.WriteLine(dict1[".net"]);
    }

    // use TryGetValue() to get a value of unknown key
    string result;

    if (dict1.TryGetValue("xm", out result))
    {
        Console.WriteLine(result);//输出 厦门
    }

    for (int i = 0; i < dict1.Count; i++)
    {
        Console.WriteLine($"Key: {dict1.ElementAt(i).Key}, Value: { dict1.ElementAt(i).Value}");
    }
    // for循环输出下面三个
    // Key: xm, Value: 厦门
    // Key: bj, Value: 北京
    // Key: sh, Value: 上海
}

更新 Dictionary

通过索引器来更新值,如果这个key 不存在的话则会添加一个新的。 (注意 如果用Add来添加的话要先判断有没有存,不然会出错,通过这种方式来添加的话则不需要判断)

static void Main(string[] args)
{
    var dict1 = new Dictionary<string, string>()
    {
        ["xm"] = "厦门",
        ["bj"] = "北京",
        ["sh"] = "上海",
    };

    dict1["cn"] = "中国";
    dict1["xm"] = "厦门2021";

    Console.WriteLine(string.Join(",", dict1.Keys));
    // xm,bj,sh,cn
}

从 Dictionary 里删除元素

  • 使用 Remove() 方法清除某个元素
  • 使用 Remove() 方法清空整个字典
static void Main(string[] args)
{
    var dict1 = new Dictionary<string, string>()
    {
        ["xm"] = "厦门",
        ["bj"] = "北京",
        ["sh"] = "上海",
    };

    dict1.Remove("xm");
    dict1.Remove("xm2"); //.net5.0 没有这个key也不会出错

    Console.WriteLine(string.Join(",", dict1.Keys));
    // bj,sh

    dict1.Clear(); //清掉所有
    Console.WriteLine(string.Join(",", dict1.Keys));
    //没东西了
}

把条件判断转化成:查表

如下的代码。我们需要把code转成名字, 我们可以用if else 或者 switch来写这个代码。 但是更方便的则是我们创建一个Dictionary然后直接查表。

private static string GetNameByCode(string code)
{

    switch (code)
    {
        case "xm":
            return "厦门";

        case "bj":
            return "北京";

        case "sh":
            return "上海";
    }
    throw new ArgumentOutOfRangeException("找不到这个Key");
}

static void Main(string[] args)
{
    var name = GetNameByCode("xm");

    var dict1 = new Dictionary<string, string>()
    {
        ["xm"] = "厦门",
        ["bj"] = "北京",
        ["sh"] = "上海",
    };

    var name2 = dict1["xm"];

}

其它复杂的操作也是可以类似上面转成查表的。只是把value部分变成 Action<> 或者 Func<>

栈 Stack

栈(stack)又名堆栈,它是一种运算受限的线性表。限定仅在表尾进行插入和删除操作的线性表。这一端被称为栈顶,相对地,把另一端称为栈底。 向一个栈插入新元素又称作进栈、入栈或压栈(push),它是把新元素放到栈顶元素的上面,使之成为新的栈顶元素; 从一个栈删除元素又称作出栈或退栈(pop),它是把栈顶元素删除掉,使其相邻的元素成为新的栈顶元素。 它这种存储方式被称为 LIFO (Last in first out)(后进先出,选进后出) C# 包括泛型 Stack 和非泛型 Stack 集合类。 建议使用泛型 Stack 集合。

特征

  • Stack 是后进先出集合。
  • 它位于 System.Collection.Generic 命名空间下。
  • Stack 可以包含指定类型的元素。 它提供编译时类型检查并且不执行装箱拆箱,因为它是泛型。
  • 可以使用 Push() 方法添加元素。 不能使用集合初始值设定项语法。
  • 可以使用 Pop() 和 Peek() 方法检索元素。 它不支持索引器。

属性

属性类型描述
Countint获取 Stack<T> 中包含的元素数。

方法

方法返回类型描述
Clear()void从 Stack<T> 中移除所有对象。
Contains(T item)bool确定某个元素是否在 Stack<T> 中。
GetEnumerator()Enumerator<T>返回循环访问 Stack<T> 的枚举数。
Peek()T返回位于 Stack<T> 顶部的对象但不将其移除。
Pop()T移除并返回位于 Stack<T> 顶部的对象。
Push(T item)void将一个对象插入到 Stack<T> 的顶部。
ToArray()T[]将 Stack<T> 复制到一个新数组中。

实践

创建一个堆栈 Stack

您可以通过为 Stack 可以存储的元素类型指定类型参数来创建 Stack 的对象。 以下示例使用 Push() 方法在 Stack 中创建和添加元素。 堆栈允许null(对于引用类型)和重复值。

static void Main(string[] args)
{
    var myStack = new Stack<int>();
    myStack.Push(1);
    myStack.Push(2);
    myStack.Push(3);
    myStack.Push(4);

    foreach (var item in myStack)
        Console.Write(item + ","); //输出 4,3,2,1, 
}

也可以通过数组来创建一个堆栈

static void Main(string[] args)
{
    int[] arr = new int[] { 1, 2, 3, 4 };
    Stack<int> myStack = new Stack<int>(arr);

    foreach (var item in myStack)
        Console.Write(item + ","); //输出 4,3,2,1, 
}

contains pop peek 例子

static void Main(string[] args)
{
    var myStack = new Stack<int>();
    myStack.Push(1);
    myStack.Push(2);
    myStack.Push(3);
    myStack.Push(4);

    Console.WriteLine("包含3" + myStack.Contains(3));//包含3True
    var first = myStack.Pop(); //4
    var second = myStack.Pop();//3
    Console.WriteLine($"第一个:{first},第二个:{second}");
    Console.WriteLine("包含3" + myStack.Contains(3));//包含3False
    Console.WriteLine(myStack.Count());//2
    var third = myStack.Peek();
    Console.WriteLine($"第三个:{third}"); //第三个: 2
    Console.WriteLine(myStack.Count());//2  说明peek没有改变数量 
}

队列 Queue

Queue 是一种特殊类型的集合,它以 FIFO (先进先出)的方式来存储元素,与 堆栈 Stack 完全相反。 C# 包括通用 Queue 和非通用 Queue 集合。 建议使用通用 Queue 集合。

特征

  • Queue 是 FIFO(先进先出)集合。
  • 它位于 System.Collection.Generic 命名空间下。
  • Queue 可以包含指定类型的元素。 它提供编译时类型检查并且不执行装箱拆箱,因为它是泛型。
  • 可以使用 Enqueue() 方法添加元素。 不能使用集合初始值设定项语法。
  • 可以使用 Dequeue() 和 Peek() 方法检索元素。 它不支持索引器。

属性

属性描述
Count获取 Queue 中包含的元素个数。

方法

方法返回类型描述
Clear()void从 Queue<T> 中移除所有对象。
Contains(T item)bool确定某个元素是否在 Queue<T> 中。
CopyTo(T[] array, int arrayIndex)void将 Queue<T> 中的元素复制到现有一维数组中,从指定的数组索引开始。
Dequeue()T移除并返回 Queue<T> 开始处的对象。
Enqueue(T item)void将一个对象添加到 Queue<T> 的结尾。
GetEnumerator()Enumerator<T>返回循环访问 Queue<T> 的枚举数。
ToArray()T[]将 Queue<T> 复制到一个新数组中。
TryDequeue(out T result)bool移除并返回 Queue<T> 开始处的对象。如果操作成功,则返回 true,否则返回 false
TryPeek(out T result)bool返回位于 Queue<T> 开始处的对象但不将其移除。如果操作成功,则返回 true,否则返回 false

实践

创建队列

我们可以通过指定类型参数来创建 Queue 的对象。 以下示例使用 Enqueue() 方法在 Queue 中创建和添加元素。 Queue 集合允许 null(对于引用类型)和重复值。

static void Main(string[] args)
{
    var myQueue = new Queue<int>();
    myQueue.Enqueue(1);
    myQueue.Enqueue(2);
    myQueue.Enqueue(3);
    myQueue.Enqueue(4);

    foreach (var id in myQueue)
        Console.Write(id); //prints 1234
}

示例

static void Main(string[] args)
{
    var myQueue = new Queue<int>(2);
    myQueue.Enqueue(1);
    myQueue.Enqueue(2);
    myQueue.Enqueue(3);
    myQueue.Enqueue(4);

    var first = myQueue.Dequeue(); //1
    var has = myQueue.Contains(2); // true;
    
    foreach (var id in myQueue)
        Console.Write(id); //prints 234
    var second = myQueue.Peek(); //2 没有移除元素
}

元组 Tuple

Tuple 类是在 .NET Framework 4.0 中引入的。 元组是一种数据结构,其中包含一系列不同数据类型的元素。 如果我想用一个临时的结构来传递数据,又不想创建一个新类的话,就可以用它了。

(ValueTuple)的使用会比这个更加方便,在下一节会介绍到它。

Tuple<T1, T2, T3, T4, T5, T6, T7, TRest>

以下示例创建一个包含三个元素的元组:

var person = new Tuple<int, string, string>(1, "malema", ".net");

在上面的例子中,我们创建了一个保存个人记录的元组实例。 我们为每个元素指定了一个类型并将值传递给构造函数。 指定每个元素的类型参数是比较麻烦的。 Tuple里面有一个静态泛型方法Create(), 使用它的话,编译器会自动进行类型推断,如下。

var person = Tuple.Create(1, "malema", ".net");

一个元组 最多只能包含八个 元素。 当您尝试包含八个以上的元素时,它会给出编译器错误。

var person = Tuple.Create(1, 2, 3, 4, 5, 6, 7, 8, 9);
//有错 “Create”方法没有采用 9 个参数的重载

属性

访问元组元素

可以使用Item<数字> 属性访问元组元素,例如,Item1、Item2、Item3 等等,直到 Item7 属性。 Item1 属性返回第一个元素,Item2 返回第二个元素,依此类推。 最后一个元素(第 8 个元素)将使用 Rest 属性返回。

static void Main(string[] args)
{
    var numbers = Tuple.Create("一", 2, 3, "四", 5, "六", 7, 8);

    Console.WriteLine(numbers.Item1); // 输出 "一"
    Console.WriteLine(numbers.Item2); // 输出 2
    Console.WriteLine(numbers.Item3); // 输出 3
    Console.WriteLine(numbers.Item4); // 输出 "四"
    Console.WriteLine(numbers.Item5); // 输出 5
    Console.WriteLine(numbers.Item6); // 输出 "六"
    Console.WriteLine(numbers.Item7); // 输出 7
    Console.WriteLine(numbers.Rest); // 输出 (8)
    Console.WriteLine(numbers.Rest.Item1); // 输出 8 //注意还要加一个Item1
}

通常,第 8 个位置用于嵌套元组,您可以使用 Rest 属性访问它。

嵌套元组

如果要在一个元组中包含八个以上的元素,可以通过嵌套另一个元组对象作为第八个元素来实现。 可以使用 Rest 属性访问最后一个嵌套的元组。 要访问嵌套元组的元素,请使用 Rest.Item1.Item<数字> 属性。

static void Main(string[] args)
{
    var numbers = Tuple.Create(1, 2, 3, 4, 5, 6, 7, Tuple.Create(8, 9, 10, 11, 12, 13));
    Console.WriteLine(numbers.Item1); // 输出 1
    Console.WriteLine(numbers.Item7); // 输出 7
    Console.WriteLine(numbers.Rest.Item1); //输出 (8, 9, 10, 11, 12, 13)
    Console.WriteLine(numbers.Rest.Item1.Item1); //输出 8
    Console.WriteLine(numbers.Rest.Item1.Item2); //输出 9
}

虽然推荐是在最后一个放入嵌套元素,但是实际上我们可以在任意位置放入嵌套元组。如下

var numbers = Tuple.Create(1, 2, Tuple.Create(3, 4, 5, 6, 7,  8), 9, 10, 11, 12, 13 );

(当需要这么多元素的时候,我们最好还是定义一个类,这样的代码可读性才更好)

实践

元组做为参数

static void Main(string[] args)
{
    var person = Tuple.Create(1, "Malema", ".net");
    DisplayTuple(person);
}

static void DisplayTuple(Tuple<int, string, string> person)
{
    Console.WriteLine($"Id = { person.Item1}");
    Console.WriteLine($"First Name = { person.Item2}");
    Console.WriteLine($"Last Name = { person.Item3}");
}

元组做为返回值

static void Main(string[] args)
{
    var person = GetPerson();
}

static Tuple<int, string, string> GetPerson() 
{
    return Tuple.Create(1, "Bill", "Gates");
}

使用场景

  • 当您想从一个方法返回多个值而不使用 ref 或 out 参数时。
  • 当您想通过单个参数将多个值传递给方法时。
  • 当您想临时保存数据库记录或某些值而不创建单独的类时。

限制

  • 元组是引用类型而不是值类型。 它在堆上分配并可能导致 CPU 密集型操作。
  • 元组仅限于包含八个元素。 如果需要存储更多元素,则需要使用嵌套元组。 但是,这可能会导致歧义。
  • 可以使用名称模式为 Item 的属性访问元组元素,可读性不好。

元组 ValueTuple

优点

优点 1

可以使用括号 () 并指定其中的值来创建和初始化它。

var person = (1, "Malema", ".net");
//等价于下面这种.
var person = ValueTuple.Create(1, "Malema", ".net"); //注意不是 Tuple.Create

优点 2

跟 Tuple 不一样 ValueTuple 可以包含8个以上的元素

var numbers = (1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14);

优点 3

可以为 ValueTuple 属性指定名称,而不是使用默认的属性名称,如 Item1、Item2 等。

static void Main(string[] args)
{
    var (id, firstName, lastName) = (1, "Malema", ".net"); // 最简写的方式
    // (int id, string firstName, string lastName) = (1, "Malema", ".net"); // 可为每个成员规定类型
    // (var id, var firstName, var lastName) = (1, "Malema", ".net"); // 不确定类型就写 var
    Console.WriteLine(id); // 1
    Console.WriteLine(firstName); // Malema
    Console.WriteLine(lastName); // .net
}

我们还可以为右侧的成员名称分配值,如下所示。

static void Main(string[] args)
{
    var person = (Id: 1, FirstName: "Malema", LastName: ".net");
    Console.WriteLine(person.Id); // 1
    Console.WriteLine(person.FirstName); // Malema
    Console.WriteLine(person.LastName); // .net
}

实践

元组作为参数传递

static void Main(string[] args)
{
    DisplayTuple((1, "Bill", "Gates"));
}

static void DisplayTuple((int id, string firstName, string lastName) person)
{
    Console.WriteLine($"Id = {person.id}"); // Id = 1
    Console.WriteLine($"First Name = { person.firstName}"); // First Name = Bill
    Console.WriteLine($"Last Name = { person.lastName}"); // Last Name = Gates
}

元组作为返回值

static void Main(string[] args)
{
    var person = GetPerson();
}

//在这边定义上成员名字。这样调用的地方也可以很方便的使用,打点的话会有智能提示
static (int Id, string FirstName, string LastName) GetPerson()
{
    return (1, "Bill", "Gates");
}

解构取出元组中的成员

我们可以通过解构,直接取出元组中的成员。 上面的代码可以变成如下的形式

static void Main(string[] args)
{
    (int id, string firstName, string lastName) = GetPerson();
}

static (int Id, string FirstName, string LastName) GetPerson()
{
    return (1, "Bill", "Gates");
}

当我们不需要某个成员的时候我们可以使用 _ 称为弃元discards

class Program
{
    static void Main(string[] args)
    {
        (int id, string firstName, _) = GetPerson();
    }

    static (int Id, string FirstName, string LastName) GetPerson()
    {
        return (1, "Bill", "Gates");
    }
}

另外,类也是可以进行解构的。如下

class Program
{
    static void Main(string[] args)
    {

        (int id, string firstName, string lastName) = GetPerson();
    }

    static Person GetPerson()
    {
        return new Person();
    }


    class Person
    {
        public void Deconstruct(out int id, out string firstName, out string lastName)
        {
            id = 3;
            firstName = "malema";
            lastName = ".net";
        }
    }
}