本章讨论如下内容:
● 简单数组
● 多维数组
● 锯齿数组
● Array类
● 数组和集合接口
● 数组作为参数
● 数组协变
● 枚举
● 结构比较
● Span
● 数组池
目录
1.1 简单数组
如果需要使用同一类型的多个对象,就可以使用数组。数组是一种数据结构,可以包含同一类型的多个元素。
注意:
如果需要使用不同类型的多个对象,可以通过类、结构和元组使用它们。
1.1.1 数组的声明
在声明数组时,应先定义数组中元素的类型,其后是一个空方括号和一个变量名。例如,下面声明了一个包含整型元素的数组:
int[] myArray;
1.1.2 数组的初始化
声明了数组后,就必须为数组分配内存,以保存数组的所有元素。数组是引用类型,所以必须给它分配堆上的内存。为此,应使用new运算符,指定数组中元素的类型和数量来初始化数组的变量。下面指定了数组的大小。
提示:
值类型和引用类型请参见C#高级编程笔记-对象和类型
myArray = new int[4];
在声明和初始化后,变量myArray就引用了4个整型值,它们位于托管堆上。
警告:
在指定了数组的大小后,如果不复制数组中的所有元素,就不能重新设置数组的大小。如果事先不知道数组中应包含多少个元素,就可以使用集合。
除了在两个语句中声明和初始化数组之外,还可以在一个语句中声明和初始化数组:
int[] myArray = new int[4];
还可以使用数组初始化器为数组的每个元素赋值。数组初始化器只能在声明数组变量时使用,不能在声明数组之后使用。
int[] myArray = new int[4] {4, 7, 11, 2};
如果用花括号初始化数组,还可以不指定数组的大小,因为编译器会计算出元素的个数:
int[] myArray = new int[] {4, 7, 11, 2};
使用C#编译器还有一种更简化的形式。使用花括号可以同时声明和初始化数组,编译器生成的代码与前面的例子相同:
int[] myArray = {4, 7, 11, 2};
1.1.3 访问数组元素
数组在声明和初始化后,就可以使用索引器访问其中的元素了。数组只支持有整型参数的索引器。
提示:
在定制的类中,可以创建支持其他类型的索引器。创建定制索引器的内容请参见第6章。
通过索引器传送元素号,就可以访问数组。索引器总是以0开头,表示第一个元素。可以传送给索引器的最大值是元素个数减1,因为索引从0开始。在下面的例子中,数组myArray用4个整型值声明和初始化。用索引器0、1、2、3就可以访问该数组中的元素。
int[] myArray = new int[] {4, 7, 11, 2};
int v1 = myArray[0]; // read first element
int v2 = myArray[1]; // read second element
myArray[3] = 44; // change fourth element
警告:
如果使用错误的索引器值(不存在对应的元素),就会抛出IndexOutOfRangeException类型的异常。
如果不知道数组中的元素个数,则可以在for语句中使用Length属性:
for (int i = 0; i < myArray.Length; i++)
{
Console.WriteLine(myArray[i]);
}
除了使用for语句迭代数组中的所有元素之外,还可以使用foreach语句:
for (int val in myArray)
{
Console.WriteLine(val);
}
提示:
foreach语句利用了本章后面讨论的IEnumerable和IEnumerator接口。
1.1.4 使用引用类型
不但能声明预定义类型的数组,还可以声明定制类型的数组。下面用Person类来说明,这个类有两个构造函数、自动实现的属性Firstname和Lastname、以及ToString()方法的一个重写:
public class Person
{
public Person()
}
{
public Person(string firstName, string lastName)
{
this. firstname = firstName;
this.lastname = lastName;
}
public string Firstname{ get; set; }
public string Lastname{ get; set; }
public override string ToString()
{
return String.Format("{0} {1}", firstName, lastName);
}
}
声明一个包含两个Person元素的数组,与声明一个int数组类似:
Person[] myPersons = new Person[2];
但是必须注意,如果数组中的元素是引用类型,就必须为每个数组元素分配内存。若使用了数组中未分配内存的元素,就会抛出NullReferenceException类型的异常。
使用从0开始的索引器,可以为数组的每个元素分配内存:
myPersons [0] = new Person("Ayrton", "Senna");
myPersons [1] = new Person("Michael", "Schumacher");
图5-2显示了Person数组中的对象在托管堆中的情况。myPersons是一个存储在堆栈上的变量,该变量引用了存储在托管堆上的Person元素数组。这个数组有足够容纳两个引用的空间。数组中的每一项都引用了一个Person对象,而这些Person对象也存储在托管堆上。
与int类型一样,也可以对定制类型使用数组初始化器:
Person[] myPersons = {new Person("Ayrton", "Senna"),
new Person("Michael", "Schumacher") };
1.2 多维数组
一般数组(也称为一维数组)用一个整数来索引。多维数组用两个或多个整数来索引。
图7-3是二维数组的数学表示法,该数组有3行3列。第1行的值是1、2和3,第3行的值是7、8和9。
注意:声明数组后,就不能修改其阶数了。
如果事先知道元素的值,就可以使用数组索引器来初始化二维数组。在初始化数组时,使用一个外层的花括号,每一行用包含在外层花括号中的内层花括号进行初始化。
注意:
使用数组初始化器时,必须初始化数组的每个元素,不能把某些值的初始化放在以后完成。
在花括号中使用两个逗号,就可以声明一个三维数组:
1.3 锯齿数组
二维数组的大小是矩形的,例如3×3个元素。而锯齿数组的大小设置是比较灵活的,在锯齿数组中,每一行都可以有不同的大小。
图7-4比较了有3×3个元素的二维数组和锯齿数组。图中的锯齿数组有3行,第一行有2个元素,第二行有6个元素,第三行有3个元素。
在声明锯齿数组时,要依次放置开闭括号。在初始化锯齿数组时,先设置该数组包含的行数。定义各行中元素个数的第二个括号设置为空,因为这类数组的每一行包含不同的元素数。之后,为每一行指定行中的元素个数:
int[][] jagged = new int[3][];
jagged[0] = new int[2] {1, 2};
jagged[1] = new int[6] {3, 4, 5, 6, 7, 8};
jagged[2] = new int[3] {9, 10, 11};
迭代锯齿数组中所有元素的代码可以放在嵌套的for循环中。在外层的for循环中,迭代每一行,内层的for循环迭代一行中的每个元素:
for ( int row = 0; row < jagged.Length; row++)
{
for ( int element = 0; element <jagged[row].Length; element++)
{
Console.WriteLine("row: {0}, element: [1], value: {2}",
row, element, jagged[row][element]);
}
}
1.4 Array类
用方括号声明数组是C#中使用Array类的记号。在后台使用C#语法,会创建一个派生于抽象基类Array的新类。这样,就可以使用Array类为每个C#数组定义的方法和属性了。例如,前面就使用了Length属性,还使用foreach语句迭代数组。其实这是使用了Array类中的GetEnumerator()方法。
Array类实现的其他属性有LongLength和Rank。如果数组包含的元素个数超出了整数的取值范围,就可以使用LongLength属性来获得元素个数。使用Rank属性可以获得数组的维数。
下面通过了解不同的功能来看看Array类的其他成员。
1.4.1 创建数组
Array类是一个抽象类,所以不能使用构造函数来创建数组。但除了可以使用C#语法创建数组实例之外,还可以使用静态方法CreateInstance()创建数组。如果事先不知道元素的类型,就可以使用该静态方法,因为类型可以作为Type对象传送给CreateInstance()方法。
下面的例子说明了如何创建类型为int、大小为5的数组。CreateInstance()方法的第一个参数应是元素的类型,第二个参数定义数组的大小。可以用SetValue()方法设置值,用GetValue()方法读取值:
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++)
{
Console.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("Alain", "Prost"), 1, 10);
racers.SetValue(new Person("Emerson", "Fittipaldi"), 1, 11);
racers.SetValue(new Person("Ayrton", "Senna"), 1, 12);
racers.SetValue(new Person("Ralf", "Schumacher"), 2, 10);
racers.SetValue(new Person("Fernando", "Alonso"), 2, 11);
racers.SetValue(new Person("Jenson", "Button"), 2, 12);
尽管数组不是基于0的,但可以用一般的C#记号将它赋予一个变量。只需注意不要超出边界即可:
Person[,] racers2 = (Person[,]) racers;
Person first = racers2[1, 10];
Person last = racers2[2, 12];
1.4.3 复制数组
因为数组是引用类型,所以将一个数组变量赋予另一个数组变量,就会得到两个指向同一数组的变量。而复制数组,会使数组实现ICloneable接口。这个接口定义的Clone()方法会创建数组的浅副本。
如果数组的元素是值类型,就会复制所有的值,如图7-5所示:
int intArray1 = {1, 2};
int intArray2 = (int[])intArray1.Clone();
如果数组包含引用类型,则不复制元素,而只复制引用。图7-6 显示了变量beatles和beatlesClone,其中beatlesClone是通过在beatles上调用Clone()方法来创建的。beatles和beatlesClone引用的Person对象是相同的。如果修改beatlesClone中一个元素的属性,就会改变beatles中的对应对象。
Person[] beatles = {
new Person("John", "Lennon"),
new Person("Paul", "McCartney"),
};
Person[] beatlesClone = (Person[])beatles.Clone();
除了使用Clone()方法之外,还可以使用Array.Copy()方法创建浅副本。但Clone()方法和Copy()方法有一个重要区别:Clone()方法会创建一个新数组,而Copy()方法只是传送了阶数相同、有足够元素空间的已有数组。
提示:
如果需要包含引用类型的数组的深副本,就必须迭代数组,创建新对象。
1.4.4 排序
Array类实现了对数组中元素的冒泡排序。Sort()方法需要数组中的元素实现IComparable接口。简单类型,如System.String和System.Int32实现了IComparable接口,所以可以对包含这些类型的元素排序。
在示例程序中,数组name包含string类型的元素,这个数组是可以排序的。
String[] names = {
"Christina Aguillera",
"Shakira",
"Beyonce",
"Gwen Stefani"
};
Array.Sort(names);
foreach (string name in names)
{
Console.WriteLine(name);
}
该应用程序的输出是排好序的数组:
Beyonce
Christina Aguillera
Gwen Stefani
Shakira
如果对数组使用定制的类,就必须实现IComparable接口。这个接口只定义了一个方法CompareTo(),如果要比较的对象相等,该方法就返回0。如果实例应排在参数对象的前面,该方法就返回小于0的值。如果实例应排在参数对象的后面,该方法就返回大于0的值。
修改Person类,使之执行IComparable接口。对LastName的值进行比较。LastName是string类型,而String类已经实现了IComparable接口,所以可以使用String类中CompareTo()方法的实现代码。如果LastName的值相同,就比较FirstName:
public class Person : IComparable
{
public int CompareTo(object obj)
{
Person other = obj as Person;
int result = this.LastName.CompareTo(
other.LastName);
if (result == 0)
{
result = this.FirstName.CompareTo(
other.FirstName);
}
return result;
}
现在可以按照姓氏对Person对象数组排序了:
Person[] persons = {
new Person("Emerson", "Fittipaldi"),
new Person("Niki", "Lauda"),
new Person("Ayrton", "Senna"),
new Person("Michael", "Schumacher"),
};
Array.Sort (persons);
foreach (Person p in persons)
{
Console.WriteLine(p);
}
使用Person类的排序功能,会得到按姓氏排序的姓名:
Emerson Fittipaldi
Niki Lauda
Michael Schumacher
Ayrton Senna
如果Person对象的排序方式与上述不同,或者不能修改在数组中用作元素的类,就可以执行IComparer接口。这个接口定义了方法Compare()。IComparable接口必须由要比较的类来执行,而IComparer接口独立于要比较的类。这就是Compare()方法定义了两个要比较的变元的原因。其返回值与IComparable接口的CompareTo()方法类似。
类PersonComparer实现了IComparer接口,可以按照firstName或lastName对Person对象排序。枚举PersonCompareType定义了与PersonComparer相当的排序选项:FirstName和LastName。排序的方式由类PersonComparer的构造函数定义,在该构造函数中设置了一个PersonCompareType值。Compare()方法用一个switch语句指定是按firstName还是lastName排序。
public class PersonComparer : IComparer
{
public enum PersonCompareType
{
FirstName,
LastName
}
private PersonCompareType compareType;
public PersonComparer(PersonCompareType compareType)
{
this. compareType = compareType;
}
public int Compare(object x, object y)
{
Person p1 = x as Person;
Person p1 = y as Person;
Switch (compareType)
{
case PersonCompareType.FirstName:
return p1. FirstName. CompareTo(p2. FirstName);
case PersonCompareType.LastName:
return p1. LastName. CompareTo(p2. LastName);
default:
throw new ArgumentException("unexpexted compare type")
}
}
}
现在,可以将一个PersonComparer对象传送给Array.Sort()方法的第二个变元。下面是按名字对persons数组排序:
Array.Sort(persons,
new PersonComparer(PersonComparer. PersonCompareType.FirstName));
foreach (Person p in persons)
{
Console.WriteLine(p);
}
persons数组现在按名字排序:
Ayrton Senna
Emerson Fittipaldi
Michael Schumacher
Niki Lauda
提示:
Array类还提供了Sort方法,它需要将一个委托作为变元。第7章将介绍如何使用委托。
1.5 数组和集合接口
Array类实现了IEumerable、ICollection和IList接口,以访问和枚举数组中的元素。由于用定制数组创建的类派生于Array抽象类,所以能使用通过数组变量执行的接口中的方法和属性。
1.5.1 IEumerable接口
IEumerable是由foreach语句用于迭代数组的接口。这是一个非常特殊的特性,在下一节中讨论。
1.5.2 ICollection接口
ICollection接口派生于IEumerable接口,并添加了如表5-2所示的属性和方法。这个接口主要用于确定集合中的元素个数,或用于同步。
ICollection接口的属性和方法 | 说 明 |
Count | Count属性可确定集合中的元素个数,它返回的值与Length属性相同 |
IsSynchronized、SyncRoot | IsSynchronized属性确定集合是否是线程安全的。对于数组,这个属性总是返回false。对于同步访问,SyncRoot属性可以用于线程安全的访问。 |
CopyTo() | 利用CopyTo()方法可以将数组的元素复制到现有的数组中。它类似于静态方法Array.Copy() |
1.5.3 IList接口
IList接口派生于ICollection接口,并添加了下面的属性和方法。Array类实现IList接口的主要原因是,IList接口定义了Item属性,以使用索引器访问元素。IList接口的许多其他成员是通过Array类抛出NotSupportedException异常实现的,因为这些不应用于数组。IList接口的所有属性和方法如表5-3所示。
IList接口的所有属性和方法 | 说 明 |
Add() | Add()方法用于在集合中添加元素。对于数组,该方法会抛出NotSupportedException异常 |
Clear() | Clear()方法可清除数组中的所有元素。值类型设置为0,引用类型设置为null |
Contains() | Contains()方法可以确定某个元素是否在数组中。其返回值是true或false。这个方法会对数组中的所有元素进行线性搜索,直到找到所需元素为止 |
IndexOf() | IndexOf()方法与Contains()方法类似,也是对数组中的所有元素进行线性搜索。不同的是,IndexOf()方法会返回所找到的第一个元素的索引 |
Insert() Remove() RemoveAt() | 对于集合,Insert()方法用于插入元素,Remove()和RemoveAt()可删除元素。对于数组,这些方法都抛出NotSupportedException异常 |
IsFixedSize | 数组的大小总是固定的,所以这个属性总是返回true |
IsReadOnly | 数组总是可以读/写的,所以这个属性返回false。第10章将介绍如何从数组中创建只读属性 |
Item | Item属性可以用整型索引访问数组 |
1.6 数组作为参数
数组可以作为参数传递给方法,也可以从方法返回。要返回一个数组,只需要把数组声明为返回类型,如下面的方法GetPersons0所示:
要把数组传递给方法,应把数组声明为参数,如下面的DisplayPersonsO方法所示:
1.7 数组协变
数组支持协变。这表示数组可以声明为基类,其派生类型的元素可以赋予数组元素。
例如,可以声明一个object[]类型的参数,给它传递一个Person[]:
注意:
数组协变只能用于引用类型,不能用于值类型。另外,数组协变有一个问题,它只能通过运行时异常来解决。如果把Person数组赋予object数组,object数组就可以使用派生自object的任何元素。例如,编译器允许把字符串传递给数组元素。但因为object数组引用Person数组,所以会出现一个运行时异常ArrayTypeMismatchException.
附:
-
协变:允许将派生类的数组分配给基类数组。这意味着如果
Dog
是Animal的派生类,那么Dog[]
(派生类数组)可以分配给Animal[]
(基类数组)。 -
逆变:允许将基类数组分配给派生类数组。这意味着Animal
[]
可以分配给Dog[]
,但是在运行时你不能将基类的实例赋值给派生类数组的元素
例如:在多态性中,你可以定义一个基类引用变量,它引用派生类对象。这是协变的典型应用,例如,有一个基类引用 Animal animal = new Dog();
。
1.8 枚举
在foreach语句中使用枚举,可以迭代集合中的元素,且无需知道集合中的元素个数。图5-7显示了调用foreach方法的客户机和集合之间的关系。数组或集合执行带GetEumerator()方法的IEumerable接口。GetEumerator()方法返回一个执行IEumerable接口的枚举。接着,foreach语句就可以使用IEumerable接口迭代集合了。
提示:
GetEnumerator()方法用IEnumerable接口定义。foreach语句并不真的需要在集合类中执行这个接口。有一个名为GetEnumerator()的方法,返回实现了IEnumerator接口的对象就足够了。
1.8.1 IEnumerator接口
foreach语句使用IEnumerator接口的方法和属性,迭代集合中的所有元素。为此,foreach语句使用IEnumerator接口的方法和属性,迭代集合中的所有元素。为此,IEnumerator定义了Current
属性,来返回光标所在的元素,该接口的MoveNextO)方法移动到集合的下一个元素上。如果有这个元素,该方法就返回true。如果集合不再有更多的元素,该方法就返回false。
注意:
Enumerator接口还定义了ResetO方法,以与COM交互操作。许多NET枚举器通过地出
NotSupportedException类型的异常,来实现这个方法。
1.8.2 foreach语句
C#的foreach语句不会解析为IL代码中的foreach语句。C#编译器会把foreach语句转换为IEnumerable接口的方法和属性。下面是一个简单的foreach语句,它迭代persons数组中的所有元素,并逐个显示它们:
foreach (Person p in persons)
{
Console.WriteLine(p);
}
foreach语句会解析为下面的代码段。首先,调用GetEnumerator()方法,获得数组的一个枚举。在while循环中—— 只要MoveNext()返回true—— 用Current属性访问数组中的元素:
IEnumerator enumerator = persons. GetEnumerator();
while (enumerator.MoveNext())
{
Person p = (Person) enumerator.Current;
Console.WriteLine(p);
}
1.8.3 yield语句
C# 1.0使用foreach语句可以轻松地迭代集合。在C# 1.0中,创建枚举器仍需要做大量的工作。C# 2.0添加了yield语句,以便于创建枚举器。
yield return语句返回集合的一个元素,并移动到下一个元素上。yield break可停止迭代。
下面的例子是用yield return语句实现一个简单集合的代码。类HelloCollection包含GetEnumerator()方法。该方法的实现代码包含两个yield return语句,它们分别返回字符串Hello和World。
using System;
using System.Collection;
namespace Wrox.ProCSharp.Arrays
{
public class HelloCollection
{
public IEumerator GetEumerator()
{
yield return "Hello";
yield return "World";
}
}
}
警告:
包含yield语句的方法或属性也称为迭代块。迭代块必须声明为返回IEnumerator或IEnumerable接口。这个块可以包含多个yield return语句或yield break语句,但不能包含return语句。
现在可以用foreach语句迭代集合了:
public class Program
{
HelloCollection helloCollection = new HelloCollection();
foreach (string s in helloCollection)
{
Console.WriteLine(s);
}
}
使用迭代块,编译器会生成一个yield 类型,其中包含一个状态机,如下面的代码所示。yield 类型执行IEnumerator和IDisposable接口的属性和方法。在下面的例子中,可以把yield 类型看作内部类Enumerator。外部类的GetEnumerator()方法实例化并返回一个新的yield 类型。在yield 类型中,变量state定义了迭代的当前位置,每次调用MoveNext()时,当前位置都会改变。MoveNext()封装了迭代块的代码,设置了current变量的值,使Current属性根据位置返回一个对象。
public class HelloCollection
{
public IEnumerator GetEnumerator() => new Enumerator(0);
public class Enumerator : IEnumerator<string>, IEnumerator,IDisposable
{
private int state;
private string current;
public Enumerator(int state) => this.state = state;
bool System.Collections.IEnumerator.MoveNext()
{
switch (state)
{
case 0:
current = "Hello";
state = 1;
return true;
case 1:
current = "World";
state = 2;
return true;
case 2:
break;
}
return false;
}
void System.Collections.IEnumerator.Reset() =>throw new NotSupportedException();
string System.Collections.Generic.IEnumerator<string>.Current => current;
object System.Collections.IEnumerator.Current => current;
void IDisposable.Dispose() { }
}
}
注意:
yield语句会生成一个枚举器,而不仅仅生成一个包含的项的列表。这个枚举器通过foreach语句调用。从foreach中依次访问每一项时,就会访问枚举器。这样就可以迭代大量的数据,而不需要一次把所有的数据都读入内存。
现在使用yield return语句,很容易实现允许以不同方式迭代集合的类。类MusicTitles可以用默认方式通过GetEnumerator()方法迭代标题,用Reverse()方法逆序迭代标题,用Subset()方法搜索子集:
public class MusicTitles
{
string[] names = {
"Tubular Bells", "Hergest Ridge",
"Ommadawn", "Platinum"};
public IEnumerator GetEnumerator()
{
for (int i = 0; i < 4; i++)
{
yield return names[i];
}
}
public IEnumerable Reverse()
{
for (int i = 3; i >= 0; i–)
{
yield return names[i];
}
}
}
public IEnumerable<string> Subset( int index, int length)
{
for (int i = index; i < index + length; i++)
{
yield return names[i];
}
}
}
注意:
类支持的默认迭代是定义为返回IEnumerator的GetEnumeratorO方法。命名的迭代返回IEnumerable。
迭代字符串数组的客户代码先使用GetEnumerator()方法,该方法不必在代码中编写,因为这是默认使用的方法。然后逆序迭代标题,最后将索引和要迭代的元素个数传送给Subset()方法,来迭代子集:
MusicTitles titles = new MusicTitles();
foreach(string title in titles)
{
ConsoleWriteLine(title);
}
ConsoleWriteLine();
ConsoleWriteLine("reverse");
foreach(string title in titles.Reverse())
{
ConsoleWriteLine(title);
}
ConsoleWriteLine();
ConsoleWriteLine("subset");
foreach(string title in titles.Subset(2, 2))
{
ConsoleWriteLine(title);
}
使用yield语句还可以完成更复杂的任务,例如从yield return中返回枚举器。
在TicTacToe游戏中有9个域,玩家轮流在这些域中放置“十”字或一个圆。这些移动操作由GameMoves类模拟。方法Cross()和Circle()是创建迭代类型的迭代块。变量cross和circle在GameMoves类的构造函数中设置为Cross()和Circle()方法。这些域不设置为调用的方法,而是设置为用迭代块定义的迭代类型。在Cross()迭代块中,将移动操作的信息写到控制台上,并递增移动次数。如果移动次数大于9,就用yield break停止迭代;否则,就在每次迭代中返回yield类型cross的枚举对象。Circle()迭代块非常类似于Cross()迭代块,只是它在每次迭代中返回circle迭代类型。
public calss GameMoves
{
private IEnumerator cross;
private IEnumerator circle;
public GameMoves()
{
cross = Cross();
circle = Circle();
}
private int move = 0;
public IEnumerator Cross()
{
while (true)
{
Console.WriteLine("Cross, move {0}", move);
move++;
if (move > 9)
yield break;
yield return circle;
}
}
public IEnumerator Circle()
{
while (true)
{
Console.WriteLine("Circle, move {0}", move);
move++;
if (move > 9)
yield break;
yield return cross;
}
}
}
在客户程序中,可以以如下方式使用GameMoves类。将枚举器设置为由game.Cross()返回的枚举器类型,以设置第一次移动。enumerator.MoveNext调用以迭代块定义的一次迭代,返回另一个枚举器。返回的值可以用Current属性访问,并设置为enumerator变量,用于下一次循环:
GameMoves game = new GameMoves();
IEnumerator enumerator = game.Cross();
while (enumerator.MoveNext())
{
enumerator = (IEnumerator) enumerator.Current;
}
这个程序的输出会显示交替移动的情况,直到最后一次移动:
Cross, move 0
Circle, move 1
Cross, move 2
Circle, move 3
Cross, move 4
Circle, move 5
Cross, move 6
Circle, move 7
Cross, move 8
1.9 结构比较
数组和元组都实现接口IStructuralEquatable和IStructuralComparable。这两个接口不仅可以比较引用,还可以比较内容。这些接口都是显式实现的,所以在使用时需要把数组和元组强制转换为这个接口。IStructuralEquatable接口用于比较两个元组或数组是否有相同的内容,IStructuralComparable接口用于给元组或数组排序。
对于说明IStructuralEquatable接口的示例,使用实现IEquatable接口的Person类。IEquatable接口定义了一个强类型化的Equals()方法,以比较FirstName和LastName属性的值
现在创建了两个包含Person项的数组。这两个数组通过变量名janet包含相同的Person对象,和两个内容相同的不同Person对象。比较运算符“!=”返回true,因为这其实是两个变量persons1和persons.2引用的两个不同数组。因为Array类没有重写带一个参数的Equals()方法,所以用“=”运算符比较引用也会得到相同的结果,即这两个变量不相同.
对于IStructuralEquatable接口定义的Equals()方法,它的第一个参数是object类型,第二个参数是
IEqualityComparer类型。调用这个方法时,通过传递一个实现了IEqualityComparer<T>的对象,就可以定义如何进行比较。通过EqualityComparer<T>类完成IEqualityComparer的一个默认实现。这个实现检查该类型是否实现了Equatable接口,并调用Equatable.EqualsO方法。如果该类型没有实现Equatable,就调用Object基类中的Equals()方法进行比较。
Person实现正IEquatable<Person>,在此过程中比较对象的内容,而数组的确包含相同的内容:
1.10 Span
为了快速访问托管或非托管的连续内存,可以使用Span<T>结构。一个可以使用Span<T>的例子是数组;Span<T>结构在后台保存在连续的内存中。另一个例子是长字符串。在第9章中使用了Spn<T>和字符串。
使用Span<T>,可以直接访问数组元素。数组的元素没有复制,但是它们可以直接使用,这比复制要快。
在下面的代码片段中,首先创建并初始化一个简单的int数组。调用构造函数,并将数组传递给Span<int>,以创建一个Span<int>对象。Span<T>类型提供了一个索引器,因此可以使用这个索引器访问Span<T>的元素。这里,第二个元素的值改为11。由于数组arr1是在span中引用的,因此通过改变span<T>元素来改变数组的第二个元素
1.10.1 创建切片
Spn<T>的一个强大特性是,可以使用它访问数组的部分或切片。使用切片时,不会复制数组元素,它们是从Span中直接访问的。
下面的代码片段展示了创建切片的两种方法。第一种方法是,使用一个构造函数重载版本传递应使用的数组的开头和长度。使用变量span3引用这个新创建的Span<int>,它只能访问数组arr2的3个元素,从第四个元素开始。构造函数还有另一个重载版本,它可以仅传递切片的开头。在这个重载版本中,会提取数组的剩余部分,一直到数组的末尾。调用Slice方法,也可以从Span<T>对象中创建一个切片。它有类似的重载版本。通过变量span4,使用之前创建的span1创建一个包含4个元素的切片,且从span1的第3个元素开始。
DisplaySpan方法用于显示span的内容。下面代码段中的方法利用了ReadOnlySpan。如果不需要更改span引用的内容,就可以使用这个span类型,DisplaySpan方法就是这样。ReadOnlySpan-<T>在本章后面详细讨论:
运行应用程序时,显示span3和span4的内容,它们是an2和ar1的子集。
注意:
Span<T>是安全的,不会出界。如果在创建的spam超出包含的数组长度,就会地出
ArgumentOutOfRangeException类型的异常。
1.10.2 使用Span改变值
前面介绍了如何使用Span<T>类型的索引器,直接更改由span引用的数组元素。下面的代码片段显示了更多的选项。
可以调用Clear方法,该方法用0填充包含nt类型的span;可以调用Fill方法,用传递给Fill方法的值来
填充span;可以将一个Span<T>复制到另一个Span<T>。在CopyTo方法中,如果目标span不够大,就会抛出ArgumentException类型的异常。可以使用TryCopyTo方法来避免这个结果。如果目标span不够大,此方法不会抛出异常;而是返回alse,因为复制没有成功。
运行应用程序时,可以看到spanl的内容,其中的最后两个数使用span4清除,还可以看到span2的内容,其中有三个元素用span5来填充值42,也可以看到span1的内容,其中前三个数字从span5中复制。从span1复制到span4是不成功的,因为span4的长度只有4,而spanl的长度是6:
1.10.3 只读的Span
如果只需要对数组段进行读访问,就可以使用ReadOnlySpan<T>,如前面的DisplaySpan方法所示。对于ReadOnlySpan<T>,索引器是只读的,这种类型没有提供Clear和Fill方法。但是,可以调用CopyTo方法,将ReadOnlySpan<T>的内容复制到Span<T>。
下面的代码片段使用ReadOnlySpan<T>的构造函数从一个数组中创建了readOnlySpan1,readOnlySpan2和readOnlySpan3是由Span<int>和int[]的直接赋予创建的。隐式转换操作符可用于ReadOnlySpan<T>。
1.11 数组池
如果一个应用程序创建和销毁了许多数组,垃圾收集器就有一些工作要做。为了减少垃圾收集器的工作,可以通过ArrayPool类使用数组池。ArrayPool管理一个数组池。数组可以从这里租借,并返回到池中。内存在ArrayPool中管理。
1.11.1 创建数组池
通过调用静态Create方法,可以创建ArrayPool<T>。为了提高效率,数组池在多个桶中为大小类似的数组管理内存。使用Create方法,可以在需要一个桶之前,在另一个桶中定义最大的数组长度和数组的数量:
maxArrayLength的默认值是1024*1024字节,maxArraysPerBucket的默认值是50。数组池使用多个桶,以便在使用多个数组时更快地访问数组。只要还没有到达数组的最大数量,大小类似的数组就尽可能保存在同一个桶中。
还可以通过访问ArrayPool<T>类的共享属性,来使用预定义的共享池:
1.11.2 从池中租用内存
调用Rent方法可以请求池中的内存。Rent方法接受应请求的最小数组长度。如果池中已经有内存,则返回该内存。如果它不可用,就给池分配内存,然后返回。在下面的代码片段中,在or循环中请求一个包含1024、2048、3096等元素的数组
Rent方法返回一个数组,其中至少包含所请求的元素个数。返回的数组可能有更多的可用内存。共享池中至少有16个元素。托管数组的元素计数总是重复的一例如,16、32、64、128、256、512、1024、2048、4096、8192个元素等。
运行应用程序时,如果请求的数组大小不符合池管理的数组,就返回较大的数组:
1.11.3 将内存返回给池
不再需要数组时,可以将其返回到池中。数组返回后,可以稍后再用另一个Rent来重用它。
调用数组池的Return方法并将数组传递给Return方法,将数组返回到池中。使用一个可选参数,可以指定在返回池之前是否清除该数组。如果不清除它,下一个从池中租用数组的人可以读取数据。清除数据,可以避免这一点,但是需要更多的CPU时间