C#中的集合操作
数组
论集合,不得不谈的第一项就是数组。C#数组需要声明元素类型,元素类型可以是值类型也可以是引用类型,数组是静态类型,初始化必须指定个数大小,且创建后的数组是连续存放在内存中的。声明方式如下所示:
int[] intArray = new int[10]; //整型数组
string[] stringArray = new string[10]; //字符串数组
Random[] randArray = new Random[10]; //类数组
数组是从Array隐式派生的,这是由编译器完成的,Array类被组织在System命名空间下。
Array myArray1 = new int[10];
Array myArray2 = new string[10];
Array myArray3 = new Random[10];
数组是引用类型,初始化后分配在堆上。
System.Collections
1. System.Collections 组织空间
集合类型根据自身的需求实现了左边接口中的一个或多个,按照实现接口大概可以分为三种:有序集合(ICollection),索引集合(IList),键式集合(IDictionary)。
2. 通用接口
Collections常用接口实现
集合类型都实现了IEnumerable接口,从而可以使用foreach迭代。
实现了ICollectoin接口的集合类表明集合中的元素是有先后顺序的。
IList接口继承了ICollection接口,实现了IList接口的集合类不止表明集合中的元素是有先后顺序,而且表明集合类可以通过下标访问的方式访问集合元素。
IDictionary接口也继承了ICollection接口,实现了IDicionary接口的集合类可以通过下标key访问的访问方式访问集合元素。
3. ArrayList
Array是静态分配的,意味着创建数组后的大小不能改变,然而实际应用中,我们很多时候无法在一开始确定数组的大小,这样便需要一种能动态分配的数组类型,ArrayList就是为此而生的。ArrayList主要实现的接口有IList,所以这是一个索引集合。
//声明ArrayList
ArrayList myArray = new ArrayList();
//插入元素 可以为值类型 也可以为引用类型
myArray.Add(1);
myArray.Add("hello");
myArray.Add(new Random());
int i = (int)myArray[0];
string str = (string)myArray[1];
Random rand = (Random)myArray[2];
使用Reflector查看ArrayList的底层实现,可以看到动态数组 其实是由一个object[] _items维系着的,这就解释了为什么集合的元素可以为值类型也可以为引用类型。这样的底层实现貌似灵活性很高,集合可以容纳异构类型,然而却带来了性 能上的问题,因为当插入值类型的时候,就存在隐式装箱操作,而当把元素还原为值类型变量的时候,又发生了一次显式拆箱操作,如果这种装箱拆箱存在上千万 次,那么程序的性能是要大打折扣的。同时要注意一点的是object类型可以强制转化为任何类型,这是说编译器不会检查object强制转换的类型,如果 无法转换的话,这必须等到运行时才能确定出错,这就是类型安全问题。
所以应该尽量避免使用ArrayList。
4. Stack 和 Queue
对于栈和队列这两种经典的数据结构,C# 也将它们组织在System.Collections命名空间中。Stack是后进先出的结构,Queue是先进先出的结构,这两者主要实现的接口有 ICollection,表示它们都是有序集合,应注意到这两者都不可以使用下标访问集合元素。这两者的底层实现都是由一个object[] _array维系着,都存在着装箱拆箱的性能问题和类型安全问题,所以应该尽量避免直接使用它们。
5. HashTable
前面我们提到的集合类都属于单元素集合类,实际应用中我们需要一种键值对的形式存储数据,即集合类中存储的不再是单个元素,而是key-value两个元素,HashTable就是为此而生的。HashTable实现了IDictionary接口。
//创建一个HashTable实例
Hashtable hashDict = new Hashtable();
//往容器中加入key-value
hashDict.Add("a", "hello");
hashDict.Add("b", "hello");
hashDict.Add("c", "go");
hashDict.Add(4, 300);
//通过下标key获取value
string str1 = hashDict["a"].ToString();
string str2 = (string)hashDict["b"];
int i4 = (int)hashDict[4];
HashTable中的key关键字必须唯一,不能重复。如果深入到 HashTable的底层实现,应该可以清楚的看到key和value是结构体bucket数组维护着,bucket中key和value的实现也是 object,所以存在着与ArrayList,Stack,Queue同样的问题,应该尽量避免使用HashTable。
为什么键值集合类要命名为HashTable?
从HashTable的命名来看,我们可以断定说键值集合跟Hash必定存在某种联系。哈希又称为散列,散列技术是在记录的存储位置和它的关键字之间建 立一个确定的对应关系f,使得每个关键字key对应一个存储位置f(key),f又称为散列函数。HashTable实现了IDictionary接口, 表明HashTable可以通过下标key访问的方式获取数组元素,即 HashTable[key],这与实现了IList接口的集合类的数字下标访问存在着明显的不同。那么如何通过key快速定位得到value呢?我相信 通过前面的铺垫大伙都知道是什么回事了,对,就是哈希函数的运用
6. SortedList
在控制台应用程序下运行以下代码并观察结果:
Hashtable hashDict = new Hashtable();
hashDict.Add("key1", "1");
hashDict.Add("key2", "2");
hashDict.Add("key3", "3");
hashDict.Add("key4", "4");
foreach (string key in hashDict.Keys)
{
Console.WriteLine(hashDict[key]);
}
我们可以看到这里并没有按照预期的Key顺序输出Value。也就是说HashTable是不按照key顺序排序的,具体原理可以参考HashTable的源码。SortedList很好地解决了键值集合顺序输出的问题。
//创建一个SortedList实例
SortedList sortList = new SortedList();
//往容器中加入元素
sortList.Add("key1", 1);
sortList.Add("key2", 2);
sortList.Add("key3", 3);
sortList.Add("key4", 4);
//获取按照key排序的第index个Key
string str1 = sortList.GetKey(0).ToString();
Console.WriteLine(str1);
//获取按照key排序的第index个Value
int i1 = (int)sortList.GetByIndex(0);
Console.WriteLine(i1.ToString());
//下标key访问
string str2 = sortList["key2"].ToString();
Console.WriteLine(str2);
//遍历sortList
foreach (DictionaryEntry item in sortList)
{
Console.WriteLine(item.Key);
Console.WriteLine(item.Value);
}
SortedList实现了IDictionary接口,所以可以使用下标key访问元素的形式,同时要求key必须唯一。
我们知道HashTable通过使用哈希函数通过key快速找到存储位置,那么SortedList又是如何实现下标key访问元素?
SortedList的底层实现与HashTable有着本质的区别。 SortedList中使用object[] keys 和 object[] values 两个对象数组分别来存储key和value,要求实现能按照key有序输出,在下标key访问的时候就无法使用Hash函数了,所以SortedList 虽然也是键值集合,但与Hash却没有任何联系。通过查看SortedList的底层代码,原来它的实现是二分查找(BinarySearch),也就是 说要求key是有序排列的,在查找的时候进行二分比较搜索,找到对应的index,从而返回values[index]。
那么如何在HashTable和SortedList中做出选择?
如果需要实现按照key有序输出,那么毫无疑问就要选择SortedList 了。如果不需要按照key有序输出,在小数据量的情况下,两者选择任何一个性能都应该差不多,但大数据量的情况下,则更应该选择HashTable。为什 么呢?理由有两点。1.HashTable的key下标访问更直接更快。通过上面分析我们知道SortedList的key下标访问是由二分查找实现的, 实现的时间复杂度为O(log n),而Hash函数的时间复杂度为O(1),HashTable的实现更优。2.SortedList要求key有序,这意味着在插入的时候必须适当地 移动数组,从而达到有序的目的,所以存在性能上的消耗,HashTable的实现更优。
SortedList也并没有走出装箱拆箱性能和类型安全的圈子,所以应该尽量避免直接使用它。
System.Collections.Generic
1. System.Collections.Generic 组织空间
System.Collections.Generic是.NET 2.0新增的一个命名空间。C#1中的集合类型都存在着装箱拆箱的性能问题以及潜在的类型安全问题,丑陋的设计必须得到改进,于是C#2就引入了泛型这个概念。
2. 何为集合泛型
新 的命名空间下,可以看到接口或集合类后都携带了<T>或<TKey,TValue>。很明显,携带<T>对应的是单 元素集合,携带<TKey,TValue>对应的是键值集合。那么泛型又是如何工作的呢?来看一下其编译过程吧:初次编译时,首先生成IL代 码和元数据,T(TKey,TValue)只是作为类型占位符,不进行泛型类型的实例化;在进行JIT编译时,将以实际类型替换IL代码和元数据中的T占 位符,并将其转换为本地代码,下一次对该泛型类型的引用将使用相同的本地代码。
泛型即解决了装箱拆箱的性能问题,又解决了潜在的类型转换安全问题,所以在实际应用中,推荐使用泛型集合代替非泛型集合。
3. 泛型集合与非泛型集合的对应
ArrayList => List<T> 新的泛型集合去掉了Array前缀。
Stack,Queue => Stack<T>,Queue<T>
HashTable => Dictionary<TKey,TValue> 新的泛型键值集合的命名放弃了HashTable,但其内部实现原理还是和HashTable有很大相似的。
SortedList => SortedList<TKey,TValue> | SortedDictionary<TKey,TValue>
如何区分SortedList<TKey,TValue> 和 SortedDictionary<TKey,TValue>?
SortedList<TKey,TValue>的底层实现基本是按照SortedList,下标key访问是二分查找的O(log n),插入和移除运算复杂度是O(n)。而SortedDictionary<TKey,TValue> 底层实现是二叉搜索树,下标key访问也为O(log n),插入和移除运算复杂度是O(log n)。所以SortedList<TKey,TValue>使用的内存会比SortedDictionary<TKey,TValue>小,SortedDictionary<TKey,TValue>在插入和移除元素的时候更快。
C#集体类型( Collections in C#)
集合是.NET FCL(Framework Class Library)中很重要的一部分,也是我们开发当中最常用到的功能之一,几乎是无处不在。俗话说知其然,知其所以然,平常看到 IEnumerable,IEnumerator,ICollection是不是知道他们之间各自的区别?除了List和Dictionary以外,你还 用过哪些其它的集合类?废话少说,今天我们就来看一些这些定义集合类的接口以及他们的实现。
集合接口
先来看一下,FCL为我们提供了哪些接口:
IEnumerable 和IEnumberator
|
1 2 3 4 5 6 7 |
|
IEnumerator定义了我们遍历集合的基本方法,以便我们可以实现单向向前的访问集合中的每一个元素。而IEnumerable只有一个方法GetEnumerator即得到遍历器。
|
1 2 3 4 |
|
注意:我们经常用的foreach即是一种语法糖,实际上还是调用Enumerator里面的Current和MoveNext实现的遍历功能。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
|
上面的代码中用到的foreach和enumerator到IL中最后都会被翻译成enumerator的MoveNext和Current。
IEnumerable是一个很有用的接口,实现它的好处包括:
- 支持foreach语句
- 作为一个标准的集合类与其它类库进行交互
- 满足更复杂的集合接口的需求
- 支持集合初始化器
当然实现的方法也有很多,如下:
- 如果我们集合是通过封装其它集合类而来的,那么我们可以直接返回这个集合的enumerator
- 通过yield return 来返回
- 实现我们自己的IEnumerator来实现
这里给大家演示一下如何通过yield来实现返回enumerator
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
|
ICollection<T>和ICollection
从最上面第一张图我们可以知道,ICollection是直接继承自IEnumerable。而实际上也是如此,我们可以说ICollection比IEnumerable多支持一些功能,不仅仅只提供基本的遍历功能,还包括:
- 统计集合和元素个数
- 获取元素的下标
- 判断是否存在
- 添加元素到未尾
- 移除元素等等。。。
ICollection 与ICollection<T> 略有不同,ICollection不提供编辑集合的功能,即Add和Remove。包括检查元素是否存在Contains也不支持。
IList<T>和IList
IList则是直接继承自ICollection和IEnumerable。所以它包括两者的功能,并且支持根据下标访问和添加元素。 IndexOf, Insert, RemoveAt等等。我们可以这样说,IEnumerable支持的功能最少,只有遍历。而ICollection支持的功能稍微多一点,不仅有遍历还 有维护这个集合的功能。而IList是最全的版本。
IReadOnlyList<T>
这个是在Framework4.5中新增的接口类型,可以被看作是IList<T>的缩减版,去掉了所有可能更改这个集合的功能。比如:Add, RemoveAt等等。
IDictionary<TKey,TValue>
IDictionary提供了对键值对集合的访问,也是继承了ICollection<T>和IEnumerable,扩展了通过Key来访问和操作数据的方法。
关联性泛型集合类
关联性集合类即我们常说的键值对集合,允许我们通过Key来访问和维护集合。我们先来看一下 FCL为我们提供了哪些泛型的关联性集合类:
- Dictionary<TKey,TValue>
- SortedDictionary<TKey,TValue>
- SortedList<TKey,TValue>
Dictionary<TKey,TValue>
Dictionary<TKey,TValue>可能是我们最常用的关联性集合了,它的访问,添加,删除数据所花费的时间是所有集 合类里面最快的,因为它内部用了Hashtable作为存储结构,所以不管存储了多少键值对,查询/添加/删除所花费的时间都是一样的,它的时间复杂度是 O(1)。
Dictionary<TKey,TValue>优势是查找插入速度快,那么什么是它的劣势呢?因为采用Hashtable作为存 储结构,就意味着里面的数据是无序排列的,所以想按一定的顺序去遍历Dictionary<TKey,TValue>里面的数据是要费一点工 夫的。
作为TKey的类型必须实现GetHashCode()和Equals() 或者提供一个IEqualityComparer,否则操作可能会出现问题。
SortedDictioanry<TKey,TValue>
SortedDictionary<TKey,TValue>和Dictionary<TKey,TValue>大致 上是类似的,但是在实现方式上有一点点区别。SortedDictionary<TKey,TValue>用二叉树作为存储结构的。并且按 key的顺序排列。那么这样的话SortedDictionary<TKey,TValue>的TKey就必须要实现IComparable<TKey>。如果想要快速查询的同时又能很好的支持排序的话,那就使用SortedDictionary吧。
SortedList<TKey,TValue>
SortedList<TKey,TValue>是另一个支持排序的关联性集合。但是不同的地方在于,SortedList实际是 将数据存存储在数组中的。也就是说添加和移除操作都是线性的,时间复杂度是O(n),因为操作其中的元素可能导致所有的数据移动。但是因为在查找的时候利 用了二分搜索,所以查找的性能会好一些,时间复杂度是O(log n)。所以推荐使用场景是这样地:如果你想要快速查找,又想集合按照key的顺序排列,最后这个集合的操作(添加和移除)比较少的话,就是 SortedList了。
非关联性泛型集合类
非关联性集合就是不用key操作的一些集合类,通常我们可以用元素本身或者下标来操作。FCL主要为我们提供了以下几种非关联性的泛型集合类。
- List<T>
- LinkedList<T>
- HashSet<T>
- SortedSet<T>
- Stack<T>
- Queue<T>
List<T>
泛型的List 类提供了不限制长度的集合类型,List在内部维护了一定长度的数组(默认初始长度是4),当我们插入元素的长度超过4或者初始长度 的时候,会去重新创建一个新的数组,这个新数组的长度是初始长度的2倍(不永远是2倍,当发现不断的要扩充的时候,倍数会变大),然后把原来的数组拷贝过 来。所以如果知道我们将要用这个集合装多少个元素的话,可以在创建的时候指定初始值,这样就避免了重复的创建新数组和拷贝值。
另外的话由于内部实质是一个数组,所以在List的未必添加数据是比较快的,但是如果在数据的头或者中间添加删除数据相对来说更低效一些因为会影响其它数据的重新排列。
LinkedList<T>
LinkedList在内部维护了一个双向的链表,也就是说我们在LinkedList的任何位置添加或者删除数据其性能都是很快的。因为它不 会导致其它元素的移动。一般情况下List已经够我们使用了,但是如果对这个集合在中间的添加删除操作非常频繁的话,就建议使用LinkedList。
HashSet<T>
HashSet是一个无序的能够保持唯一性的集合。我们也可以把HashSet看作是 Dictionary<TKey,TValue>,只不过TKey和TValue都指向同一个对象。HashSet非常适合在我们需要保持集 合内元素唯一性但又不需要按顺序排列的时候。
HashSet不支持下标访问。
SortedSet<T>
SortedSet和HashSet,就像SortedDictionary和Dictionary一样,还记得这两个的区别么?SortedSet内部也是一个二叉树,用来支持按顺序的排列元素。
Stack<T>
后进先出的队列
不支持按下标访问
Queu<T>
先进先出的队列
不支持按下标访问
推荐使用场景
|
集合 |
顺序排列 |
连顺存储 |
直接访问方式 |
访问时间 |
操作时间 |
备注 |
|
Dictionary |
|
是 |
Key |
Key: O(1)
|
O(1) |
访问性能最快,不支持排序 |
|
SortedDinctionary |
顺序排列 |
否 |
Key |
Key: |
O(log n) |
快速访问和支持排序的折衷 |
|
SortedList |
顺序排列 |
是 |
Key |
Key: O(log n)
|
O(n) |
和SortedDictionary相似,只是内部用数据替代树作为存储结构。 |
|
List |
使用者可以精确控制元素的位置 |
是 |
Index |
Index: O(1) Value: O(n)
|
O(n) |
最适合需要直接访问每一个元素的少量集合。 |
|
LinkedList |
使用者可以精确控制元素的位置 |
否 |
不支持 |
Value: O(n)
|
O(1) |
最适合不需要直接访问单个元素,但是在集合中添加/移除非常频繁的场景。 |
|
HashSet |
不支持 |
是 |
Key |
Key: O(1)
|
O(1) |
能保持元素唯一性的集合。不支持排序 |
|
SortedSet |
顺序排列 |
否 |
Key |
Key: O(log n)
|
O(log n) |
能保持元素唯一性并且支持排序。 |
|
Stack |
LIFO |
是 |
只能获取顶部元素 |
Top: O(1) |
O(1) |
|
|
Queue |
FIFO |
是 |
只能获底部元素 |
Front: O(1) |
O(1) |
|
非泛型类集合
泛型集合类是在.NET2.0的时候出来的,也就是说在1.0的时候是没有这么方便的东西的。现在基本上我们已经不使用这些集合类了,除非在做一些和老代码保持兼容的工作的时候。来看看1.0时代的.NET程序员们都有哪些集合类可以用。
- ArraryList
后来被List<T>替代。
- HashTable 后来被Dictionary<TKey,TValue>替代。
- Queue 后来被Queue<T>替代。
- SortedList 后来被SortedList<T>替代。
- Stack 后来被Stack<T>替代。
线程安全的集合类
- ConcurrentQueue 线程安全版本的Queue
- ConcurrentStack线程安全版本的Stack
- ConcurrentBag线程安全的对象集合
- ConcurrentDictionary线程安全的Dictionary
- BlockingCollection
浙公网安备 33010602011771号