C# 集合
一、集合接口和类型
大多数的集合类都可在 System.Clooections 和System.Collections.Generic名称空间中找到。泛型集合类位于 System.Collections.Specialized名称空间中。线程安全的集合类位于 System.Collections.Concurrent 名称空间中。
集合和列表实现的接口如下

二、列表
.NET Framework 为动态列表提供了泛型类 List<T>。这个类实现了 IList、ICollection、IEnumerable、IList<T>、ICollection<T> 和 IEnumerable<T> 接口。
1、创建列表
调用默认的构造函数,就可以创建列表对象。在泛型类中,必须为列表的值指定类型。
使用默认的构造函数创建一个空列表后,元素添加到列表后,列表的容量就会扩大为可接纳4个元素,如果添加了5个元素,就会,设置大小为8,还不够,就16。每次都会重新设置为原来的2倍。
如果创建时指定了大小,每次扩容也是原来的2倍。
使用 Capacity 可以获取和设置集合的容量。
Count属性 获取集合中元素个数。
如果需要去掉不需要的容量,可以用 TrimExcess() 方法。但内容超过容量的90%此方法就什么都不做了。
集合初始值设定项:后面加上 { } 设置
添加元素:Add( value ) 方法,AddRange( value1, value2, ...) 可以添加多个
插入元素:Insert(Index, value) 方法,指定位置,InsertRange(index1,value1,index2,value2, ...) 可以插入多个
访问元素:支持索引器访问(即直接用下标);用Count() 确认元素个数,for 循环遍历;因为实现了 IEnumerable 接口,所以也可以用 foreach 语句遍历。除了使用foreach语句外,List<T>类还提供了ForEach() 方法,该方法用 Action<T> 参数声明。使用直接为ForEach()方法传递一个方法,无返回值。在T 项列表中,ForEach() 方法的处理程序必须声明为以 T 对象作为参数,返回类型是 void。因为Console.WriteLine() 方法的一个重载版本将Object作为参数,所以直接可以将这个方法的地址传给ForEach() 方法。
racers.ForEach(Console.WriteLine);
Lambda 版本: racers.ForEach(r => Console.WriteLine($"{r}"));
删除元素:RemoveAt(Index);Remove(value)(不推荐,会先用IndexOf() 查找出位置后删除)(indexOf() 方法先检查元素类型是否实现了 IEquatable<T> 接口,如果是,就调用这个接口的 Equals() 方法。如果没有,就使用Object类的Equals() 方法比较这些元素。Object 类中Equals() 方法默认实现代码对值类型进行按位比较,对引用类型只比较其引用)。Clear() 方法,删除所有,ICollection<T>接口中的。
搜索:IndexOf()、LastIndexOf()、FindIndex()、FindLastIndex()、Find() 和 FindLasst()。如果只是检查元素是否存在,List<T>类就提供了Exists() 方法。
IndexOf() 找到返回该元素的索引,没有找到就返回 -1
搜索由某个特性的元素,用FindIndex() 方法来定义,FindIndex() 方法需要一个Predicate 类型的参数,Predicate<T>类型是一个委托,该委托返回一个布尔值,并且需要把 T 作为参数。true找到,false没找到。在List<T>类中,把 Racer 对象作为类型 T ,所以可以将一个方法的地址传递给 FindIndex() 方法,(该方法将类型 Racer(自定义的一个赛车手类) 定义为一个参数且返回一个布尔值)。比如说查找一个国家的第一个赛车手时,就可以创建一个FindCountry类,FindCountryPredicate() 方法的签名和返回类型通过 Predicate<T> 委托定义。里面的Find() 方法使用变量 country的 搜索用 FindCountry 类构造函数定义的国家。
1 public class FindCountry 2 { 3 private string country; 4 public FindCountry(string country) 5 { 6 this.country = country; 7 } 8 public bool FindCountryPredicate(Racer racer) 9 { 10 if (racer == null) throw new ArgumentNullException("racer"); 11 return racer.Country == country; 12 } 13 }
使用FindIndex() 方法可以创建 FindCountry() 方法的一个新实例,把一个表示国家的字符串传递给构造函数,再传递 其中Find() 方法的地址。FindIndex() 方法成功完成后,index1 就包含了集合中赛车手的Country 属性设置为 USA 的第一项索引。
int index1 = racers.FindIndex(new FindCountry("USA").FindCountryPredicate );
这里使用 Lambda 表达式来处理的话,结果一样,但是代码量大大减少。
int index2 = racers.FindIndex(r => r.Country=="USA" );
FindLastIndex() 方法是从后往前找
Find() 是直接返回元素,相反的是,FindLast() 从后面开始找
FindAll() 方法是返回一个集合
排序:
List<T>类可以使用Sort() 方法对元素进行排序,使用的是快速排序算法。
Sort() 方法使用了4个重载方法,如下
public void List<T>.Sort(); public void List<T>.Sort(Comparsion<T>); public void List<T>.Sort(IComparer<T>); public void List<T>.Sort(Int32, Int32, IComparer<T>);
只有在集合中的元素实现了IComparable 接口,才能使用不带参数的Sort() 方法。
如果需要以其他方式进行排序,就需要使用其他技术,例如传递一个实现了IComparaer<T> 接口(就是实现Compara() 方法)的对象。一般会用到 string 和 int 类型的 CompareTo() 方法。
用的时候直接传 新实例化的实现类中某个属性,例如:
racers.Sort(new RacerComparer(RaceComparer.CompareType.Country ));
另一种方法就是使用重载的Sort() 方法,需要一个Comparison<T>委托;该委托有两个T类型的参数,返回类型为int.T1 == T2 返回 0 ,T1 < T2 返回小于 0,T1 > T2 返回大于 0 。
public delegate int Comparsion<T>( T x, T y );
这边也可以直接用Lambda 表达式传递,这里实现降序,所以用 r2 和 r1 作比较。
racers.Sort((r1, r2) => r2.Wins.CompareTo(r1.Wins) );
类型转换:
ConverAll<TOutput>() 方法。可以把所有类型的集合转换为另一种类型。 ConverAll<TOutput>() 方法需要使用一个 Conveter委托
public sealed delegate TOutput Converter<TInput, TOutput>(TIput from); //TInput转换为TOutput,TInput为参数,TOutput为返回类型
比如将赛车手Racer 转换为一个Person 类,后者只有一个name 类型,转换就只进行name 属性转换,其他的可以忽略,Lambda 表达式如下
List<Person> persons = racers.ConvertAll<Person>( r => new Person(r.FirstName + " " + r.LastName));
2、只读集合
创建集合后,就是可读写的。需要得到只读集合,List<T> 集合的AsReadOnly() 方法返回 ReadOnlyCollection<T> 类型的对象。ReadOnlyCollection<T> 类实现的接口和 List<T> 集合相同,但是所有修改集合的方法和属性都会抛出 NotSupportedException 异常。
三、队列
FIFO 先进先出的集合,使用 System.Collections.Generic 名称空间中的泛型类 Queue<T>实现。它是实现ICollection 和 IEnumerable<T> 接口,但没有实现 ICollection<T> 接口,因为这个接口的 Add() 和 Remove() 方法不能用于队列。
因为没有实现 IList<T> 接口,所以不能使用索引器访问队列,只有 Enqueue() 方法在队尾添加元素,Dequeue() 从队列头部获取元素。它的方法有
四、栈
LIFO 后进先出,Pusn() 方法添加,Pop() 方法获取最近添加的元素
与Queue<T> 类相同,Statck<T> 类实现 IEnumerable<T> 和 ICollection 接口。方法有
可以使用 foreach 方法,其使用IEnmuerable 接口迭代所有的元素。此方法不会删除元素,只会逐个返回元素。
五、链表
LinkedList<T> 是一个双向链表,元素指向它前面和后面的元素,就是存了三个,Value值、Next地址和Previous 地址。这也是LinkedList<T> 中包含了LinkedListNode<T>类型的元素的原因。优点是将元素插入中间的位置,用链表会非常快;缺点是只能一个一个访问,需要用较长的时间来访问中间或者尾部的元素。
LinkedListNode<T> 定义了属性 List、Next、Previous 和 Value。List属性返回与节点相关的LinkedList<T> 对象,Next 和 Previous 属性用于遍历链表,访问当前节点之后和之前的节点。Value 返回与节点相关的元素,其类型是 T 。
LinkedList<T> 类定义的成员可以访问链表中的第一个和最后一个元素他(Frist 和 Last)、在指定位置插入元素( AddAfter()、AddBefore()、AddFirst() 和 AddLast() 方法),删除指定位置的元素( Remove()、RemoveFirst() 和 RemoveLast() 方法)、从链表的开头(Find() 方法)或结尾(FindLast() 方法)开始搜索元素。
六、有序列表
如果基于键对所需集合排序,使用 SortedList<TKey, TValue> 类。这个类按照键给元素排序。
此类只能一个键对应一个值,如果每个键对应多个值,可以使用Lookup<Tkey, TElement> 类。
可以使用foreach 语句遍历,枚举器返回的元素是 KeyValuePair<TKey, TValue> 类型。也可以使用Values 和 Keys 属性访问值和键。Values 返回 IList<TValue>,Keys属性返回 IList<TKey>。
七、字典
字典是一种复杂的数据结构,这种数据结构允许按照某个键来访问元素。字典也称为映射或散列表。其主要特性是能根据键快速查找值,也可以自由添加和删除元素。,这和List<T>类相似,但没有在内存中移动后续元素的性能开销。
一个添加到字典中的键,会转换成一个散列,利用散列创建一个数字,它将索引和值关联起来。然后索引包含一个到值的链接。
.NET Framework 提供了几个字典类。最常用的就是 Dictionary<TKey, TValue> 类。
1、键的类型
作为字典中键的类型必须重写Object 类的 GetHashCode() 方法。只要字典类需要确定元素的位置,它就要调用 GetHashCode() 方法。GetHashCode() 方法返回的 int 由字典用于计算在对应位置放置元素的索引。其算法涉及素数,字典的容量是一个素数。
GetHashCode() 方法的实现代码必须满足如下要求:
-相同的对象总是返回相同的值
-不同的对象可以返回相同的值
-它执行得比较快,计算开销不大
-它不能抛出异常
-它应至少使用一个实例字段
-散列代码值应平均分布在 int 可以存储的整个数字范围上
-散列代码最好在对象的生存期中不发生变化
字典的性能取决于 GetHashCode() 方法的实现代码。
还需要实现 IEquatable<T>.Equals() 方法,或者重写 Object 类的 Equals() 方法。当 A.Equals(B)方法返回 true ,则A.GetHashCode() 和 B.GetHashCode() 方法必须总是返回相同的散列代码。
System.String 实现了IEquatable 接口,并重载了 GetHashCode() 方法。Equals() 方法提供了值得比较,GetHashCode() 方法根据字符串的值返回一个散列代码。所以在字典中把字符串用作键非常方便。
如果需要用作的键类型,没有实现 IEquatable 接口,也没有重载 GetHashCode() 方法,就可以创建一个实现 IEqualityComparer<T> 接口的比较器。这里面有GetHashCode() 和 Equals() 方法,并将传递的对象作为参数。
2、Lookup 类
一对多,一个键映射到一个值集上,调用方法 ToLookup(),该方法返回一个 Lookup<TKey, TElement>对象。List<T> 类实现了IEnumerable<T> 接口,所以可以直接调用ToLookup() 方法。此方法需要一个 Func<TSorce, TKey>类型的委托。
3、有序字典
SortedDictionary<TKey, TValue>类是一个二叉搜索树。与SortList<TKey, TValue>类功能相似。前者是一个字典,后者是一个基于数组的列表。
八、集
set,没有重复的元素。HashSet<T> 和 SortSet<T> 两个集,一个无序,一个有序。
ISet<T> 接口提供的方法可以创建合集、交集,或者给出一个集是另一个集的超集或子集的信息。
Add() 方法返回true或者false,表示是否添加成功。
IsSubsetOf(),A.IsSubsetOf(B),A是否为B的子集
IsSupsetOf(),A.IsSupsetOf(B),A是否为B的超集(即B是否为A的子集)
UnionWith(),合集
ExceptWith(),A.ExceptWith(B)删除A中的B
九、可观察的集合
需要了解集合中的元素何时删除或添加的信息,可以使用 ObservableCollection<T> 类。
可以通过接收 NotifyCollectionChangeedEventArgs,这里面包含的Action属性可以得出结果,示例代码如下:

1 using System; 2 using System.Collections.ObjectModel; 3 4 namespace Wrox.ProCSharp.Collections 5 { 6 class Program 7 { 8 static void Main() 9 { 10 var data = new ObservableCollection<string>(); 11 data.CollectionChanged += Data_CollectionChanged; 12 data.Add("One"); 13 data.Add("Two"); 14 data.Insert(1, "Three"); 15 data.Remove("One"); 16 17 } 18 19 static void Data_CollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e) 20 { 21 Console.WriteLine("action: {0}", e.Action.ToString()); 22 23 if (e.OldItems != null) //有删除元素 24 { 25 Console.WriteLine("starting index for old item(s): {0}", e.OldStartingIndex); 26 Console.WriteLine("old item(s):"); 27 foreach (var item in e.OldItems) 28 { 29 Console.WriteLine(item); 30 } 31 } 32 if (e.NewItems != null) //有添加新元素 33 { 34 Console.WriteLine("starting index for new item(s): {0}", e.NewStartingIndex); 35 Console.WriteLine("new item(s): "); 36 foreach (var item in e.NewItems) 37 { 38 Console.WriteLine(item); 39 } 40 } 41 42 Console.WriteLine(); 43 44 } 45 } 46 }
十、位数组
当我们着重处理一个以位为单位的数据时,就可以考虑用位数组。例如在很多 PCI 的 IO 卡中涉及很多的 I 位的读取以此判断各个开关量的状态,或者设置继电器的输出状态时,就会频繁用到位的操作。
这种情况C#提供的BitArray和BitVector32类就会起到很大作用。
BItArray类可以重新设置大小,BitVector32不能,它是固定32位的。
BitArray类是一个引用类型,它包含一个 int 数组,每32位用一个新整数。比如63就是32+31,65就是32+32+1。
BitArray常用方法:
1 public sealed class BitArray : ICollection, IEnumerable, ICloneable 2 { 3 public BitArray(BitArray bits); //用已有的BitArray给新的BitArray初始化 4 5 public BitArray(bool[] values); //用布尔数组初始化 6 7 public BitArray(byte[] bytes); //用字节数组初始化 8 9 public BitArray(int length); //初始化并设置位数值,此值会在使用中自动增长 10 11 public BitArray(int[] values); //用int数组初始化 12 13 public BitArray(int length, bool defaultValue); //初始化并设置默认值 14 15 public int Count { get; } //位数组中现存的位的个数 16 17 public bool IsReadOnly { get; } //确定位数组是否只读 18 19 public bool IsSynchronized { get; } //是否同步对此BitArray的操作,用在线程安全上 20 21 public int Length { get; set; } //位数组的位数 22 23 public object SyncRoot { get; } 24 25 public bool this[int index] { get; set; } //索引器,利用索引读位值 26 27 public BitArray And(BitArray value); //按位与 28 29 public object Clone(); //创建BitArray 的浅表副本。 30 31 public void CopyTo(Array array, int index); //将BitArray拷贝到其他数组中 32 33 public bool Get(int index); //按下标读取位值 34 35 public IEnumerator GetEnumerator(); //返回循环访问BitArray 的枚举数 36 37 public BitArray Not(); //按位非 38 39 public BitArray Or(BitArray value); //按位或 40 41 public void Set(int index, bool value); //按位设置值 42 43 public void SetAll(bool value); //设置所有位为指定值 44 45 public BitArray Xor(BitArray value); //按位异或 46 }
BitVector32固定长度32位,是一个结构,存储在栈上,效率较高。
BitVector32常用方法:
1 namespace System.Collections.Specialized 2 { 3 public struct BitVector32 //首先它是是个结构,存储在栈上 4 { 5 public BitVector32(BitVector32 value); //用现有的来初始化 6 7 public BitVector32(int data); //以指定int值初始化 8 9 public int Data { get; } //返回当前BitVector32变量的值 10 11 public int this[BitVector32.Section section] { get; set; }//以索引方式获取或设置指定片段上的值 12 13 public bool this[int bit] { get; set; } //按照掩码来访问或设置值,注意bit是[掩码]不是[下标] 14 15 public static int CreateMask(); //创建第一个屏蔽,即值为1 16 17 public static int CreateMask(int previous); //按照指定值创建屏蔽 18 19 public static BitVector32.Section CreateSection(short maxValue); //创建一个片段并指定最大值 20 21 public static BitVector32.Section CreateSection(short maxValue, BitVector32.Section previous); //在指定的片段后面再创建一个片段并制定最大值。 22 23 public override bool Equals(object o); //相等判断 24 25 public override int GetHashCode(); //返回哈希代码 26 27 public override string ToString(); //重写ToString()方法 28 29 public static string ToString(BitVector32 value); //将位数组以字符串输出 30 31 public struct Section //BitVector32片段结构 32 { 33 public static bool operator !=(BitVector32.Section a, BitVector32.Section b); //重写!=操作符 34 35 public static bool operator ==(BitVector32.Section a, BitVector32.Section b); //重写==操作符 36 37 public short Mask { get; } //获取隔离掩码 38 39 public short Offset { get; } //获取从 BitVector32 的起始处开始的此节的偏移量。 40 41 public bool Equals(BitVector32.Section obj); //片段相等判断 42 43 public override bool Equals(object o); //相等判断 44 45 public override int GetHashCode(); 46 47 public override string ToString(); 48 49 public static string ToString(BitVector32.Section value); 50 } 51 } 52 }
本节参考文章:BitArray、BItVector32
十一、并发集合
待学习多线程后回来补充。。。
十二、性能
有些功能类似的集合性能却相差甚远(na表示此操作不能进行)
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 无需6万激活码!GitHub神秘组织3小时极速复刻Manus,手把手教你使用OpenManus搭建本
· C#/.NET/.NET Core优秀项目和框架2025年2月简报
· Manus爆火,是硬核还是营销?
· 一文读懂知识蒸馏
· 终于写完轮子一部分:tcp代理 了,记录一下