![C# 10核心技术指南](https://wfqqreader-1252317822.image.myqcloud.com/cover/89/52513089/b_52513089.jpg)
2.7 数组
数组是固定数量的特定类型的变量集合(称为元素)。为了实现高效访问,数组中的元素总是存储在连续的内存块中。
C#中的数组用元素类型后加方括号的方式表示:
![](https://epubservercos.yuewen.com/77D764/31147986804769406/epubprivate/OEBPS/Images/0073-02.jpg?sign=1738922633-jDrhUWH0FVKjXdK5CdpmtvZnnaXtZEDq-0-6d7cf70efa664a20c6958d9d2c632725)
方括号也可用于检索数组,通过位置访问特定元素:
![](https://epubservercos.yuewen.com/77D764/31147986804769406/epubprivate/OEBPS/Images/0073-03.jpg?sign=1738922633-bDpPVcbsHhvLAVhGvodtkaDNP4su67GK-0-e8b6a2484d809c36a73770a72efcdfd3)
数组索引是从0开始的,所以上面的语句输出“e”。我们可以使用for循环语句来遍历数组中的每一个元素。下面例子中的for循环将把整数变量i从0到4进行循环:
![](https://epubservercos.yuewen.com/77D764/31147986804769406/epubprivate/OEBPS/Images/0073-04.jpg?sign=1738922633-OtECTNWQmC1I67JhL66kcl3deIlNvgtN-0-1f002be58a9e9f096ff975734c96b886)
数组的Length属性返回数组中的元素数目。一旦数组创建完毕,它的长度将无法更改。System.Collection命名空间和子命名空间提供了可变长度数组和字典等高级数据结构。
我们可以使用数组初始化表达式声明数组并填充数组元素:
![](https://epubservercos.yuewen.com/77D764/31147986804769406/epubprivate/OEBPS/Images/0073-05.jpg?sign=1738922633-kuKTj2SBXMgrY70ZFUmBrobMxphNMiyy-0-5bc5845f0c630c0f312a6cce4bedeeaf)
或者简写为
![](https://epubservercos.yuewen.com/77D764/31147986804769406/epubprivate/OEBPS/Images/0074-01.jpg?sign=1738922633-6E5JPuFyNUTxLtm63ny9rwVcc5yoZ57o-0-a8453482ea7c6ea62f0d614ac00af20f)
所有的数组都继承自System.Array类,它为所有数组提供了通用服务。这些成员包括与数组类型无关的获取和设定数组元素的方法,我们将在7.3节介绍。
2.7.1 默认数组元素初始化
创建数组时总会用默认值初始化数组中的元素,类型的默认值是按位取0的内存表示的值。例如,若定义一个整数数组,由于int是值类型,因此该操作会在连续的内存块中分配1000个整数。每一个元素的默认值都是0:
![](https://epubservercos.yuewen.com/77D764/31147986804769406/epubprivate/OEBPS/Images/0074-02.jpg?sign=1738922633-dAnQuRHSNfEqah5Ze7qvBNdqeEqlvNZZ-0-5eecbd78c1b00c28900733d47b5a7ba8)
值类型和引用类型的区别
数组元素的类型是值类型还是引用类型对其性能有重要的影响。若元素类型是值类型,每个元素的值将作为数组的一部分进行分配,例如:
![](https://epubservercos.yuewen.com/77D764/31147986804769406/epubprivate/OEBPS/Images/0074-03.jpg?sign=1738922633-CAX2z9OHVYRqS7NQu2XD8Abj0IxGIvMG-0-ae4c18832b12ba966207517695a503f3)
若Point是类,创建数组则仅仅分配了1000个空引用:
![](https://epubservercos.yuewen.com/77D764/31147986804769406/epubprivate/OEBPS/Images/0074-04.jpg?sign=1738922633-4xXz9q0nIPIGOmOUTP2oKOXrauRERelS-0-3938f9bf1deb9967bd7395f1b60e8d37)
为避免这个错误,我们必须在实例化数组之后显式实例化1000个Point实例:
![](https://epubservercos.yuewen.com/77D764/31147986804769406/epubprivate/OEBPS/Images/0074-05.jpg?sign=1738922633-0DxM8GlgpuTPQXt2zEs7xvMB7jtMs5J6-0-200874d1006a356b89ed398f0b477779)
不论元素是何种类型,数组本身总是引用类型对象。例如,下面的语句是合法的:
![](https://epubservercos.yuewen.com/77D764/31147986804769406/epubprivate/OEBPS/Images/0074-06.jpg?sign=1738922633-4p5hZZjypy2rdp4FpqjJ7xbGwgV0jp9K-0-e41b600d5a170ce9921049bf667080ff)
2.7.2 索引和范围
C# 8引入了索引和范围的概念以简化对数组元素或局部数组的操作。
索引和范围可以和CLR类型Span<T>与ReadOnlySpan<T>配合使用(请参见第23章)。
自定义类型也可以定义类型为Index或Range的索引器来使用索引和范围(请参见3.1.9节)。
2.7.2.1 索引
在索引中可以使用^运算符从数组的末尾来引用数组元素。^1代表最后一个元素而^2代表倒数第二个元素,以此类推:
![](https://epubservercos.yuewen.com/77D764/31147986804769406/epubprivate/OEBPS/Images/0075-01.jpg?sign=1738922633-mJEYIFeWhvOyUZaMLmBJM1De0qFapsEV-0-cf71b438309892e1d9ca55d53e6b09e6)
(^0等于数组的长度,因此vowels[^0]将会产生错误。)
C#的Index类型实现了索引的功能,因此也可以使用如下方式来引用数组元素:
![](https://epubservercos.yuewen.com/77D764/31147986804769406/epubprivate/OEBPS/Images/0075-02.jpg?sign=1738922633-RApvLdRMa4eUYmEgwqTmB5cSxO9UrG7y-0-4153ea3a14aefae6fd77c2eeb500125c)
2.7.2.2 范围
范围使用..运算符得到数组的一个“切片”:
![](https://epubservercos.yuewen.com/77D764/31147986804769406/epubprivate/OEBPS/Images/0075-03.jpg?sign=1738922633-YMtfhdngDEHvcaYjzOM3DdEab4ayjPMc-0-c43ad30691f7bd8545921f260ef4b8d8)
注意,范围中的第二个数字是开区间的。因此..2的意思是返回vowels[2]之前的元素。
在范围中也可以使用^符号,例如,以下语句返回数组中的最后两个字符:
![](https://epubservercos.yuewen.com/77D764/31147986804769406/epubprivate/OEBPS/Images/0075-04.jpg?sign=1738922633-VxEt3SrYnZUEcAqv9HXy3JkkAJWHmhWp-0-697dd59c4449eaad76e5c6df5d8b306f)
C#的Range类型实现了范围的功能,因此我们也可以用如下方式来操作范围:
![](https://epubservercos.yuewen.com/77D764/31147986804769406/epubprivate/OEBPS/Images/0075-05.jpg?sign=1738922633-YaR76lksPzzzZA1JCgo1OGim2nrTYTn1-0-95a428f165acb7a99f94fe078b8545bf)
2.7.3 多维数组
多维数组分为两种类型:矩形数组和锯齿形数组。矩形数组代表n维的内存块,而锯齿形数组则是数组的数组。
2.7.3.1 矩形数组
矩形数组声明时用逗号分隔每个维度。下面的语句声明了一个矩形二维数组,它的维度是3×3:
![](https://epubservercos.yuewen.com/77D764/31147986804769406/epubprivate/OEBPS/Images/0075-06.jpg?sign=1738922633-gaIAWz17l1WwteYQf8UiAnFPe78deuDP-0-67752b43a7e722031b53044fd60e8af5)
数组的GetLength方法返回给定维度的长度(从0开始):
![](https://epubservercos.yuewen.com/77D764/31147986804769406/epubprivate/OEBPS/Images/0076-01.jpg?sign=1738922633-Hz6xrDwaHxzKlyh0fv301cR1Yzl5vt8y-0-3e3e9871873d5c9d5e26a20ddd8e2655)
矩形数组可以显式地以具体值来初始化。以下示例创建了一个和上例一样的数组:
![](https://epubservercos.yuewen.com/77D764/31147986804769406/epubprivate/OEBPS/Images/0076-02.jpg?sign=1738922633-DDg73tzJoKN2bc7tNWeghUnHaCvwbqha-0-b6f6a8ae432b8701571f5e7006c03904)
2.7.3.2 锯齿形数组
锯齿形数组在声明时用一对方括号表示一个维度。以下例子声明了一个最外层维度是3的二维锯齿形数组:
![](https://epubservercos.yuewen.com/77D764/31147986804769406/epubprivate/OEBPS/Images/0076-03.jpg?sign=1738922633-g97BWeUVNpNr18vwWKjbN5FqmXZxEr6O-0-20ad06156ecc9f08dbc4662a3484bbfa)
有意思的是,这里是new int[3][]而非new int[][3]。Eric Lippert有一篇精彩的文章(http://albahari.com/jagged)详细解释了这个问题。
不同于矩形数组,锯齿形数组内层维度在声明时并未指定,每个内层数组都可以是任意长度,每一个内层数组都隐式初始化为null而不是一个空数组,因此都需要手动创建:
![](https://epubservercos.yuewen.com/77D764/31147986804769406/epubprivate/OEBPS/Images/0076-05.jpg?sign=1738922633-hw5SltGXs5ICWnw8IcHfn3cvCSL7qk0B-0-4d6a39b7f5cf5048e0ba263baec3b11b)
锯齿形数组也可以使用具体值进行初始化。以下例子创建了一个和前面例子类似的数组,并在最后额外追加了一个元素:
![](https://epubservercos.yuewen.com/77D764/31147986804769406/epubprivate/OEBPS/Images/0076-06.jpg?sign=1738922633-nKwk7ZFZXWsCC4WDrIuMUUz1a2fAFMIf-0-ac5ea99ad9f5b5b3d7533153896c4d2b)
2.7.4 简化数组初始化表达式
有两种方式可以简化数组初始化表达式。第一种是省略new运算符和类型限制条件:
![](https://epubservercos.yuewen.com/77D764/31147986804769406/epubprivate/OEBPS/Images/0076-07.jpg?sign=1738922633-NWpiWL3IrcAGBYJGSW39zt111u7mqwlX-0-5abc1e8933c829627298ef84f78aef2b)
第二种是使用var关键字,使编译器隐式确定局部变量类型:
![](https://epubservercos.yuewen.com/77D764/31147986804769406/epubprivate/OEBPS/Images/0077-01.jpg?sign=1738922633-r5nZzdDorQNkFh0sRW7YS4liivNypWlw-0-e6577f77c2bb7ec95e3a1570530ab8f3)
数组类型可以进一步应用隐式类型转换规则,直接在new关键字之后忽略类型限定符,而由编译器推断数组类型:
![](https://epubservercos.yuewen.com/77D764/31147986804769406/epubprivate/OEBPS/Images/0077-02.jpg?sign=1738922633-6hyN11IqeeKJswgji0a9jcbKPqpkpkV9-0-0d3f77e6f78f727c61fc806dfa2d06d6)
为了使上述机制工作,数组中的所有元素必须能够隐式转换为一种类型(至少有一个元素是目标类型,而且最终只有一种最佳类型),例如:
![](https://epubservercos.yuewen.com/77D764/31147986804769406/epubprivate/OEBPS/Images/0077-03.jpg?sign=1738922633-28kZE2PrsezCvNRqFrt6ELYGNZ7hZuMd-0-4fa413683510121accfbe6c63c0485bd)
2.7.5 边界检查
运行时会为所有数组的索引操作进行边界检查。如果使用了不合法的索引值,就会抛出IndexOutOfRangeException异常:
![](https://epubservercos.yuewen.com/77D764/31147986804769406/epubprivate/OEBPS/Images/0077-04.jpg?sign=1738922633-6suygjpKtsTh6LHv69ACxE45mkFkLj9M-0-a41a77b33230f65e7b0e18ba3b002213)
数组边界检查在确保类型安全和简化调试过程中都是非常必要的。
通常,边界检查的性能开销很小,且JIT(即时编译器)也会对此进行优化。例如,在进入循环之前预先确保所有的索引操作的安全性来避免每次循环中都进行检查。另外C#还提供了unsafe代码来显式绕过边界检查(请参见4.18节)。