1.声明和创建数组
数组:无序的元素序列。数组中的所有元素都具有相同类型。数组中的元素存储在一个连续性的内存块中,并通过索引来访问。
1.1声明数组变量
语法:先写它的元素类型名称,后跟一对方括号([ ]),最后写变量名。
例如:
int[] array;
数组元素并非只能是基本数据类型。还可以是结构、枚举或类。
例如:为了创建由Date结构构成的数组,可以像下面这样写:
Date [] date;
1.2创建数组实例
无论元素是什么类型,数组始终都是引用类型。
为了创建数组实例,要先写new关键字,后跟元素的类型名称,然后在一对方括号中指定要创建的数组的大小。创建数组实例时,会使用默认值(0,null 或者false,分别取决于是数值类型,是引用类型,还是Boolean类型)对其元素进行初始化。例如,针对早先声明的pins数组变量,以下语句创建并初始化由4个整数构成的新数组:
int [] pins;
pins = new int[4];
内存图:
动态分配数组大小:
int size = int.Parse(Console.ReadLine());
int [] pins = new int[size];
1.3填充和使用数组
大括号中的值的数量必须和要创建的数组实例的大小完全匹配:
int[] pins = new int[3]{ 9, 3, 7, 2 }; //编译时错误
int[] pins = new int[4]{ 9, 3, 7);//编译时错误
int[] pins = new int[4]{ 9, 3, 7, 2 }; // 正确
初始化数组变量时可以省略new表达式和数组大小。编译器根据初始值的数量来计算
大小,并生成代码来创建数组。例如:
int [] pins = {9,3,7,2};
创建由结构或对象构成的数组时,可以调用构造器来初始化数组中的每个元素,例如:
Time [] schedule = { new Time(12,30),new Time(5,30)};
1.4创建隐式类型的数组
声明数组时,元素类型必须与准备存储的元素类型匹配。例如,将pins 声明为int类型的数组(就像前面的例子那样),就不能把double, struct, string 或其他非int类型的值保存到其中。如果在声明数组时指定了初始值列表,就可以让C#编译器自已推断数组元素的类型,如下所示:
var names = new[]{"John","Diana","James"};
在这个例子中,C#编译器推断names是string类型的数组变量。注意语法有两个特别之处。首先,类型后的方括号没了,本例中的names变量被直接声明为var,而不是var[]。其次,必须在初始值列表之前添加new[]。
使用这个语法,必须保证所有初始值都有相同类型。下例将导致编译器报错:“找不到隐式类型数组的最佳类型。"
var bad = new[]{ "John","Diana",23,54};
但有时编译器会把元素转换为不同的类型一前 提是结果有意义。下例的numbers会被推断成double数组,因为常量3.5和99.999都是double值,而C#编译器能将整数值1和2转换成double:
var numbers = new[]{1,2,3.5,99.999};
一般最好避免混合使用多种类型,不要单纯寄希望于编译器帮自己转换。
隐式类型的数组尤其适合第7章描述的匿名类型。以下代码创建由匿名对象构成的数
组,其中每个对象都包含两个字段,分别指定了我的家庭成员的姓名和年龄:
var name = new[] { new {Name = "John", Age = 50 ],
new (Name = "Diana", Age= 50 }
new {Name = "James", Age = 23 },
new {Name = "Francesca", Age = 21 } };
匿名类型中的字段对于每个数组元素都必须相同。
1.5访问单独的数组元素
必须通过索引来访问单独的数组元素。数组索引基于零,第一个元素的索引是0而不
是1。索引1访问的是第二个元素。例如,以下代码将pins数组的索引为2的元素(第三
个元素)的内容读入-一个int变量:
int myPin;
myPin = pins[2];
类似地,可通过索引向元素赋值来更改数组内容:
myPin = 1645;
pins[2] = myPin;
所有数组元素访问都要进行边界(上下限)检查。使用小于日或大于等于数组长度的整
数索引,编译器会抛出IndexoutofRangeException异常,如下例所示:
try
{
int[] pins = {9,3,7,2};
Console.WriteLine(pins[4]); //错误,第4个也是最后一个元素的索引是3
}
catch(IndexOutOfRangeException ex)
{
…
}
1.6遍历数组
所有数组都是Microsoft .NET Framework的System .Array类的实例,该类定义了许多有用的属性和方法。例如,可以查询Length 属性来了解数组中包含多少个元素,并借助for语句来遍历所有元素。下例将pins数组的各个元素的值输出到控制台:
int[] pins = {9,3,7,2};
for(int index = 0;index <pins.Length;index++)
{
int pin = pins[index];
Console.WriteLine(pin);
}
再介绍一个foreach语句来遍历数组:
int[] pins = {9,3,7,2};
foreach (int pin in pins)
{
Console.WriteLine(pin);
}
foreach语句是遍历数组的首选方式,它更明确地表达了代码地目的,而且避免了使用for循环地麻烦。但少数情况下for语句更佳,如下所示:
(1)foreach语句总是遍历整个数组。如果只想遍历数组的一部分(例如前 半部分),或者希望中途跳过特定元素(例如每隔两个元素就跳过一个), 那么使用for语句将更容易。
(2)foreach 语句总是从索引0遍历到索引Length-1。要反向或者以其他顺序遍历,更简单的做法是使用for语句。
(3)如果循环主体需要 知道元素的索引,而非只是元素的值,就必须使用for语句。
(4)修改数组元素必须使用 for语句。这是因为foreach语句的循环变量是数组的每个元素的只读拷贝。
1.7数组作为方法参数和返回值传递
方法可获取数组类型的参数,也可把它们作为返回值传递。
例如:数组作为参数
public void ProcessDate(int[] data)
{
foreach(int i in data)
{
//…
}
}
方法要返回一个数组,返回类型必须是数组类型。方法内部要创建并填充数组。下例提示用户输入数组大小,再输入每个元素的数据。最后,方法返回创建好的数组。
public int[] ReadData()
{
Console .WriteLine("How many elements?");
string reply = Console. ReadLine();
int numElements = int. Parse(reply);
int[] data = new intfnumElements];
for (int i = 0; i < numElements; i++)
{
Console.WriteLine($"Enter data for element ({1});
reply = Console.ReadLine();
int elementData = int.Parse(reply);
data[i] = elementData;
}
return data;
}
可像下面这样调用ReadData:
int[] data = ReadData();
补充:
Main方法的数组参数
你可能早已注意到应用程序的Main方法获取- 个字符串數组作为参数:
static void Main()
{
//…
}
Main方法是程序运行时的入口方法。从命令行启动程序时,可以指定附加的命令行参数。Microsof Windows操作系统将这些参数传给CLR,后者将它们作为实参传給Main方法。这个机制允许在程序开始运行时直接提供信息,而不必交互式地提示输入信息。编写能通过自动脚本运行的实用程序时,这个机制相当有用。下例来自一个用于文件处理的MyFileUtil实用程序。它允许在命令行输入一一组文件名,然后调用ProcessFile方法(这里
没有显示)处理每个文件:
static vold Main(string[] args)
{
foreach (string fllename in args)
{
Procesflle(filename);
}
}
可以在命令行上像下面这样运行MyFileUil程序:
MyFlleUtil C:\Temp\TestDatadat C: \Users\John\Documents\MyDoc .txt
每个命令参数都以空格分隔。由MyFileUil程序负责验证实参的有效性。
1.8复制数组
数组是引用类 型(记住,数组是System.Array类的实例)。数组变量包含对数组实例的引用。这意味着在复制了数组变量之后,将获得对同一个数组实例的两个引用。例如:
int[] pins = {9,3,7,2};
int[] alias = pins;//alias和pins现在引用同一个数组实例
在这个例子中,修改pins[1]的值,读取alias[1]时也会看到改动。要完全复制数组实例,获得堆上实际数据的拷贝,必须做两件事情。首先,必须创建类型和大小与原始数组一样的新数组实例,然后将数据元素从原始数组逐个复制到新数组,如下例所示:
int[] pins={9,3, 7,2 };
int[] copy =new int[pins .Length];
for (int 1=0;1 < copy.Length; 1++)
{
copy[i] = pins[i];
}
注意,上例使用原始数组的Length属性指定新数组大小。
使用System. Array类提供了一些方法来复制数组:
(1)CopyTo()
下例从索引0开始将pins数组的所有元素复制到copy数组:
int[] pins - {9,3,7, 2};
int[] copy = new int[pins.Length];
pins.CopyTo(copy, 0);
(2)Copy()
复制值的另一个办法是使用System.Array的静态方法Copy。和CopyTo一样, 目标数组必须在调用Copy之前初始化:
int[] pins={9,3,7, 2};
int[] copy = new int[pins ,Length];
Array,Copy(pins, copy, copy. Length);
注意:Array.Copy方法的长度参数必须是一个有效的值。提供负值会抛出ArgumentoutOfRangeException异常。提供比元素数量大的值,会抛出ArgumentException异常。
(3)Clone()
使用System. Array的实例方法Clone。它允许在一次调 用中创建数组并完成复制。
int[] pins={9, 3,7,2};
int[] copy = (int[])pins.Clone();
1.9使用多维数组
下列代码创建包含24个整数的二维数组items,可将二维数组想象成表格,第一维是表行,第二维是表列。
int[ ; ] items =new int[4, 6];
访问二维数组元素:
items[2, 3]=99;//将单元格(2, 3)的元素设为99
items[2, 4] = items [2,3]; /将单元格(2, 3)的元素复制到单元格(2, 4)
items[2, 4]++;//递增单元格(2, 4)的整数值
数组维数没有限制。以下代码创建并使用名为cube的三维数组。访问三维数组的元素必须指定3个索引。
int[, ] cube . new int[5, 5, 5];
cube[1, 2, 1] .101;
cube[1, 2, 2]=cube[1, 2, 1]*3;
使用超过三维的数组时要小心,数组可能耗用大量内存。上例的cube 数组包含125个元素(5*5*5)。而对于每一维大小都是5的四维数组,则总共包含625个元素。使用多维数组时,一般都要准备好捕捉并处理OutOfMemoryException异常。
1.10创建交错数组
因为多维数组可能消耗大量内存。如果应用程序只使用每一列的部分数据,为未使用的元素分配内存就是巨大的浪费。这时可以考虑使用交错数组(或称为不规则数组),其每一列的长度都可以不同,如下所示:
int[][] items=new int[4][];
int[] columnForRow0 =new int[3];
int[] columnForRow1 =new int[10];
int[] columnForRow2 =new int[40];
int[] columnForRow3 =new int[25];
items[0]=columnForRov0;
items[1]=colunnForRow1;
items[2]=columnForRow2;
items[3]=columnForRow3;
本例第一列3个元素,第二列10个元素,第三列40个元素,最后一列25个元素。交错数组其实就是由数组构成的数组。和二维数组不同,交错数组只有一维,但那一-维中的元素本身就是数组。除此之外,items 数组的总大小是78个元素而不是160个:不用的元素不分配空间。
参考书籍:《Visual C#从入门到精通》