数组是最通用的数据结构,它出现在几乎所有的编程语言里。在 C#语言中使用数组包括创建 System.Array 类型的数组对象,以及创建针对所有数组的抽象的基类型。Array 类提供了一套方法,这些方法是为了执行诸如排序和查找这类过去需要程序员手工实现的任务。
C#语言中另外一种使用数组的有趣替换方式就是 ArrayList类。ArrayList是一种像要更多空间来动态生长的数组。对于无法精确知道数组最终大小的情况,或者对于程序生命周期内数组大小可能会发生一点变化的情况,用 ArrayList比用数组更合适。
本章将简要介绍 C#语言中使用数组的基本概念,然后继续展开更加深入的主题,这其中包括复制、克隆、相等判定,以及使用 Array 类和ArrayList类的静态方法。
2.1数组基本概念
数组是可索引的数据的集合。数据既可以是内置的类型,也可以是用户自定义的类型。事实上,把数组数据称为对象大概是最简便的方式。C#语言中的数组实际上就是对象本身,因为它们都来源于 System.Array 类。既然数组是System.Array 类的一个声明实例,所以在使用数组时也可以使用此类的所有方法和属性。
2.1.1数组的声明和初始化
这里采用下列语法规则对数组进行声明:
type[ ] array-name;
这里的类型就是数组元素的数据类型。下面是一个实例:
string[ ] names;
接下来一行需要实例化数组(既然它是 System.Array类型的一个对象),还需要确定数组的大小。下面这行就实例化了刚声明的 name数组,并且预留了五个字符串的内存空间:
names = new string[10];
必要时还可以把上述这两条语句合并成为一条语句:
string[ ] names = new string[10];
当想要在一条语句中对数组进行声明、例示以及赋值操作时都要花费时间。在 C#语言中可以采用初始化列表的方式来实现:
int[ ] numbers = new int[ ] {1, 2, 3, 4, 5};
上述这个数的列表被称为是初始化列表。它用一对大括号作为界定符,并且每个元素之间用逗号进行分割。当用这种方法来声明数组时,不需要指定元素的个数。编译器会通过初始列表中数据项的数量来推断出此数据。
2.1.2数组元素的设置和存取访问
存储数组元素既可以采用直接存取访问的方法也可以通过调用 Array 类的 SetValue 方法。直接存取方式通过赋值语句左侧的索引来引用数组位置:
nNames[2] = "Raymond";
sSales[19] = 23123;
而 SetValue 方法则提供了一种更加面向对象的方法来为数组元素赋值。这种方法会取走两个参数,一个是索引数,另一个则是元素的值。
names.SetValue("Raymond", 2);
sales.SetValue(23123, 9);
数组元素的访问既可以通过直接存取的方式也可以通过调用GetValue方法的方式。 GetValue方法取走单独一个参数——即索引。
myName = names[2];
monthSales = sales.GetValue([19)];
为了存取每一个数组元素用 For 循环来循环遍历数组是一种通用的方法。程序员在编写循环时常犯的错误即可能是写死循环的上限值(如果数组是动态的,那么这样做就是错误的,因为循环的上限可能会改变),也可能是每次循环重复时调用函数来存取循环的上限:
(for( int i = 0; i <= sales.GetUpperBound(0); i++)
totalSales = totalSales + sales[i];
2.1.3取回数组元数据的方法和属性
Array类为取回数组元数据提供了几种属性:
Length:返回数组所有维数内元素的总数量。
GetLength:返回数组指定维数内元素的数量。
Rank:返回数组的维数。
GetType:返回当前数组实例的类型。
Length 方法对于计算多维数组中元素的数量以及返回数组中元素的准确编号都是很有用的。另外,还可以使用GetUpperBound方法,而且要对数值加一。
既然 Length 返回数组元素的总数量,所以 GetLength 方法统计了数组某一维内元素的数量。这种方法和 Rank属性一起可用来在运行时调整数组的大小,而且不必冒丢失数据的风险。此方法将在本章的后续内容中进行讨论。
在无法确定数组类型的情况下,GetType 方法可以用来确定数组的数据类型,比如数组作为参数传递给方法的时候。在下列代码段中,为了确定对象是否是数组,这里创建了一个类型变量 Type,此变量允许用来调用类方法IsArray。如果对象是一个数组,那么代码返回数组的数据类型。
int[] numbers;
numbers = new int[] { 0, 1, 2, 3, 4 };
Type arrayType = numbers.GetType();
if (arrayType.IsArray)
Console.WriteLine("The array type is: {0}", arrayType);
else
Console.WriteLine("Not an array");
Console.Read();
Gettype方法不仅返回数组的类型,而且还让大家明白对象确实是一个数组。下面是代码的输出:
The array type is: System.Int32[]
这里的方括号说明对象是一个数组。还需要注意在显示数据类型的时候采用了一种格式。这里必须这么做,因为要把 Type 数据与显示的字符串的剩余部分相连接就不能把Type数据转变成为字符串。
2.1.4多维数组
到目前为止的讨论只限于一维数组的情况。在C#语言中,尽管数组多于三维的情况是非常少见的(而且也是非
常容易使人混乱的),但是数组还是可以达到32维的。
通过提供数组每一维上限值的方式可以声明多维数组。二维数组的声明:
int [ , ] grades = new int [4,5] ;
此语句声明了一个4 行5列的数组。二维数组经常用来模拟矩阵。
声明多维数组也可以不指定维数的上限值。要想这样做就要用到逗号来明确数组的维数。例如,声明一个二维
数组如下所示
double [ , ] Sales ;
再比如声明一个三维数组,
double [ , , ] Sales ;
在声明不带维数上限的数组的时候,需要稍后对具有这类上限的数组重新确定维数:
sales = new double [4,5] ;
对多维数组可以用初始化表进行初始化操作。请看下面这条语句:
Iint[,] grades = new int[,]
{
{1, 82, 74, 89, 100},
{2, 93, 96, 85, 86},
{3, 83, 72, 95, 89},
{4, 91, 98, 79, 88}
};
首先要注意这里没有指明数组的上限。当初始化带有初始化表的数组的时候,不用说明数组的上限。编译器会根据初始化表中数据计算出每一维的上限值。初始化表本身也像数组的每一行那样用大括号进行标记。数组行内的每一个元素则用逗号进行分割。
存取访问多维数组元素的方法类似于存取一维数组元素的方法。大家可以采用传统的数组存取访问方式,
grade = gGrades[2,2];
gGrades([2,2]) = 99
也可以采用 Array 类的方法:
grade = Grades.GetValue([0,2)];
但是,对多维数组不能使用 SetValue 方法。这是因为这种方法只接收两个参数:一个数值和一个单独的索引。
尽管常常是基于存储在数组行中的数值或者是基于存储在数组列中的数值进行计算,但是 对多维数
组上所有元素的计算还是很常见的操作。假设有一个 Grades数组,且数组的每一行是一条学生记录,那么就能如下
所示计算出每个学生的平均成绩:
int[,] grades = new int[,]
{
{1, 82, 74, 89, 100},
{2, 93, 96, 85, 86},
{3, 83, 72, 95, 89},
{4, 91, 98, 79, 88}
};
int last_grade = grades.GetUpperBound(1);
double average = 0.0;
int total;
int last_student = grades.GetUpperBound(0);
for(int row = 0; row <= last_student; row++)
{
total = 0;
for (int col = 0; col <= last_grade; col++)
total += grades[row, col];
average = total / last_grade;
Console.WriteLine("Average: " + average);
}
2.1.5参数数组
大多数的方法定义要求一套提供给方法的参数的数目,但是想要编写一个允许可选参数数目的方法定义是需要时间的。用一种称为参数数组的构造就可以做到。
通过使用关键字 ParamArray 就可以在方法定义的参数列表中指明参数数组。下面的方法定义允许提供任意数量的数作为参数,并且方法会返回数的总量:
static int sumNums(params int[] nums)
{
int sum = 0;
for (int i = 0; i <= nums.GetUpperBound(0); i++)
sum += nums[i];
return sum;
}
此方法可以处理下列任意一种调用:
total = sumNums(1, 2, 3);
当用参数数组定义方法的时候,为了使编译器能够正确处理参数列表,需要在参数列表的最后提供参数数组的参数。否则,编译器无法知道参数数组元素的截止位置以及方法其他参数的起始位置。
2.1.6锯齿状数组
在创建一个多维数组的时候,需要始终新建一种每行都有相同元素数量的结构。例如,下面这个数组的声明:int sales[,] = new int[12,30]; ‘// Sales for each day of each month
这个数组假设每行(即月数)都有相同的元素(即天数)数量,但是大家知道某些月有 30 天,而某些月是 31天,还有一个月是 29 天。因而,这个刚刚声明的数组会有几个空元素在其中。对于这个数组而言,这不是太大的问题,但是对于更加庞大的数组而言,就需要减少大量浪费的空间。
解决这个问题的方法是用锯齿状数组代替二维数组。锯齿状数组是一种每行都能组成一个数组的数组。锯齿状数组的每一维就是一个一维数组。大家称其为“锯齿状”数组的原因是由于数组每一行元素的数量都可能不相同。锯齿状数组的图形不是正方形或矩形,而是具有不均匀边缘或锯齿边缘的图形。
锯齿状数组的声明需要通过在数组变量名后放置两组方括号的方式来完成。第一组方括号说明了数组的行数。第二组方括号则是留白的。这为存储在每行内的一维数组标记了位置。通常情况下,声明语句的初始化列表会设置行数,就像下列这样: int[][] jagged = new int[12][];
这条语句看上去很奇怪,但是把它分解后就一目了然了。jagged 是一个有着 12 个元素的整数数组,其中的每个元素又是一个整数数组。初始化列表实际上就是对数组行的初始化,这表明每一个行元素都是一个有着 12 个元素的数组,而且每个元素都初始化为默认的值。
一旦声明了锯齿状的数组,就可以分别对各自行数组的元素进行赋值操作了。下面这段代码对 jaggedArray 进行了赋值操作:
jagged[0][0] = 23;
jagged[0][1] = 13;
. . .
jagged[7][5] = 45;
第一组方括号说明了行编号,而第二组方括号则表明了行数组的元素。第一条语句存取访问到第一个数组的第一个元素,接着第二条语句存取访问了第一个数组的第二个元素,而第三条语句存取访问的则是第八个数组的第六个元素。
为了做一个使用锯齿状数组的实例,下边这段程序创建了一个名为 sales 的数组(用来跟踪两个月内每星期的销售情况),并且把销售额赋值给数组的元素,然后循环遍历整个数组从而计算出存储在数组内的每月一个星期的平均销售额。
using System; class class1 { static void Main()[] { int[] Jan = new int[31]; int[] Feb = new int[29]; int[][] sales = new int[][] { Jan, Feb }; int month, day, total; double average = 0.0; sales[0][0] = 41; sales[0][1] = 30; sales[0][0] = 41; sales[0][1] = 30; sales[0][2] = 23; sales[0][3] = 34; sales[0][4] = 28; sales[0][5] = 35; sales[0][6] = 45; sales[1][0] = 35; sales[1][1] = 37; sales[1][2] = 32; sales[1][3] = 26; sales[1][4] = 45; sales[1][5] = 38; sales[1][6] = 42; for(month = 0; month <= 1; month++) { total = 0; for(day = 0; day <= 6; day++) { total += sales[month][day]; } average = total / 7; Console.WriteLine("Average sales for month: " +month + ": " + average); } } }
2.2ArrayList 类
当无法提前知道数组的大小或者在程序运行期间数组的大小可能会发生改变的时候,静态数组就不是很适用了。这类问题的一种解决方案就是当数组超出存储空间的时使用能够自动调整自身大小的数组类型。这种数组被称为是ArrayList。它是.NET框架库中System.Collections命名空间的内容。
ArrayList对象拥有可存储数组大小尺寸的Capacity属性。该属性的初始值为 16。当ArrayList中元素的数量达到此界限值时,Capacity属性就会为ArrayList的存储空间另外增加16个元素。在数组内元素数量有可能扩大或缩小的情况下使用 ArrayList会比用带标准数组的 ReDim Preserver更加有效。
就像第 1章讨论过的那样,ArrayList用Object类型来存储对象。如果需要强类型的数组,就应该采用标准数组或者其他一些数据结构。
2.2.1ArrayList 类的成员
ArrayList类包含几种用于ArrayList的方法和属性。下面这个列表就是最常用到的一些方法和属性:
l Add( ):向ArrayList添加一个元素。
l AddRange( ):在ArrayList末尾处添加群集的元素。
l Capacity:存储ArrayList所能包含的元素的数量。
l Clear( ):从ArrayList中移除全部元素。
l Contains( ):确定制定的对象是否在 ArrayList内。
l Copy To( ):把ArrayList或其中的某一段复制给一个数组。
l Count:返回ArrayList中当前元素的数量。
l GetEnumerator( ):返回迭代 ArrayList的计数器。
l GetRange( ):返回ArrayList的子集作为ArrayList。
l IndexOf( ):返回指定数据项首次出现的索引。
l Insert( ):在 ArrayList的指定索引处插入一个元素。
l InsertRange( ):从ArrayList指定索引处开始插入群集的元素。
l Item( ):在指定索引处获取或者设置一个元素。
l Remove( ):移除指定数据项的首次出现。
l RemoveAt( ):在指定索引处移除一个元素。
l Reverse( ):对 ArrayList中元素的顺序进行反转。
l Sort( ):对ArrayList中的元素按照阿拉伯字母表顺序进行排序。
l ToArray( ):把ArrayList的元素复制给一个数组。
l TrimToSize( ):为ArrayList中元素数量设置ArrayList的容量。
2.2.2应用 ArrayList类
ArrayList的使用不同于标准数组。除非事出有因要把数据项添加到特殊位置上,否则通常情况下使用Add方法只是向 ArrayList添加数据项,而对于上述特殊情况就要采用Insert方法来进行操作了。本节会讨论如何使用这些操作及ArrayList类的其他成员。
首先要做的事情就是如下所示那样声明 ArrayList:
ArrayList grades = new ArrayList();
注意此声明中使用到了构造器。如果 ArrayList没有声明使用构造器,那么在后续程序语句里就无法获得对象。 用Add方法把对象添加ArrayList。此方法会取走一个参数,即添加给ArrayList的对象。Add方法也会返回一个整
数用来说明 ArrayList中被添加元素的位置,当然这个值是很少会在程序中用到的。下面是一些实例:
grades.Add(100);
grades.Add(84);
int position;
position = grades.Add(77);
Console.WriteLine("The grade 77 was added at position:" + position);
用 For Each循环可以把ArrayList中的对象显示出来。ArrayList有一个内置计数器用来记录循环遍历ArrayList内所有对象的次数,而且是每次一个。下面这段代码就说明了对 ArrayLsit使用For Each循环的方法:
int total = 0;
double average = 0.0;
foreach (Object grade in grades)
total += (int)grade;
average = total / grades.Count;
Console.WriteLine("The average grade is: " + average);
如果需要在 ArrayList某个特殊位置上添加元素,则可以采用 Insert方法。此方法会取走两个参数:插入元素的索引,以及要插入的元素。下面这段代码为了保持ArrayList内对象的次序而在指定位置上插入了两个成绩:
grades.Insert(1, 99);
grades.Insert(3, 80);
通过调用Capacity属性可以检查ArrayList当前的容量,而通过调用Count属性可以确定ArrayList中元素的数量:
Console.WriteLine("The current capacity of grades is:" + grades.Capacity);
Console.WriteLine("The number of grades in grades is:" + grades.Count);
这里有几种从 ArrayList中移除数据项的方法。如果知道要移除的数据项,但又不确定它所处的位置,那么就可以采用 Remove方法。此方法会取走一个参数,即要从ArrayList中移除的对象。如果ArrayList内有这个对象,就可以把它移除掉。如果此对象不在ArrayList内,那就什么也做。当使用像 Remove 这样的方法时,典型做法是把方法放置在 If-Then 语句内进行调用,并且使用诸如 Contains 这样的方法来验证对象确实存在 ArrayList 内。下面是一个。
代码段实例:
if (grades.Contains(54))
grades.Remove(54)
else
Console.Write("Object not in ArrayList.");
如果知道所要移除数据项的索引,那么可以使用 RemoveAt 方法。此方法会取走一个参数,即要移除对象的索引。唯一能接受的人为错误就是给方法传递一个无效的索引。此方法的工作形式如下所示: grades.RemoveAt(2)
通过调用 IndexOf 方法可以确定 ArrayList 中某个对象的位置。这种方法会取走一个参数,即一个对象,然后返回此对象在 ArrayList 内的位置。如果对象不在 ArrayList 内,那么方法就会返回-1。下面这段代码把 IndexOf 方法与RemoveAt方法结合在一起使用:
int pos;
pos = grades.IndexOf(70);
grades.RemoveAt(pos);
除了向 ArrayList中添加单独的对象,还可以添加对象的范围。对象必须存储在来源于ICollection 的数据类型里面。这就意味着可以把对象存储在数组里,或存储在 Collection里,甚至是存储到另一个ArrayList里面。
有两种不同的方法可以用来给 ArrayList 添加范围。它们是 AddRange 方法和 InsertRange 方法。AddRange 方法会把对象的范围添加到 ArrayList的末尾处,而 InsertRange 方法则会把范围添加到 ArrayList内指定的位置上。
下面这段程序说明了如何使用这两种方法:
using System; using System.Collections; class class1 { static void Main() { ArrayList names = new ArrayList(); names.Add("Mike"); names.Add("Beata"); names.Add("Raymond"); names.Add("Bernica"); names.Add("Jennifer"); Console.WriteLine("The original list of names: "); foreach (Object name in names) Console.WriteLine(name); Console.WriteLine(); string[] newNames = new string[] { "David", "Michael" }; ArrayList moreNames = new ArrayList(); moreNames.Add("Terrill"); moreNames.Add("Donnie"); moreNames.Add("Mayo"); moreNames.Add("Clayton"); moreNames.Add("Alisa"); names.InsertRange(0, newNames); names.AddRange(moreNames); Console.WriteLine("The new list of names: "); foreach (Object name in names) Console.WriteLine(name); } }
此程序输出是:
Dvid
Michael
Mike
Bernica
Beata
Raymond
Jennifer
Terrill
Donnie
Mayo
Clayton
Alisa
因为指定的索引为 0,所以是在 ArrayList 开始处添加了前两个名字。而后面的几个名字由于使用了 AddRange方法而被添加到了末尾处。
许多程序员还找到了另外两种非常有用的方法ToArray方法和GetRange方法。 GetRange方法会返回来自ArrayList的对象的范围作为另外一个 ArrayList。而 ToArray 方法则会把 ArrayList 的所有元素复制给一个数组。首先来看一看GetRange方法。
GetRange 方法会取走两个参数:起始索引以及要从 ArrayList 找回的元素数量。GetRange 方法没有破坏性,因
为这只是把对象从原始 ArrayList 复制给新的 ArrayList。下面这个实例采用和上述相同的程序来说明此方法的工作原
理:
ArrayList someNames = new ArrayList();
someNames = names.GetRange(2, 4);
Console.WriteLine("someNames sub-ArrayList: ");
foreach (Object name in someNames)
Console.WriteLine(name);
这个程序段的输出是:
Mike
Bernica
Beata
Raymond
ToArray方法允许把ArrayList的内容轻松传递给一个标准数组。采用 ToArray 方法的主要原因就是由于用户需要更快的数组存取访问速度。
ToArray方法不带参数,但是会把 ArrayList的元素返回给数组。下面这个例子就说明了此方法的使用原理:
Object[] arrNames;
arrNames = names.ToArray();
Console.WriteLine("Names from an array: ");
for(int i = 0; i <= arrNames.GetUpperBound(0); i++)
Console.WriteLine(arrNames[i]);
这段代码的后半部分证明了确实把来自 ArrayList的元素存储到了数组 arrNames里面。
小结
数组是计算机编程中最常采用的数据结构。即使不是全部也是绝大多数的编程语言都会提供一些内置数组类型。对许多应用程序而言,数组是最容易实现的数据结构,也是最有效率的数据结构。数组对于需要直接存取访问数据集合“偏远”元素的情况是非常有用的。
.NET框架介绍了一种被称为ArrayList的新的数组类型。ArrayList具有数组的许多特征,但是在某些方面它比数组更强大,这是因为ArrayLsit可以在结构容量已满的情况下我调整自身的大小。ArrayList还有几种对执行插入、删除以及查找操作很有用的方法。既然 C#语言不允许程序员像在 VB.NET 中那样动态地调整数组的大小,所以在无法提前知道要存储的数据项数量的情况下 ArrayList就是一种非常有用的数据结构了。
练习
1.请设计并实现一个类,这个类允许教师跟踪记录单独一门课的成绩。要包括的方法有计算平均分、计算最高分以
及计算最低分。请编写程序来测试一下此类的实现。
2.请修改练习1 的内容使得此类可以记录多门课的成绩。请编写程序来测试实现。
3 请用 ArrayList 重新编写练习 1 的内容。请编写程序来测试实现,还请用 Timing 类把此实现的性能与练习 1 用数组实现的性能进行比较。
4.请设计并实现一个类,这个类要用数组来模拟 ArrayList类的行为。此类还要包含尽可能多的来自ArrayList类的方
法。请编写程序测试实现。