首页 > 编程笔记 > C#笔记 阅读:90

C#数组的用法(非常详细)

数组是固定数量的特定类型的变量集合(称为元素)。为了实现高效访问,数组中的元素总是存储在连续的内存块中。

C# 中的数组用元素类型后加方括号的方式表示:
char[] vowels = new char[5];  // 声明一个包含5个字符的数组

方括号也可用于检索数组,通过位置访问特定元素:
vowels[0] = 'a';
vowels[1] = 'e';
vowels[2] = 'i';
vowels[3] = 'o';
vowels[4] = 'u';
Console.WriteLine(vowels[1]);  // e
数组索引是从 0 开始的,所以上面的语句输出“e”。

我们可以使用 for 循环语句来遍历数组中的每一个元素。下面例子中的 for 循环将把整数变量 i 从 0 到 4 进行循环:
for (int i = 0; i < vowels.Length; i++)
    Console.Write(vowels[i]);  // aeiou
数组的 Length 属性返回数组中的元素数目。一旦数组创建完毕,它的长度将无法更改。System.Collection 命名空间和子命名空间提供了可变长度数组和字典等高级数据结构

我们可以使用数组初始化表达式声明数组并填充数组元素:
char[] vowels = new char[5]{'a','e','i','o',u};
或者简写为:
char[] vowels = {'a','e','i','o',u};

默认数组元素初始化

创建数组时总会用默认值初始化数组中的元素,类型的默认值是按位取 0 的内存表示的值。

例如,若定义一个整数数组,由于 int 是值类型,因此该操作会在连续的内存块中分配 1000 个整数。每一个元素的默认值都是 0:
int[] a = new int[1000];
Console.Write(a[123]);  // 0

值类型和引用类型的区别

数组元素的类型是值类型还是引用类型对其性能有重要的影响。

若元素类型是值类型,每个元素的值将作为数组的一部分进行分配,例如:
Point[] a = new Point[1000];
int x = a[500].X;  // 0

public struct Point { public int X, Y; }

若 Point 是类,创建数组则仅仅分配了 1000 个空引用:
Point[] a = new Point[1000];
int x = a[500].X;  // 运行时错误,NullReferenceException

public class Point { public int X, Y; }

为避免这个错误,我们必须在实例化数组之后显式实例化 1000 个 Point 实例:
Point[] a = new Point[1000];
int x = a[500].X;  // 运行时错误,NullReferenceException

public class Point { public int X, Y; }

不论元素是何种类型,数组本身总是引用类型对象。例如,下面的语句是合法的:
int[] a = null;

索引和范围

C# 8 引入了索引和范围的概念以简化对数组元素或局部数组的操作,可以和 CLR 类型 Span<T> 与 ReadOnlySpan<T> 配合使用。

自定义类型也可以定义类型为 Index 或 Range 的索引器来使用索引和范围。

1) 索引

在索引中可以使用 ^ 运算符从数组的末尾来引用数组元素。^1 代表最后一个元素而 ^2 代表倒数第二个元素,以此类推:
char[] vowels = new char[] {'a', 'e', 'i', 'o', 'u'};
char lastElement = vowels[^1];  // 'u'
char secondToLast = vowels[^2]; // 'o'

^0 等于数组的长度,因此 vowels[^0] 将会产生错误。


C# 的 Index 类型实现了索引的功能,因此也可以使用如下方式来引用数组元素:
Index first = 0;
Index last = ^1;
char firstElement = vowels[first];  // 'a'
char lastElement = vowels[last];   // 'u'

2) 范围

范围使用 .. 运算符得到数组的一个“切片”:
char[] firstTwo = vowels[..2];    // 'a', 'e'
char[] lastThree = vowels[2..];   // 'i', 'o', 'u'
char[] middleOne = vowels[2..3];  // 'i'
注意,范围中的第二个数字是开区间的。因此..2的意思是返回 vowels[2] 之前的元素。

在范围中也可以使用^符号,例如,以下语句返回数组中的最后两个字符:
char[] lastTwo = vowels[^2..];  // 'o', 'u'

C# 的 Range 类型实现了范围的功能,因此我们也可以用如下方式来操作范围:
Range firstTwoRange = 0..2;
char[] firstTwo = vowels[firstTwoRange];  // 'a', 'e'

C#多维数组

多维数组分为两种类型:矩形数组和锯齿形数组。矩形数组代表 n 维的内存块,而锯齿形数组则是数组的数组。

1) 矩形数组

矩形数组声明时用逗号分隔每个维度。下面的语句声明了一个矩形二维数组,它的维度是 3×3:
int[,] matrix = new int[3,3];

数组的 GetLength() 方法返回给定维度的长度(从 0 开始):
for (int i = 0; i < matrix.GetLength(0); i++)
    for (int j = 0; j < matrix.GetLength(1); j++)
        matrix[i,j] = i * 3 + j;

矩形数组可以显式地以具体值来初始化。以下示例创建了一个和上例一样的数组:
int[,] matrix = new int[,]
{
    {0, 1, 2},
    {3, 4, 5},
    {6, 7, 8}
};

2) 锯齿形数组

锯齿形数组在声明时用一对方括号表示一个维度。以下例子声明了一个最外层维度是 3 的二维锯齿形数组:
int[][] matrix = new int[3][];
这里是 new int[3][] 而非 new int[][3]。

不同于矩形数组,锯齿形数组内层维度在声明时并未指定,每个内层数组都可以是任意长度,每一个内层数组都隐式初始化为 null 而不是一个空数组,因此都需要手动创建:
for (int i = 0; i < matrix.Length; i++)
{
    matrix[i] = new int[3];  // 创建内部数组
    for (int j = 0; j < matrix[i].Length; j++)
        matrix[i][j] = i * 3 + j;
}

锯齿形数组也可以使用具体值进行初始化。以下例子创建了一个和前面例子类似的数组,并在最后额外追加了一个元素:
int[][] matrix = new int[][]
{
    new int[] {0, 1, 2},
    new int[] {3, 4, 5},
    new int[] {6, 7, 8, 9}
};

简化数组初始化表达式

有两种方式可以简化数组初始化表达式。第一种是省略 new 运算符和类型限制条件:
char[] vowels = {'a', 'e', 'i', 'o', 'u'};

int[,] rectangularMatrix =
{
    {0, 1, 2},
    {3, 4, 5},
    {6, 7, 8}
};

int[][] jaggedMatrix =
{
    new int[] {0, 1, 2},
    new int[] {3, 4, 5},
    new int[] {6, 7, 8, 9}
};

第二种是使用 var 关键字,使编译器隐式确定局部变量类型:
var i = 3;            // i 隐式地为 int 类型
var s = "sausage";    // s 隐式地为 string 类型

// 因此:

var rectMatrix = new int[,]  // rectMatrix 隐式地为 int[,] 类型
{
    {0, 1, 2},
    {3, 4, 5},
    {6, 7, 8}
};

var jaggedMat = new int[][]  // jaggedMat 隐式地为 int[][] 类型
{
    new int[] {0, 1, 2},
    new int[] {3, 4, 5},
    new int[] {6, 7, 8, 9}
};

数组类型可以进一步应用隐式类型转换规则,直接在 new 关键字之后忽略类型限定符,而由编译器推断数组类型:
var vowels = new[] {'a', 'e', 'i', 'o', 'u'};  // 编译器推断为 char[]

为了使上述机制工作,数组中的所有元素必须能够隐式转换为一种类型(至少有一个元素是目标类型,而且最终只有一种最佳类型),例如:
var x = new[] {1, 10000000000000L};  // 都可以转换为 long

边界检查

运行时会为所有数组的索引操作进行边界检查。如果使用了不合法的索引值,就会抛出 IndexOutOfRangeException 异常:
int[] arr = new int[3];
arr[3] = 1;  // 抛出 IndexOutOfRangeException
数组边界检查在确保类型安全和简化调试过程中都是非常必要的。通常边界检查的性能开销很小,且 JIT(即时编译器)也会对此进行优化。

例如,在进入循环之前预先确保所有的索引操作的安全性来避免每次循环中都进行检查。另外 C# 还提供了 unsafe 代码来显式绕过边界检查。

相关文章