目录
数组
7 - 数组
7.1 - 相同类型的多个对象
可以使用集合和数组,对于数量固定的可以使用数组。
对于多个不同类型的对象可以使用类、结构或元组。
7.2 - 简单数组
数组是一种数据结构,它可以包含同一类型的多个元素。
7.2.1 - 数组的声明
使用类型紧跟一对中括号来声明数组,后面跟标识符。比如,下面的代码声明了一个 int
类型的数组:
|
|
7.2.2 - 数组的初始化
数组是引用类型,所以必须给它分配堆上的内存。应该使用 new
运算符,指定元素的类型和数量来初始化数组的变量:
|
|
上面的代码中 array
引用了一个包含8个 int
类型的元素的数组,它们位于托管堆上。
注意:在指定了数组的大小后,如果不复制其中的所有元素就不能调整大小。而对于很大的数组来说复制数组是一场性能灾难,如果事先不知道包含多少个元素,应该使用集合。
还可以使用数组初始化器为每个元素复制,数组初始化器只能在声明数组的时候使用,不能在声明之后使用。
|
|
如果使用了初始化器(花括号那个)来初始化数组,可以不指定数组的大小,由编译器自动计算:
|
|
当然,使用花括号可以同时声明和初始化数组:
|
|
7.2.3 访问数组元素
通过索引器传递元素编号,就可以访问数组。索引器总是从0开始,array[0]
表示 array
数组的第一个元素。
如果不知道数组中元素的个数,又想遍历数组,可以在 for
使用 array.Length
属性:
|
|
当然,也可以使用 foreach
语句,但是 foreach
不能更改其中的元素:
|
|
7.2.4 - 使用引用类型
除了能声明预定义类型的数组,还可以声明自定义类型的数组,语法同上:
|
|
声明完后每个元素的引用都是 null
,因此,需要使用从0开始的索引器,可以为数组每个元素分配内存:
|
|
当然,也可以使用数组初始化器。
7.3 - 多维数组
正如前面所见的,一维数组使用一个整数来索引,而多维数组使用多个整数来索引(比如我们可以用两个整数分别索引行和列)。
在CSharp中声明一个二维数组可以在方括号中间加上一个逗号,比如:
|
|
显而易见,适用于一维数组的初始化器也能用。
7.4 - 锯齿数组
锯齿数组的大小设置比较灵活,不像二维数组那样必须是“方方正正”的,锯齿数组中每一行都可以有不同的大小:
|
|
定义时,第一个方括号里写行数,第二个留空。
可以看作每个元素都是另外一个数组吧。
7.5 - Array 类
用方括号声明数组是CSharp中 Array
类的表示法。在后台使用 CSharp
语法,会创建一个派生自抽象基类 Array
的新类。这样,就可以使用 Array
类中的方法和属性了。
7.5.1 - 创建数组
除了可以使用CSharp语法来创建数组实例之外,还可以使用静态方法 Array.CreateInstance
来创建数组。如果事先不知道元素的类型,该静态方法就非常有用,因为类型可以作为 Type
对象传递给 CreateInstance
方法。
还可以将用 CreateInstance
方法创建好的数组强制转换为 int
数组:
|
|
7.5.2 - 复制数组
因为数组是引用类型,所以将数组的一个变量赋值给另外一个变量,得到是引用同一个数组的变量。使用 Clone
方法可以复制数组:
|
|
Clone
方法是浅复制,也就是说,如果数组的元素的引用类型,则仅复制引用而不深入复制值。
还可以使用 Array.Copy
方法创建浅表副本,与 Clone
的区别是, Copy
方法需要传递一个阶数仙童并且足够大的数组作为参数,而不是像 Clone
创建并返回一个新数组。
7.5.3 - 排序
Array
类使用快排算法来对数组中的元素进行排序。Sort
方法需要数组中的元素实现 IComparable
接口。在简单类型中已经实现了该接口,所以可以直接进行排序。
如果对数组使用自定义类,就实现 IComparable
接口(或者它的泛型版本),这个接口定义了方法:Compare
,该方法接受一个要被用来比较的对象作为参数:
- 如果要比较的对象的相等,就返回0。
- 如果该实例排在参数对象的前面,就返回小于0的值。
- 如果该实例排在参数对象的后面,就返回大于0的值。
如果对象的排序方法与上述不同,或者不能修改数组元素中用作元素的类(即修改元素的类并添加继承自 IComparable
),就可以考虑另外一个类(比如 Comparer
)实现 IComparer
接口或者其泛型版本。这两个接口定义了方法 Compare
,这个方法接受两个参数:
- 如果两个参数对象相等,就返回0。
- 如果第一个参数在第二个参数前面,则返回小于0的值。
- 如果第一个参数在第二个参数后面,则返回大于0的值。
Tips:
Array
类提供了Sort
方法,需要传递一个委托i作为参数,而不需要依赖IComparer
接口或者IComparable
接口。
7.6 - 数组作为参数
要把数组传递给方法,就按着你想象的来就行了。将参数声明为数组:
|
|
7.7 - 数组协变
数组支持协变,也就是说,可以把数组声明为基类,其它派生类的对象可以被赋予数组:
注意:数组协变只能用于引用类型不能用于值类型。
7.8 - 枚举
在 foreach
语句中使用枚举,可以迭代集合中的元素,且不需要知道集合中的元素个数。数组和集合实现了带 GetEnumerator
方法的 IEnumerable
接口。 GetEnumerator
方法返回一个实现 IEnumerable
接口的枚举。接着 foreach
语句就可以使用 IEnumerable
接口迭代集合了。
7.8.1 - IEnumerator
接口
foreach
语句使用 IEnumerator
接口的方法和属性,迭代集合中的所有元素。为此接口定义了 Current
用来返回当前迭代的元素。
MoveNext
方法移动到下一个元素上,如果没有下一个元素则返回 false
,移动成功则返回 true
。
7.8.2 - foreach
语句
foreach
首先调用 GetEnumerator
方法获取一个枚举器,并且当 MoveNext
方法返回 true
时进行迭代。
|
|
相当于:
|
|
7.8.3 yield
语句
yield return
语句返回集合的一个元素,并移动到下一个元素上。yield break
可停止迭代。
Tips:包含
yield
语句的方法或属性也成为迭代块。迭代块必须声明为返回IEnumerator
或IEnumerable
接口,或者这写接口的泛型版本。这个块可以包含多条yield return
语句或yield break
语句,但不能包含return
语句。
1. 迭代集合的不同方式
2. 用 yield return
返回枚举器
常见的使用方法是,用一个循环来迭代集合,然后每一次迭代来一个 yield return
语句,比如:
|
|
首先,定义了一个学生类 Student
,其中含有学生的基础信息和一个用于实例化的构造函数以及重写的 ToString
方法。
然后,定义了另外一个类 School
,在学校类中写了 Add
方法来添加学生。另外实现了 GetEnumerator
方法,由于在 foreach
中的元素是 Student
类的实例,因此类型为泛型的 IEnumerator<Student>
。yield return
返回每一次迭代的结果即可。
7.9 - 结构比较
数组和元组都实现了接口 IStructuralEquatable
(用于进行内容比较是否相等)和 IStructuralComparable
(用于在排序时进行比较决定顺序)。这两个接口不仅可以比较引用,还可以比较内容。
可以让需要的结构实现 IEquatable
接口,这个接口定义了一个强化的 Equals
方法。这个方法接收两个参数,第一个是 object
,第二个是 IEquatableComparer
:
|
|
7.10 - Span
为了快速访问连续内存,可以使用 Span<T>
结构。一个可以使用 Span<T>
的例子是数组; Span<T>
结构在后台保存在连续的内存中。
使用 Span<T>
可以直接访问数组元素。数组的元素没有复制,可以直接使用,这比复制要快(废话)。
Span
结构有个构造函数,把数组传参进去就行了,就能获得一个 Span
。
7.10.1 - 创建切片
Span<T>
的一个强大功特性是,可以使用它创建访问数组的一部分或者是切片。使用切片时,不会复制数组元素,它们是从 Span
直接访问的。
调用 Slice
方法,也可以从 Span<T>
对象中创建一个切片。该方法接受两个参数:切片开始的索引号以及切片的长度。当然,也可以使用 Span
的构造函数创建一个切片:第一个参数是数组,后面是切片开始的索引号和长度。
如果以后不需要修改 Span
引用的内容,可以使用 ReadOnlySpan
这个类型,转换是隐式进行的。
注意:
Span<T>
是安全的,不会出界破坏其它内存中的数据。如果创建的Span
超出了数组的长度,则会抛出ArgumentOutOfRangeException
异常。
7.10.2 - 使用 Span
改变值
调用 Clear
方法,使用 0
填充 Span<int>
。
调用 Fill
方法,使用传递的参数填充 Span<T>
。
调用 CopyTo
方法,将一个 Span
复制到另一个 Span
中,如果目标不够大则会抛出 ArgumentException
异常。调用 TryCopyTo
方法来避免发生异常:
|
|
7.10.3 - 只读的 Span
如上文所说,对于不需要修改的 Span
可以使用 ReadOnlySpan<T>
类型,这种类型没有 Clear
和 Fill
等方法,但是可以调用 CopyTo
复制到新的 Span
。
7.11 - 数组池
如果一个应用程序创建、销毁了很多数组,那么GC酱就会很累。可以通过 ArrayPool
类创建数组池。
7.11.1 - 创建数组池
通过调用静态的 Create
方法,就可以创建 ArrayPool<T>
。使用 Create
方法可以在创建之前定义最大的数组长度和数组的数量:
|
|
maxArrayLength
的默认值是 1024x1024 字节,maxArraysPerBucket
的默认值是 50 。数组池使用了多个桶,以便使用多个数组时更快。只要还没有达到数组的最大数量,大小类似的数组就尽可能保存在一个桶里。
1.12.2 - 从池中租借内存
调用 Rent
方法从池中租借内存。该方法接受应请求的最小数组长度。Rent
方法返回一个数组,其中至少包含所请求的元素个数。返回的数组可能有更多的可用内存:
|
|
1.12.3 - 将内存返回给池
不再需要数组的时候,可以把内存返回回去。数组返回后,稍后可以使用一个 Rent
来重用这些内存。
调用数组池的 Return
方法并将数组作为参数传递给它。这样返回后不会清除数组内的数据,也就是下次 Rent
后还可以访问其中的数据。为此该方法还有一个可选参数,指定在返回池之前是否清空它。