C#.Net 筑基 - 集合知识全解
01、集合基础知识
.Net 中提供了一系列的管理对象集合的类型,数组、可变列表、字典等。从类型安全上集合分为两类,泛型集合 和 非泛型集合,传统的非泛型集合存储为 Object,需要类型转。而泛型集合提供了更好的性能、编译时类型安全,推荐使用。
.Net 中集合主要集中在下面几个命名空间中:
1.1、集合的起源:接口关系
天赋技能 —— foreach:几乎所有集合都可以用
foreach
循环操作,是因为他们都继承自IEnumerable
接口,由枚举器(IEnumerator)提供枚举操作。几乎所有集合都提供添加、删除、计数,来自基础接口
ICollection
、ICollection<T>
。IList
、IList<T>
提供了数组的索引器、查找、插入等操作,几乎所有具体的集合类型都实现了该接口。Array 是一个抽象类,是所有数组
T[]
的基类,她是类型安全的。推荐尽量使用数组 T[]、泛型版的集合,提供了更好的类型安全和性能。
1.2、非泛型集合—— 还有什么存在的价值?
非泛型的
Hashtable
,Key、Value 都是 Object 类型的,Dictionary 是泛型版本的 Hashtable。ArrayList 是非泛型版本的
List<T>
,基本很少使用,也尽量不用。
❓既然非泛型版本类型不安全,性能还差,为什么还存在呢?
主要是历史原因,泛型是.Net2.0
引入的,因此为了向后兼容,依然保留的非泛型版本集合。在接口实现时,非泛型接口一般都是显示实现的,因此基本不会用到。不过在有些场景下,非泛型接口、集合还是有点用的,如类型不固定的集合,或者用接口作为约束条件或类型判断。
1.3、Collection<T>
、List<T>
有何不同?
❓两者比较相似,他们到底有什么区别呢?该如何选择?
Collection<T>
作为自定义集合基类,内部提供了一些virtual
的实现,便于继承实现自己的集合类型。其内部集合用的就是List<T>
,如下部分源码 Collection.cs。List<T>
作为集合使用,是最常用的可变长集合类型了,他优化了性能,但是丢失了可扩展性,没有提供任何可以override
的成员。
02、枚举器——foreach 的秘密!
foreach
用来循环迭代可枚举对象,用一种非常简洁、优雅的姿势访问可枚举元素。常用于数组、集合,当然不仅限于集合,只要符合要求枚举要求的都可以。
2.1、IEnumerator 枚举器
枚举可以foreach
枚举的密码是他们都继承自IEnumerable
接口,而更重要的是其内部的枚举器 —— IEnumerator。枚举器IEnumerator
定义了向前遍历集合元素的基本协议,其申明如下:
MoveNext()
移动当前元素到下一个位置,Current
获取当前元素,如果没有元素了,则MoveNext()
返回false
。注意MoveNext()
会先调用,因此首次MoveNext()
是把位置移动到第一个位置。Reset()
用于重置到起点,主要用于 COM 互操作,使用很少,可不用实现(直接抛出 NotSupportedException)。
📢 该接口不是必须的,只要实现了公共的
Current
、无参MoveNext()
成员就可进行枚举操作。
实现一个获取偶数的枚举器:
2.2、IEnumerable 可枚举集合
IEnumerable
、IEnumerable<T>
是所有集合的基础接口,其核心方法就是 GetEnumerator()
获取一个枚举器。
📢 该接口也不是必须的,只要包含
public
的“GetEnumerator()”方法也是一样的。
有了 GetEnumerator()
,就可以使用foreach
来枚举元素了,这里foreach
会被编译为 while (evenor.MoveNext()){}
形式的代码。在上面 偶数枚举器的基础上实现 一个偶数类型。
foreach 迭代其实就是调用其GetEnumerator()
、Current
、MoveNext()
实现的,因此接口并不是必须的,只要有对应的成员即可。
2.3、yield 迭代器
yield return
是一个用于实现迭代器的专用语句,它允许你一次返回一个元素,而不是一次性返回整个集合。常来用来实现自定义的简单迭代器,非常方便,无需实现IEnumerator
接口。
🔸惰性执行:元素是按需生成的,这可以提高性能并减少内存占用(当然这个要看具体情况),特别是在处理大型集合或复杂的计算时。迭代器方法在被调用时,不会立即执行,而是在MoveNext()
时,才会执行对应yield return
的语句,并返回该语句的结果。📢Linq 里的很多操作也是惰性的。
🔸简化代码:使用yield return
可以避免手动编写迭代器的繁琐过程。
🔸状态保持:yield return
自动处理状态保持,使得在每次迭代中保存当前状态变得非常简单。每一条yield return
语句执行完后,代码的控制权会交还给调用者,由调用者控制继续。
yield
迭代器方法会被会被编译为一个实现了IEnumerator
接口的私有类,可以看做是一个高级的语法糖,有一些限制(要求):
迭代器的返回类型可以是
IEnumerable
、IEnumerator
或他们的泛型版本。还可以用 IAsyncEnumerable<T>
来实现异步的迭代器。yield break
语句提前退出迭代器,不可直接用return
,是非法的。yield
语句不能和try...catch
一起使用。
03、集合!装逼了!
3.1、⭐常用集合类型
3.2、⭐数组 Array[]
Array 数组是一种有序的集合,通过唯一索引编号进行访问。数组T[]
是最常用的数据集合了,几乎支持创建任意类型的数组。Array
是所有的数组T[]
的(隐式)基类,包括一维、多维数组。CLR 会将数组隐式转换为 Array 的子类,生成一个伪类型。
索引从 0 开始。
定长:数组在申明时必须指定长度,超出长度访问会抛出
IndexOutOfRangeException
异常。内存连续:为了高效访问,数组元素在内存中总是连续存储的。如果是值类型数组,值和数组是存储在一起的;如果是引用类型数组,则数组值存储其引用对象的(堆内存)地址。因此数组的访问是非常高效的!
多维数组:矩阵数组 用逗号隔开,
int[,] arr = {{1,2},{3,4}};
多维数组:锯齿形数组(数组的数组),
int[][] arr =new int[3][];
📢 几乎大部分编程语言的数组索引都是从 0 开始的,如 C、Java、Python、JavaScript 等。当然也有从 1 开始的,如 MATLAB、R、Lua。
📢 通过上表发现,Array 的很多方法都是静态方法,而不是实例方法,这一点有点困惑,造成了使用不便。而且大部分方法都可以用 Linq 的扩展来代替。
3.3、Linq 扩展
LINQ to Objects (C#) 提供了大量的对集合操作的扩展,可以使用 LINQ 来查询任何可枚举的集合(IEnumerable)。扩展实现主要集中在 代码 Enumerable 类(源码 Enumerable.cs),涵盖了查询、排序、分组、统计等各种功能,非常强大。
简洁、易读,可以链式操作,简单的代码即可实现丰富的筛选、排序和分组功能。
延迟执行,只有在 ToList、ToArray 时才会正式执行,和
yeild
一样的效果。
04、集合的一些小技巧
4.1、集合初始化器{}
同类的初始化器类似,用{}
来初始化设置集合值,支持数组、字典。
4.2、集合表达式[]
集合表达式 简化了集合的申明和赋值,直接用[]
赋值,比初始化器更简洁,语法形式和JavaScript
差不多了。可用于数组、Sapn、List,还可以自定义集合生成器。
4.3、范围运算符..
a..b
表示 a 到 b 的范围(不含 b),其本质是 System.Range 类型数据,表示一个索引范围,常用与集合操作。
可省略
a
或b
,缺省则表示到边界。可结合倒数
^
使用。
自定义的索引器也可用用范围
Range
作为范围参数。
05、提高集合性能的一些实践
🚩尽量给集合一个合适的“容量”( capacity),几乎所有可变长集合的“动态变长”其实都是有代价的。他们内部会有一个定长的“数组”,当添加元素较多(大于容量)时,就会自动扩容(如倍增),然后把原有“数组”数据拷贝(搬运)到新“数组“中。
因此在使用可变长集合时,尽量给一个合适的大小,可减少频繁扩容带来的性能影响。当然也不可盲目设置一个比较大的容量,这就很浪费内存空间了。
stringBuilder
也是一样的道理。可变长集合的插入、删除效率都不高,因为会移动其后续元素。
下面测试一下List<T>
,当创建一个长度为 1000 的List
时,设置容量(1000)和不设置容量(默认 4)的对比。
很明显,自动长度的List
速度更慢,也消耗了更多的内存。
🚩尽量不创建新数组,使用一些数组方法时需要注意尽量不要创建新的数组,如下面示例代码:
比较一下上面两种反序的性能:
文章转载自:安木夕
评论