C#高级编程第11版

【1】7.1 相同类型的多个对象

1.假如你需要处理同一类型的多个对象,你可以使用集合或者数组。

2.如果你想使用不同类型的不同对象,你最好将它们组合成class、struct或者元组。

【2】7.2.1 数组的声明

数组是一种数据结构,它包含一系列同类型的元素。数组通过声明内部元素类型,并随后紧跟着一对中括号来定义一个数组变量。举个例子,假如你要定义整型数组,你可能会像这么写:

int[] myArray;

【3】7.2.2 数组的初始化

myArray = new int[4];

1.变量myArray将是4个整型值的引用,并且这4个整型值的内存由托管堆进行分配。

2.一旦一个元素完成初始化,它无法简单地调整自己的大小。假如你事先并不知道数组的元素数量,你可以使用集合。

3.简写:

int[] myArray = new int[4];
int[] myArray = new int[4] {4, 7, 11, 2};
int[] myArray = new int[] {4, 7, 11, 2};
int[] myArray = {4, 7, 11, 2};

【4】7.2.3 访问数组元素

1.当数组声明与初始化之后,你可以通过索引访问数组元素。数组只支持的索引参数为整型的索引。

2.假如你使用了一个越界的序号,你将会得到一个IndexOutOfRangeException。

3.可以通过数组的Length属性来访问数组的长度。

for (int i = 0; i < myArray.Length; i++)
{
    Console.WriteLine(myArray[i]);
}

4.可以用foreach来遍历数组:

foreach (var val in myArray)
{
    Console.WriteLine(val);
}

foreach语句使用了IEnumerable和IEnumerator接口,并且遍历数组中的每个元素。

【5】7.2.4 使用引用类型

假如你使用了某个数组元素,它尚未分配内存,此时你想使用它的成员的话,就会抛出一个NullReferenceException。因此得先进性内存分配:

myPersons[0] = new Person("Ayrton", "Senna");
myPersons[1] = new Person("Michael", "Schumacher");

或者:

Person[] myPersons2 =
{
    new Person("Ayrton", "Senna"),
    new Person("Michael", "Schumacher")
};

【6】7.3 多维数组

1.二维数组:

int[,] twodim = new int[3, 3];

声明完数组之后,你无法修改其大小。

2.你也可以通过使用{}包裹起来的每一行数据来声明数组,并且所有行的数据也包含在一对{}中:

int[,] twodim = {
    {1, 2, 3},
    {4, 5, 6},
    {7, 8, 9}
};

假如你试图使用这种方式声明数组,你必须指定每一个元素的值。你无法事先留空某个位置不做任何声明,而试图在之后再指定该位置的值。

3.三维数组:

int[,,] threedim = {
    { { 1, 2 }, { 3, 4 } },
    { { 5, 6 }, { 7, 8 } },
    { { 9, 10 }, { 11, 12 } }
};
Console.WriteLine(threedim[0, 1, 1]);

【7】7.4 锯齿数组

 1.锯齿数组是通过两对[]进行声明的,初始化锯齿数组时,仅仅只需要指定第一个[]的值,因为它代表具体有多少行。而第二个[]用来定义每一行的元素数量,这里需要留空,因为每一行拥有不同数量的元素。接下来我们初始化每一行的元素:

int[][] jagged = new int[3][]; // 假如你指定了第二个中括号的大小,则会得到一个编译错误CS0178
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 };

2.你可以通过嵌套for循环来遍历锯齿数组的所有元素,外层的for循环遍历了每一行,而内部的for循环则遍历行内的每个元素:

for (int row = 0; row < jagged.Length; row++)
{
    for (int element = 0; element < jagged[row].Length; element++)
    {
        Console.WriteLine($"row: {row}, element: {element}, " + $"value: {jagged[row][element]}");
    }
}

【8】7.5 Array 类

数组的Length属性又或者使用foreach语句来遍历数组中的所有元素。而想做到这一点,你实际上使用的是Array类中定义的GetEnumerator方法。

【9】7.5.1 创建数组

1.因为Array类是抽象类,因此你无法通过构造函数创建一个数组的实例对象。

2.可以通过使用Array类中的CreateInstance静态方法来创建数组实例。当你无法事先知道数组中的元素类型的时候,这个方法会特别管用,因为你可以将实际类型作为一个Type实例传递给CreateInstance方法。

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));
}

使用CreateInstance方法创建一个长度为5的int类型数组的例子。方法的第一个参数传递的是元素的类型,而第二个参数则定义了数组的大小。你可以通过SetValue方法来设置数组的值,并通过GetValue方法来获取它。

3.你也可以通过强制转换将Array实例转换成int[]类型:

int[] intArray2 = (int[])intArray1;

4.二维数组,下面的例子创建了一个二维数组,包含2×3个元素。其中一维是从1开始的(1 based),而二维则从10开始(10 based):

int[] lengths = { 2, 3 };
int[] lowerBounds = { 1, 10 };
Array racers = Array.CreateInstance(typeof(Person), lengths, lowerBounds);

5.SetValue方法可以设置每个维度的索引:

racers.SetValue(new Person("Alain", "Prost"), 1, 10);
racers.SetValue(new Person("Michael", "Schumacher"), 2, 10);

6.小心不要越过数组的边界,即不要超过索引的范围。

 【10】7.5.2 复制数组

1.因为数组是引用类型,将一个数组变量赋值给另外一个变量,仅仅只是为你创建了一个新的引用,指向同一个数组。

2.数组的浅拷贝:Clone方法,Array.Copy方法。

3.假如数组包含引用类型,Clone方法仅仅拷贝引用,而非所有元素。比如数组b2拷贝b1的引用,修改b2中某个元素的属性,那么b1中该元素属性也会被修改。

4.Clone方法返回的是一个新的数组;而Copy方法你需要先创建一个同样大小的数组,然后将它作为参数传递给Copy方法。

5.Array的浅表副本仅复制Array的元素,仅复制Array的元素,不管他们是引用类型还是值类型。可是不负责这些引用所引用的对象。新Array中的引用与原始Array的引用指向同样的对象。

6.差别:

Clone()返回值是Object,Copy返回值为void 

Clone()是非静态方法。Copy为静态方法。

Clone()会创建一个新数组。Copy方法必须传递阶数同样且有足够元素的已有数组。

【11】7.5.3 排序

1.Array类使用的是快速排序算法(QuickSort)来对数组中的元素进行排序。Sort方法需要数组内的元素实现IComparable接口。普通类型例如System.String和System.Int32也实现了IComparable,所以你也可以对这些类型进行排序。

2.使用自定义的类作为数组的元素,想要通过Sort方法进行排序,你需要为你的类实现IComparable接口。

3.Array类同样提供了接收委托作为参数的Sort方法。通过这个参数,你可以直接传递一个方法来对两个对象进行比较而非依赖于IComparable或者IComparer接口。

4.通过IComparable接口或重写排序方法来进行排序。

【12】7.6 数组作为参数

数组可以作为参数传递给方法,也可以作为方法的返回值。

为了返回一个数组,你只需要声明相应的数组类型作为返回类型。

同样地你将参数声明成数组类型,你就可以为方法传递相应的数组作为参数。

【13】7.7 数组协变

1.这意味着数组可以被定义成基础类型,但是数组元素可以赋值成派生类型。

2.例如,你声明了类型为object[]的方法参数,你也可以直接为方法传递Person[]类型的数组作为参数,如下所示:

static void DisplayArray(object[] data)
{
    //
}

object

3.数组的协变仅仅对引用类型有效,对值类型是不起作用的。另外,数组协变有个小问题,只能在运行时通过异常进行处理(can only be resolved with runtime exceptions)。假如你将一个Person数组赋值给一个Object数组,这个Object数组可以被用在Object的派生类上。例如,编译器会允许你为这个数组赋值一个string类型的元素,编译器并不会提示错误。而当你实际运行的时候,因为这个Object数组实际上指向的是Person类型的数组,试图给Person对象相应的内存赋值一个string类型会引发一个运行时错误:ArrayTypeMismatchException。

【14】7.8 枚举

1.foreach语句使用了一个枚举器(enumerator)。数组或者集合需要实现IEnumerable接口里的GetEnumerator方法,这个方法返回了一个实现了IEnumerator接口的枚举器对象。然后foreach语句遍历的是IEnumerator对象来遍历集合里的所有元素。

2.GetEnumerator方法定义在IEnumerable接口中。foreach语句并非一定需要集合类实现这个接口。只需要集合类中有个叫GetEnumerator的方法并且它返回的对象实现了IEnumerator接口即可。

【15】7.8.1 IEnumerator 接口

1.Current属性,用来返回当前游标(Cursor)指向集合中的哪个元素,然后定义了方法MoveNext,来访问集合中的下一个元素。假如仍然存在下一个元素,MoveNext方法则返回true,假如当前已经是集合的最后一个元素了,则返回false。

2.Dispose方法来清除枚举器申请的内存资源。

3.IEnumerator接口同样为COM互操作(interoperability)定义了Reset方法。许多.NET枚举器仅仅在这个方法中抛出了一个NotSupportedException异常。

4.Component Object Model(COM)。Component不是其他的什么,而是可以嵌入到其他程序构成的并可重用的二进制软件。它是通过提供的通用接口而和其他程序进行交互操作的,这样就允许任何语言写的两个不同的应用程序进行通信。

【16】7.8.2 foreach 语句

1.C#的foreach语句在IL代码中并不是以foreach语句的形式生成的,而是将其转换成了IEnumerator接口相应的方法和属性。

2.foreach语句遍历数组实际上是调用数组对象的GetEnumerator方法返回一个数组的枚举器。然后在while循环中,只要MoveNext方法返回true,你就可以通过Current属性来访问数组当前的元素。

IEnumerator<Person> enumerator = persons.GetEnumerator();
while (enumerator.MoveNext())
{
    Person p = enumerator.Current;
    Console.WriteLine(p);
}

【17】7.8.3 yield 语句

1.yield return语句返回集合中的一个元素,并且移动指针到下一个元素,而yield break则停止这次遍历。

2.包含有yield语句的方法或者属性也被称为迭代器(iterator block)。一个迭代器必须返回的是IEnumerator接口或者IEnumerable接口,或者这些接口的泛型版本。迭代器中可能包含有多个yield return或者yield break语句,单纯的return语句不允许在这里使用。

3.yield语句生成了一个枚举器,而非是一个item列表。这个枚举器是被foreach语句调用的。因为foreach调用过程中的每一项都会调用到这个枚举器。这使得它可以逐步遍历大量的数据而非需要一次性地将所有数据加载到内存中。

4.MoveNext方法。

【18】7.8.4 遍历集合的其他方式

1.MusicTitles类中允许在GetEnumerator方法中通过默认的方式遍历标题,然后Reverse方法提供反序遍历,而通过Subset方法来访问子集:

public class MusicTitles
{
    string[] names = {"Tubular Bells", "Hergest Ridge", "Ommadawn", "Platinum"};
    public IEnumerator<string> GetEnumerator()
    {
        for (int i = 0; i < 4; i++)
        {
            yield return names[i];
        }
    }
    public IEnumerable<string> 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];
        }
    }
}

2.类默认支持的迭代方法(iteration)是GetEnumerator方法,返回IEnumerator对象。命名迭代(Named iterations)则返回IEnumerable对象。

3.foreach遍历时,GetEnumerator方法不用你手动写代码进行调用,因为foreach语句会默认帮你调用这个实现。

【19】7.8.5 通过yield return返回枚举器

井字棋游戏作为例子,通过调用了enumerator的MoveNext方法,并通过yield return语句返回另外一个Circle类型的枚举器。先是×,然后返回到另一个枚举器○。

【20】7.9 结构的比较

数组和元组(Tuples,第13章介绍)一样也实现了接口IStructuralEquatable和IStructuralComparable。这些接口不单只比较引用还包括内容比较。这个接口是显式实现的,所以有必要在使用的时候将数组和元组强制转换成接口类型。IStructuralEquatable用来比较两个元组或者数组是否具有相同的内容,而IStructuralComparable则用来对元组或者数组进行排序。

【21】7.10 Span

1.使用Span<T>结构体来快速地访问托管或者非托管的连续内存。

2.通过使用Span<T>,你可以直接访问数组元素。数组元素并没有重新复制一份,但它们仍然可以直接使用,而且比拷贝的速度更快。

3.Span<T>类型提供了一个索引器,因此Span<T>的元素可以通过索引进行访问。这里,Span<T>的第二个元素的值被修改成了11。因为数组arr1是被span引用的,因此在修改Span<T>的元素的时候,其对应的数组的第二个元素也被修改了:

private static Span<int> IntroSpans()
{
    int[] arr1 = { 1, 4, 5, 11, 13, 18 };
    var span1 = new Span<int>(arr1);
    span1[1] = 11;
    Console.WriteLine($"arr1[1] is changed via span1[1]: {arr1[1]}");
    return span1;
}

【22】7.10.1 创建切片

1.两种创建切片的方式:第一种方式是通过一个构造函数重载,通过传递开始位置和数组长度作为参数来实现的。另外一个构造函数的重载版本你可以只传递开始位置作为参数,这种情况下,将从开始位置直到数组结束作为切片。你也可以直接从Span<T>实例中创建切片,通过调用它的Slice方法,这里它的重载版本跟构造函数很类似。通过变量span4,先前创建的span1将会从第3个元素开始,创建连续4个元素的切片。

private static Span<int> CreateSlices(Span<int> span1)
{
    Console.WriteLine(nameof(CreateSlices));
    int[] arr2 = { 3, 5, 7, 9, 11, 13, 15 };
    var span2 = new Span<int>(arr2);
    var span3 = new Span<int>(arr2, start: 3, length: 3);
    var span4 = span1.Slice(start: 2, length: 4);
    DisplaySpan("content of span3", span3);
    DisplaySpan("content of span4", span4);
    Console.WriteLine();
    return span2;
}

2.DisplaySpan方法用来显示一个Span的内容。这个方法中使用了ReadOnlySpan,当你不需要修改span引用的内容时你可以使用这种span类型,就像DisplaySpan方法这么使用。

private static void DisplaySpan(string title, ReadOnlySpan<int> span)
{
    Console.WriteLine(title);
    for (int i = 0; i < span.Length; i++)
    {
        Console.Write($"{span[i]}.");
    }
    Console.WriteLine();
}

【23】7.10.2 使用Span 改变值

1.可以调用Clear方法,将int类型的Span内容全部置为0。你也可以调用Fill方法,来将Span中的内容都设置为你传递给Fill方法的参数值。你还可以拷贝Span<T>的内容到另外一个Span<T>对象。在使用CopyTo方法时,假如目标span不够大的话,将会抛出一个ArgumentException异常。你可以使用TryCopyTo方法避免这种情况的发生,假如拷贝失败的话,该方法会返回一个false。

2.通过以上方法来改变数组元素。

【24】7.10.3 只读的Span

1.假如你只需要读取数组段的值而不需要修改它,你可以使用ReadOnlySpan<T>,就像在DisplaySpan方法中已经用过的那样。通过ReadOnlySpan<T>,索引器是只读的,并且这种类型无法提供Clear和Fill操作。虽然你仍旧可以调用CopyTo方法,来将ReadOnlySpan<T>的内容拷贝到一个Span<T>上。

2.通过Span<int>和int[]赋值进行创建并且隐式地转换成了ReadOnlySpan<T>。

【25】7.11.1 创建数组池

1.数组池(Array Pool),也就是ArrayPool类。ArrayPool类中管理了许多数组(a pool of arrays)。你可以从数组池中申请(rent)数组并在使用完毕后归还给它。内存管理由ArrayPool自己负责处理。

2.通过ArrayPool<T>的静态Create方法来创建ArrayPool<T>数组池。出于效率的考虑,数组池将大小接近的数组安排到一起,并分成多个地址进行内存管理(manages memory in multiple buckets for arrays of similar sizes)。通过Create方法,你可以定义数组的最大长度以及一个地址(within a bucket)能管理的最大数组数量:

ArrayPool<int> customPool = ArrayPool<int>.Create(maxArrayLength: 40000, maxArraysPerBucket: 10);

3.默认的最大数组长度(maxArrayLength)是1024×1024字节,默认的地址最大数组数量(maxArraysPerBucket)是50。数组池使用多个地址(multiple buckets),以便更快地访问到大量不同的数组。大小接近的数组会尽可能地安排在一起(in the same bucket as long as possible),不超过最大数组数目。

【26】7.11.2 从数组池中申请内存

从数组池中请求内存是通过调用Rent方法。它接收一个数组需要的最小数组长度作为参数。假如数组池中的差不多大小的内存已经分配过,就返回该块内存。假如没有可用的内存,那么数组池就负责分配相应的内存并随后返回。

【27】7.11.3 将内存返回给数组池

你通过调用数组池的Return方法,将数组作为参数传递给它,来将数组占用的内存返回给数组池。通过一个可选的参数clearArray,你可以指定在将数组返回给数组池前是否需要清空该数组的值。假如不清空,下次从数组池中请求该大小的数组时,你依然可以读取原来的值。清空数据,你可以避免这个问题,但你需要更多的CPU时间去处理:

ArrayPool<int>.Shared.Return(arr, clearArray: true);

【28】7.12 小结

本章,你了解了C#创建使用简单、多维、锯齿数组的语法。Array类是数组后台实际对象,因此你可以通过数组变量调用Array类里的方法和属性。

你也了解了如何通过实现IComparable和IComparer接口对数组元素进行排序,并且你学习了如何创建和使用枚举器,包括IEnumerable和IEnumerator接口,以及yield语句。

本章最后一小节向你演示了如何更高效地通过Span<T>和ArrayPool进行数组操作。

参考网址:https://www.cnblogs.com/zenronphy/p/ProfessionalCSharp7Chapter7.html#751-%E5%88%9B%E5%BB%BA%E6%95%B0%E7%BB%84

原文地址:https://www.cnblogs.com/wazz/p/14143894.html