C#高级编程笔记-数组

本章讨论如下内容:

●       简单数组

●       多维数组

●       锯齿数组

●       Array类

●       数组和集合接口

●       数组作为参数

●       数组协变

●       枚举

●       结构比较

●       Span

●       数组池

目录

1.1  简单数组

1.1.1  数组的声明

1.1.2  数组的初始化

1.1.3  访问数组元素

1.1.4  使用引用类型

1.2  多维数组

1.3 锯齿数组

1.4  Array类

1.4.1 创建数组

1.4.3  复制数组

1.4.4  排序

1.5  数组和集合接口

1.5.1  IEumerable接口

1.5.2  ICollection接口

1.5.3  IList接口

1.6 数组作为参数

1.7 数组协变

1.8 枚举

1.8.1  IEnumerator接口

1.8.2  foreach语句

1.8.3  yield语句

1.9 结构比较

1.10 Span

1.10.1 创建切片

1.10.2 使用Span改变值

1.10.3 只读的Span

1.11 数组池

1.11.1 创建数组池

1.11.2 从池中租用内存

1.11.3 将内存返回给池


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接口的属性和方法说    明
CountCount属性可确定集合中的元素个数,它返回的值与Length属性相同
IsSynchronized、SyncRootIsSynchronized属性确定集合是否是线程安全的。对于数组,这个属性总是返回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章将介绍如何从数组中创建只读属性
ItemItem属性可以用整型索引访问数组

1.6 数组作为参数

数组可以作为参数传递给方法,也可以从方法返回。要返回一个数组,只需要把数组声明为返回类型,如下面的方法GetPersons0所示:

要把数组传递给方法,应把数组声明为参数,如下面的DisplayPersonsO方法所示:

1.7 数组协变

数组支持协变。这表示数组可以声明为基类,其派生类型的元素可以赋予数组元素。
例如,可以声明一个object[]类型的参数,给它传递一个Person[]:

注意:
数组协变只能用于引用类型,不能用于值类型。另外,数组协变有一个问题,它只能通过运行时异常来解决。如果把Person数组赋予object数组,object数组就可以使用派生自object的任何元素。例如,编译器允许把字符串传递给数组元素。但因为object数组引用Person数组,所以会出现一个运行时异常ArrayTypeMismatchException.

附:

  • 协变:允许将派生类的数组分配给基类数组。这意味着如果DogAnimal的派生类,那么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时间

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值