第七章 数组

简单数组

数组的声明与初始化

​ 声明一个int数组:

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

对于上面声明并初始化数组的三者, 编译器生成的代码都是相同的. 声明后的数组引用实际上指向数组元素的第一个. 已声明的数组无法改变大小.

简单数组的使用

​ 使用索引器[]访问数组元素, 索引从 0 开始. 数组只支持整型参数的索引器. ​ 如果使用了错误的索引值, 就会抛出IndexOutOfRangeException异常. ​ 数组的Length属性可以得到数组长度, 可以借此迭代数组中元素. 数组也支持foreach迭代.

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

for (var val in myArray)
{
    Console.WriteLine(myArray[i]);
}

​ 对于引用类型的数组, 数组变量引用(位于栈上)指向的元素也是引用(位于托管堆上), 初始化时需要单独为这些作为元素的引用构造对象(也在托管堆上).

Person[] couple = 
{
    new Person{"Adachi", "Sakura"},
    new Person{"Shimamura", "Hougetsu"}
};

多维数组

​ 不同于 C++, C# 中的多维数组使用一个方括号, 各维索引间使用逗号隔开. 数组在声明时应指定每一维的大小(也称为阶), 声明后不可修改. 下面声明一个二维数组:

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

可以使用数组初始化器来初始化多维数组, 可将其视为嵌套的等长数组(事实上, 锯齿数组才是真正的嵌套数组, 即元素为数组的数组).

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

Console.WriteLine(twodim[1,2]);	//output: 6

锯齿数组

​ 二维数组的大小对应一个矩形, 而对于锯齿数组, 每一行可以有不同的大小, 声明方式与多维数组不同, 锯齿数组通过留空最后的阶实现(这里的声明不能省略new int[], 因为数组初始化器只能用于变量或字段的初始化):

int[][] jagged = new int [3][]
{
    new int[] {1, 2},
    new int[] {3, 4, 5, 6, 7, 8},
    new int[] {9, 10, 11}
};

可以分别通过各级数组的Length属性获得当前维度数组长度:

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

下面这种用法更容易理解, 将锯齿数组作为管理一组数组的引用:

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

Array

​ 使用typename[]语法声明数组时, 对于编译器而言会创建一个派绳子抽象基类Array的新类. 前面使用的Length属性, foreach迭代数组用到的GetEnumerator()方法, 实际上都是Array类的成员. Array类实现的其他属性有LongLengthRank等, 这二者分别用来获得数组元素个数(Long类型)和数组维数.

创建数组

Array类是抽象类, 不能用构造函数创建Array类的对象. 如果只有要创建数组的对象而不知道对象类型, 可以使用静态方法CreateInstance()来创建数组, 将对象的Type对象作为第一个参数, 将数组的维度信息作为剩余参数.

//创建一个长度为5的一维 int 数组
Array intArray1 = Array.CreateInstance(typeof(int), 5);

//创建一个5行6列二维 int 数组
Array intArray2 = Array.CreateInstance(typeof(int), 5, 6);

//将Array对象转换为int[]对象
int[] intArray = (int[])intArray1;

CreateInstance()有许多重载版本, 可以创建多维数组和初始索引不为0的数组. SetValue()方法设置数组元素, 其参数为每一维的索引.

int[] length = { 2, 3 };	//作为各维度大小
int[] lowerBounds = { 1, 10 };	//作为索引起始值
//下面创建了一个2行3列的, 每行索引分别从1,10开始的二维数组
Array racers = Array.CreateInstance(typeof(Person), lengths, lowerBounds);
racers.SetValue(new Person("Alain", "Prost"), 1, 10);//为索引[1,10]的元素设置值

//转换为Person[]对象后索引起始值不变
Person[] racers2 = (Person[])racers;
var first = racers2[1,10];

复制数组

​ 数组是引用类型, 将数组变量赋值给另一个变量只会复制引用, 这会使两个数组对象实际上引用了同一个数组. 要复制数组, 可以使用Array类的Clone()方法, 这个方法会创建数组的浅表副本. ​ 如果数组的元素是值类型, 该方法会复制数组的所有值, 用这些值创建一个新的Array类对象并返回.

int[] intArray1 = {1, 2};
int[] intArray2 = (int[])intArray1.Clone();//创建一个新的Array对象, 引用的对象是与intArray1不同的数组对象

​ 如果数组元素是引用类型, Clone()方法会复制数组中的所有引用, 创建新的数组对象并返回. ​ Array 的浅层副本仅复制 Array 的元素, 无论它们是引用类型还是值类型, 它不复制引用所引用的对象. 新 Array 中的引用指向原始Array中的引用所指向的相同对象. ​ 如果需要创建引用类型对象的深层拷贝副本, 则必须迭代数组来创建新对象.

​ 除了使用Clone()方法外, 还可以使用Array.Copy()静态方法. Copy()方法需要把源数组与接收数据的目标数组分别作为前两个参数, 且要求目标数组兼容源数组. 该方法不创建新数组. 该方法相当于 C++ 的memmove()函数, 而不是memcpy().

排序

Array类的Sort()静态方法使用 QuickSort 算法对数组元素进行排序. 该方法要求数组元素实现IComparable接口(或其泛型版本IComparable<T>, 下面接口同样可以使用旧版本或泛型版本), 包含一个方法CompareTo(). 简单类型(如System.Int32System.String)已经实现了IComparable接口, 所以可以直接对这些元素的数组进行排序(string类按照字典序逐字符进行排序).

IComparable<T>.CompareTo(T)方法声明为public int CompareTo (T? other);. 该方法的返回值意义如下:

含义
小于零 此实例在排序顺序中位于 other 之前
此实例在排序顺序中的位置与 other 相同
大于零 此实例在排序顺序中位于 other 之后

如果实现 IComparable, 则一般情况下还建议重载>, >=, <<=运算符, 以返回与CompareTo(T)一致的值. 此外, 还应实现IEquatable.

​ 下面修改Person类, 使其实现IComparable<Person>接口. 使用string类的CompareTo()方法先后对LastName, FirstName字段排序.

public class Person: IComparable<Person>
{
    public int CompareTo(Person other)
    {
        if (other == null) return 1;
        int result = string.Compare(LastName, other.LastName);
        if (result == 0)
            result = string.Compare(FirstName, other.FirstName);
        return result;
    }
    //other members
}

现在可以按照上述规则对Person数组使用Array.Sort()方法进行排序.

Array.Sort()方法有很多重载版本, 例如可以传递一个IComparer对象或Comparison<T>委托来由该对象或委托定义排序方式. 如果使用Array.Sort方法并传递了一个Comparer对象或Comparison<T>委托, 那么被排序的数组元素不需要实现IComparable接口. ​ 下面实现类PersonComparer, 实现接口IComparer<Person>, 并使用该类的对象进行排序:

public enum PersonComoareType
{
    FirstName, LastName
}
public class PersonComparer : IComparer<Person>
{
    private PersonComoareType _typ;
    public PersonComparer(PersonComoareType compareType)
    {
        _typ = compareType;
    }
    public int Compare(Person? x, Person? y)
    {
        if (x is null && y is null) return 0;
        if (x is null) return 1;
        if (y is null) return -1;
        switch (_typ)
        {
            case PersonComoareType.FirstName:
                return string.Compare(x.FirstName, y.FirstName);
            case PersonComoareType.LastName:
                return string.Compare(x.LastName, y.LastName);
            default:
                throw new ArgumentException("unexpected compare type");
        }
    }
}

现在可以将PersonComparer对象传递给Array.Sort()作为第二个参数.

Array.Sort(persons, new PersonComparer(PersonComoareType.FirstName));

以上代码将persons数组按照名字( FirstName )排序.

数组协变

​ 数组支持协变, 这意味着可以将某类型派生类的数组赋值给该类型数组. 例如, 可以声明一个object[]类型的参数, 将一个Person[]参数传递给它. 但数组斜边只用于引用类型, 不能用于值类型, 且数组协变的异常只能用过运行时异常解决.

枚举器与迭代器

迭代器模式是设计模式中行为模式(behavioral pattern)的一个例子, 他是一种简化对象间通讯的模式, 也是一种非常容易理解和使用的模式. 简单来说, 迭代器模式使得你能够获取到序列中的所有元素而不用关心是其类型是 array, list, linked list 或者是其他什么序列结构. 这一点使得能够非常高效的构建数据处理通道(data pipeline)–即数据能够进入处理通道, 进行一系列的变换, 或者过滤, 然后得到结果. 事实上, 这正是LINQ的核心模式.

枚举器(enumerator)是一个实现了System.Collections.IEnumerator或者System.Collections.Generic.IEnumerator<T>接口的, 作用于序列值的, 只能向前的只读游标对象. 枚举器通过调用MoveNext()移动到下一个元素, Current属性返回当前元素. 通常来说任何一个包含名为MoveNext()方法和名称为Current属性的对象, .NET都会将它作为枚举器对待. IEnumerator接口的泛型版本IEnumerator<T>派生自接口IDisposable(声明为public interface IEnumerator<out T>: IDisposable, System.Collections.IEnumerator), 因此定义了Dispose()方法, 用来及时清理给枚举器分配的资源. 这使你可以在使用其他资源时关闭数据库连接或释放文件句柄或类似操作. 如果没有要释放的其他资源, 需要提供空的Dispose()实现. 枚举器通常用于静态集合(即预先确定的集合), 每次遍历时不会动态生成数据.

迭代器(iterator)是一种实现了集合对象遍历逻辑的模式, 可以让我们逐一访问集合中的元素, 而不需关心底层实现. C#中的迭代器通常利用yield return关键字定义, 简化了逐步返回元素的过程. yield return语句的意思是请求从枚举器产生的下一个元素. 每当遇到yield控制权都会回归到调用者那里, 但是被调用者的状态还会保持. 这个状态的生命周期绑定到了枚举器上, 当调用这完成枚举之后状态就被释放. 迭代器允许在任意时刻暂停和恢复, 因此更灵活, 可以在需要时动态生成元素.

可枚举对象(enumerator object)是一序列的逻辑表示, 本身不是游标, 但可以基于本身产生游标对象. 如果要迭代可枚举对象, 可以使用foreach语句. 可枚举对象可以是实现了IEnumerableIEnumerable<T>的对象, 也可以是具有名为GetEnumerator()方法并且方法返回一个枚举器的对象.

IEnumerator接口

​ C# 的foreach语句隐藏枚举器的复杂性. 因此, 建议使用IEnumerable接口与foreach, 而不是直接操作枚举器.

​ 实现IEnumerator接口的对象是枚举器. 由于IEnumerator<T>继承自IEnumerator, 下面给出IEnumerator<T>的函数成员:

成员 作用
Current属性 获取集合中枚举器当前位置的元素.
Dispose()方法 执行与释放, 释放或重置非托管资源关联的应用程序定义任务. (继承自IDisposable)
MoveNext()方法 将枚举器推进到集合的下一个元素, 并返回一个布尔值指示枚举数是否到达集合末尾. (继承自IEnumerator)
Reset()方法 将枚举器设置为其初始位置, 该位置位于集合中的第一个元素之前. (继承自IEnumerator)

foreach语句

​ C# 编译器会把foreach语句转换为IEnumerator接口的方法与属性. 下面是一条简单的foreach语句, 它迭代persons数组中的所有元素:

foreach (var p in persons)
{
    Console.WriteLine(p);
}

这段代码会被解析为下面的代码段:

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

yield语句

​ 在 C# 1.0 中, foreach语句可以轻松迭代集合, 但创建枚举器仍需要做大量工作. C# 2.0 中添加了yield语句, 极大简化了创建枚举器的工作.

​ 包含yield语句的方法也称为迭代块. 使用迭代块的方法返回类型必须声明为IEnumeratorIEnumerable接口, 或其泛型版本. 这个代码块中可以包含多条yield return语句或yield break语句, 但不能包含return语句. ​ 根据使用迭代块的方法返回类型不同(IEnumeratorIEnumerable), 编译器处理yield语句的方式略有差异. 当返回类型为IEnumerator时, yield语句使编译器自动生成一个实现了IEnumerator接口的隐藏的状态机类, 以支持迭代操作; 当返回类型为IEnumerable时, 编译器会生成一个支持多次遍历的状态机类, 该类同时实现IEnumerableIEnumerator 两个接口.

​ 值得注意的是, yield语句生成了一个用于枚举的状态机类, 而并非生成了一个包含所有枚举项的列表. 通过该枚举器访问每一项时, 就会访问枚举器得到当前项的数据. 这样做的结果是每进行一次访问枚举器的运算, 才会读取一个被迭代对象. 这样就可以迭代大量的数据而不需要一次把所有数据读入内存.

“状态机类”是一种自动生成的类, 在C#中主要用于处理异步代码, 迭代器(yield语句)或其他需要在执行过程中保存和恢复状态的代码结构. 编译器通过状态机类来跟踪代码的执行状态, 使得它可以在不同的执行点暂停和恢复, 从而支持延迟执行, 异步执行等复杂的流程控制.

​ 下面的例子用不同方式迭代类MusicTitles:

public class MusicTitles
{
    string[] names = 
    {"ANEMONE", "ARCADIA", "Bad Apple!!", "Mebius Ash", "Void"};
    public IEnumerator<string> GetEnumerator()
    {
        for(int i = 0; i < 5; ++i)
            yield return names[i];
    }
    public IEnumerable<string> Reverse()
    {
        for(int i = 4; 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];
    }
}

​ 也许你已经注意到了, 上述代码中GetEnumerator()返回类型为IEnumerator, 而下面两个自定义的命名迭代返回类型为IEnumerable. GetEnumerator()方法实现的迭代称为默认迭代, 约定返回类型为IEnumerator; 而自定义的命名迭代(如上述代码中Reverse()Subset()方法)返回值应当为IEnumerable, 以支持多次迭代而不需要每次手动重置枚举器状态. ​ 有了上述定义就可以使用foreach语句以不同形式对该类进行迭代, 通过下面代码理解为什么命名迭代放回类型应为IEnumerable而不是IEnumerator:

var titles = new MusicTitles();
for(var title in titles) 
{//titles是一个有GetEnumerator方法的可迭代对象
    //do something...
}
for(var title in titles.Reverse()) 
{//titles.Reverse()是实现了接口IEnumerable的可迭代对象
    //do something...
}
for(var title in titles.Subset(2,2))
{//titles.Subset()是实现了接口IEnumerable的可迭代对象
    //do something...
}

yield语句状态机使用示例

​ 可以使用yield语句生成的多个状态机类完成更复杂的任务. 下面的代码使用GameMove类完成 Tic-Tac-Toe 游戏(井字棋)的 Cross 与 Circle 轮流放置的操作, 使用两个迭代块交叉执行实现:

public class GameMoves
{
    private IEnumerator _cross;
    private IEnumerator _circle;

    public GameMoves()
    {
        _cross = Cross();
        _circle = Circle();
    }

    private int _move = 0;
    const int MaxMoves = 9;

    public IEnumerator Cross()
    {
        while (true)
        {
            Console.WriteLine($"Cross, move {_move}");
            //do something else...
            if (++_move >= MaxMoves)
                yield break;
            yield return _circle;
        }
    }

    public IEnumerator Circle()
    {
        while (true)
        {
            Console.WriteLine($"Circle, move {_move}");
            //do something else...
            if (++_move >= MaxMoves)
                yield break;
            yield return _cross;
        }
    }
}

在客户端程序中可以按照如下方式使用该类:

var game = new GameMoves();
IEnumerator enumerator = game.Cross();	//规定第一次放置是cross还是circle
while (enumerator.MoveNext())
{
    enumerator = enumerator.Current as IEnumerator;
}

输出如下:

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

要理解上面的代码, 需要明确的是代码中的_cross, _circle, enumerator始终都是对Cross()Circle()方法创建的两个状态机类的引用. 局部变量enumerator的值始终在两个状态机引用之间交替变化, 因此在每次循环条件enumerator.MoveNext()的计算中实际上实现了两个状态机交替调用各自的MoveNext()方法, 执行MoveNext()方法实际作用是继续执行状态机类迭代块的代码, 直到遇到下一个yield语句(进而更新Current属性), 因此得以在两个代码块中交替执行各自代码.

状态机工作原理简述

  1. 状态保存和恢复: 状态机类在每次执行yield return时会保存当前状态, 并将控制权交还给调用方(例如foreach循环). 状态机类内部维护了一个state字段, 记录当前的迭代状态. 每次调用MoveNext()时, 状态机会根据该state字段判断从哪个位置继续执行.

  2. 遇到yield return时暂停: 当代码执行到yield return语句时, 状态机会将Current属性设置为yield return返回的值, 并暂停执行, 将控制权返回给调用方. 此时, MoveNext()返回true, 表示有新元素可以访问.

  3. 控制权交还调用方: 在调用方的代码中, 可以通过访问Current属性获取当前的迭代值. 然后, 调用方可以再次调用MoveNext()以继续遍历, 状态机会恢复上一次的执行状态, 从yield return之后的代码继续执行.

  4. 遇到yield break或代码末尾: 当代码执行到yield break或方法结尾时, MoveNext()会返回false, 表示迭代结束.

结构比较

IStructuralEquatable接口用于比较两个集合化对象(数组, 元组等)是否有相同的内容, IStructuralComparable接口用于集合化对象的结构化比较. 这两个接口不仅可以比较引用, 还可以比较内容. 这些接口是显示实现的, 所以在使用时需要把对象强制转换为这个接口类型. 数组和元组等实现了这两个接口.

IStructuralEquatable接口包含两个方法:Equals(Object, IEqualityComparer), GetHashCode(IEqualityComparer), 其中IEqualityComparer对象用来比较相等性与计算哈希值. 调用这个方法时, 可以使用可以通过IEqualityComparer<T>完成对相应类型的默认实现. 这个默认实现检查该类型是否实现了IEquatable接口, 如果实现则调用IEquatable.Equals()方法, 否则调用object.Equals()方法进行比较.

Span

​ 为了快速访问托管或非托管的连续内存, 可以使用Span<T>结构. 使用Span<T>可以提供任意连续内存的类型安全和内存安全表示形式. Span 直接访问数组或其他连续内存的切片, 而无需复制这段数据.

Span<T>声明如下:public readonly ref struct Span<T>.

Span<T>提供了索引器, 可以像使用数组那样通过索引器访问Span<T>的数据. 通过Span<T>可以改变原数组的元素.

创建切片

Span<T>有如下构造函数:

构造函数 解释
Span(T) 围绕指定的引用创建一个长度为 1 的新 Span
Span(T[]) 在整个指定数组上创建新的 Span 对象
Span(T[], Int32, Int32) 从指定索引开始, 创建包含数组的指定元素数的新 Span 对象
Span(Void*, Int32)) 从指定数量的T元素(从指定内存地址开始)创建一个新 Span 对象
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); //9.11.13.
    DisplaySpan("content of span4", span4); //6.8.10.12.
    Console.WriteLine();
    return span2;
}

使用 Span 改变值

​ 可以使用索引器直接改变 Span 的值:

span3[0] = 3;
DisplaySpan("content of span3", span3); //3.11.13.
Console.WriteLine(arr2[3]); //3

Clear()方法用 0 填充包含int类型的 span. ​ Fill(T)方法可以用传递给方法的值来填充 span. ​ CopyTo(Span<T>)方法可以将调用方法的 span 赋值给另一个 span, 如果目标 span 不够大, 就会抛出ArgumentException异常. 可以用bool TryCopyTo(Span<T>)方法避免抛出异常, 该方法返回一个布尔值指示复制是否成功, 如果成功返回 true, 否则返回 false. ​ Slice()方法可以创建当前 span 的切片, 结果作为返回值返回. Span<T> Slice(int start)返回由当前范围(从start到范围末尾)的所有元素组成的切片, Span<T> Slice(int start, int length)返回由当前范围(从start开始)中length元素组成的切片. ​ Span 类对==运算符进行了重载, 如果两个 Span 对象具有相同的长度并且左和右的相应元素指向相同的内存, 则它们相等. 请注意, 该运算符并不试图确定内容是否相等, 而仅用于确定 Span 对象是否引用一段相同的内存.

只读的 Span

​ 如果不需要更改 span 引用的内容则可以使用ReadOnlySpan<T>. 这种类型没有提供Clear()Fill()方法, 但是可以调用CopyTo()方法将只读的 ReadOnlySpan 的内容复制到 Span 中.

数组池

​ 如果程序创建和销毁了很多数组, GC (垃圾收集器)就会有一些工作要做. 为了减少 GC 的工作从而优化性能, 可以使用ArrayPool类. ArrayPool类管理一个数组池, 数组可以从这里租借, 并在数组不需要之后返回到池中. 内存由ArrayPool管理. ​ 数组池使用多个桶, 以遍在使用多个数组时更快地访问数组.

​ 调用Rent()方法时, 参数传入所需的数组长度, ArrayPool<T>会在池中找到一个满足需求的最小数组. 如果没有合适的数组, 它会分配一个新数组. 这样做的好处是避免了过多的小数组分配, 提升内存利用率. ​ 当不再需要数组时, 可以调用Return方法将其归还池中. ArrayPool<T>会在归还时判断数组是否合适保留在池中. 如果池已满或数组不符合缓存要求, 可能会直接释放该数组内存. ArrayPool<T>会限制池中的数组大小, 以避免极大或不常用的数组占据大量内存.

创建数组池

ArrayPool<T>.Shared提供了一个全局共享池, 适合大多数通用场景; 同时, 也可以通过ArrayPool<T>.Create()创建自定义的池, 允许根据特定需求自定义行为, 比如最大池大小和数组大小控制.

ArrayPool类的构造函数受保护(protected), 无法使用构造函数直接构造该类对象. 应该调用静态方法Create()方法创建一个ArrayPool<T>对象. 该方法声明如下:

public static System.Buffers.ArrayPool<T> Create ();
public static System.Buffers.ArrayPool<T> Create (int maxArrayLength, int maxArraysPerBucket);

其中maxArrayLength默认值为 1024*1024 字节, maxArraysPerBucket默认值为 50.

租用和返回内存

Rent(int minimumLength)方法请求池中长度至少为minimumLength的数组. 若这样的内存存在则直接返回, 若不存在则分配一块新的内存并返回.

Return(T[] array, bool clearArray = false)方法将数组返还给数组池中. 可选参数clearArray指定在返回池之前是否清除该段内存数据. 如果不清除, 下一个租借到该段内存的主体可以读取到返回前的数据. 清除数据可以避免这一点, 但会消耗更多的 CPU 时间.

for (int i = 0; i < 10; i++)
{
    int arrayLength = (i + 1) << 10;
    int[] arr = ArrayPool<int>.Shared.Rent(arrayLength);
    Console.WriteLine($"requested an array of {arrayLength} and received {arr.Length}");
    for (int j = 0; j < arrayLength * j; j++)
    {
        arr[j] = j;
    }
    ArrayPool<int>.Shared.Return(arr, clearArray: true);
}

输出结果为:

requested an array of 1024 and received 1024
requested an array of 2048 and received 2048
requested an array of 3072 and received 4096
requested an array of 4096 and received 4096
requested an array of 5120 and received 8192
requested an array of 6144 and received 8192
requested an array of 7168 and received 8192
requested an array of 8192 and received 8192
requested an array of 9216 and received 16384
requested an array of 10240 and received 16384