7.5 Array类
用方括号声明数组是C#中使用Array类的表示法。在后台使用C#语法,会创建一个派生自抽象基类Array的新类。这样,就可以使用Array类为每个C#数组定义的方法和属性了。例如,前面就使用了Length属性,或者使用foreach语句迭代数组。其实这是使用了Array类中的GetEnumerator()方法。
Array类实现的其他属性有LongLength和Rank。如果数组包含的元素个数超出了整数的取值范围,就可以使用LongLength属性来获得元素个数。使用Rank属性可以获得数组的维数。
下面通过了解不同的功能来看看Array类的其他成员。
7.5.1 创建数组
Array类是一个抽象类,所以不能使用构造函数来创建数组。但除了可以使用C#语法创建数组实例之外,还可以使用静态方法CreateInstance()创建数组。如果事先不知道元素的类型,该静态方法就非常有用,因为类型可以作为Type对象传递给CreateInstance()方法。
下面的例子说明了如何创建类型为int、大小为5的数组。CreateInstance()方法的第1个参数应是元素的类型,第2个参数定义数组的大小。可以用SetValue()方法设置对应元素的值,用GetValue()方法读取对应元素的值(代码文件SimpleArrays/Program.cs):
Array intArray1 = Array.CreateInstance(typeof(int), 5); for (int i = 0; i < 5; i++) { intArray1.SetValue(33, i); } for (int i = 0; i < 5; i++) { WriteLine(intArray1.GetValue(i)); }
还可以将已创建的数组强制转换成声明为int[]的数组:
int[] intArray2 = (int[])intArray1;
CreateInstance()方法有许多重载版本,可以创建多维数组和不基于0的数组。下面的例子就创建了一个包含2×3个元素的二维数组。第一维基于1,第二维基于10:
int[] lengths = { 2, 3 }; int[] lowerBounds = { 1, 10 }; Array racers = Array.CreateInstance(typeof(Person), lengths, lowerBounds);
SetValue()方法设置数组的元素,其参数是每一维的索引:
racers.SetValue(new Person { FirstName = "Alain", LastName = "Prost" }, 1, 10); racers.SetValue(new Person { FirstName = "Emerson", LastName = "Fittipaldi" }, 1, 11); racers.SetValue(new Person { FirstName = "Ayrton", LastName = "Senna" }, 1, 12); racers.SetValue(new Person { FirstName = "Michael", LastName = "Schumacher" }, 2, 10); racers.SetValue(new Person { FirstName = "Fernando", LastName = "Alonso" }, 2, 11); racers.SetValue(new Person { FirstName = "Jenson", LastName = "Button" }, 2, 12);
尽管数组不是基于0,但可以用一般的C#表示法为它赋予一个变量。只需要注意不要超出边界即可:
Person[, ] racers2 = (Person[, ])racers; Person first = racers2[1, 10]; Person last = racers2[2, 12];
7.5.2 复制数组
因为数组是引用类型,所以将一个数组变量赋予另一个数组变量,就会得到两个引用同一数组的变量。而复制数组,会使数组实现ICloneable接口。这个接口定义的Clone()方法会创建数组的浅表副本。
如果数组的元素是值类型,以下代码段就会复制所有值,如图7-5所示:
图7-5
int[] intArray1 = {1, 2}; int[] intArray2 = (int[])intArray1.Clone();
如果数组包含引用类型,则不复制元素,而只复制引用。图7-6显示了变量beatles和beatlesClone,其中beatlesClone通过从beatles中调用Clone()方法来创建。beatles和beatlesClone引用的Person对象是相同的。如果修改beatlesClone中一个元素的属性,就会改变beatles中的对应对象(代码文件SimpleArray/Program.cs)。
Person[] beatles = { new Person { FirstName="John", LastName="Lennon" }, new Person { FirstName="Paul", LastName="McCartney" } }; Person[] beatlesClone = (Person[])beatles.Clone();
图7-6
除了使用Clone()方法之外,还可以使用Array.Copy()方法创建浅表副本。但Clone()方法和Copy()方法有一个重要区别:Clone()方法会创建一个新数组,而Copy()方法必须传递阶数相同且有足够元素的已有数组。
注意:如果需要包含引用类型的数组的深层副本,就必须迭代数组并创建新对象。
7.5.3 排序
Array类使用Quicksort算法对数组中的元素进行排序。Sort()方法需要数组中的元素实现IComparable接口。因为简单类型(如System.String和System.Int32)实现IComparable接口,所以可以对包含这些类型的元素排序。
在示例程序中,数组名称包含string类型的元素,这个数组可以排序(代码文件SortingSample/Program.cs)。
string[] names = { "Christina Aguilera", "Shakira", "Beyonce", "Lady Gaga" }; Array.Sort(names); foreach (var name in names) { WriteLine(name); }
该应用程序的输出是排好序的数组:
Beyonce Christina Aguilera Lady Gaga Shakira
如果对数组使用自定义类,就必须实现IComparable接口。这个接口只定义了一个方法CompareTo(),如果要比较的对象相等,该方法就返回0。如果该实例应排在参数对象的前面,该方法就返回小于0的值。如果该实例应排在参数对象的后面,该方法就返回大于0的值。
修改Person类,使之实现IComparable<Person>接口。先使用String类中的CompareTo()方法对LastName的值进行比较。如果LastName的值相同,就比较FirstName(代码文件SortingSample/Person.cs):
public class Person: IComparable<Person> { public int CompareTo(Person other) { if (other == null) return 1; int result = string.Compare(this.LastName, other.LastName); if (result == 0) { result = string.Compare(this.FirstName, other.FirstName); } return result; } //...
现在可以按照姓氏对Person对象对应的数组排序(代码文件SortingSample/Program.cs):
Person[] persons = { new Person { FirstName="Damon", LastName="Hill" }, new Person { FirstName="Niki", LastName="Lauda" }, new Person { FirstName="Ayrton", LastName="Senna" }, new Person { FirstName="Graham", LastName="Hill" } }; Array.Sort(persons); foreach (var p in persons) { WriteLine(p); }
使用Person类的排序功能,会得到按姓氏排序的姓名:
Damon Hill Graham Hill Niki Lauda Ayrton Senna
如果Person对象的排序方式与上述不同,或者不能修改在数组中用作元素的类,就可以实现IComparer接口或IComparer<T>接口。这两个接口定义了方法Compare()。要比较的类必须实现这两个接口之一。IComparer接口独立于要比较的类。这就是Compare()方法定义了两个要比较的参数的原因。其返回值与IComparable接口的CompareTo()方法类似。
类PersonComparer实现了IComparer<Person>接口,可以按照firstName或lastName对Person对象排序。枚举PersonCompareType定义了可用于PersonComparer的排序选项:FirstName和LastName。排序方式由PersonComparer类的构造函数定义,在该构造函数中设置了一个PersonCompareType值。实现Compare()方法时用一个switch语句指定是按FirstName还是LastName排序(代码文件SortingSample/PersonComparer.cs)。
public enum PersonCompareType { FirstName, LastName } public class PersonComparer: IComparer<Person> { private PersonCompareType _compareType; public PersonComparer(PersonCompareType compareType) { _compareType = compareType; } public int Compare(Person x, Person y) { if (x == null && y == null) return 0; if (x == null) return 1; if (y == null) return -1; switch (_compareType) { case PersonCompareType.FirstName: return string.Compare(x.FirstName, y.FirstName); case PersonCompareType.LastName: return string.Compare(x.LastName, y.LastName); default: throw new ArgumentException("unexpected compare type"); } } }
现在,可以将一个PersonComparer对象传递给Array.Sort()方法的第2个参数。下面按名字对persons数组排序(代码文件SortingSample/Program.cs):
Array.Sort(persons, new PersonComparer(PersonCompareType.FirstName)); foreach (var p in persons) { WriteLine(p); }
persons数组现在按名字排序:
Ayrton Senna Damon Hill Graham Hill Niki Lauda
注意:Array类还提供了Sort方法,它需要将一个委托作为参数。这个参数可以传递给方法,从而比较两个对象,而不需要依赖IComparable或IComparer接口。第9章将介绍如何使用委托。