C# 数组

1. 简单数组

当需要存储多个相同类型的元素时,C# 数组是一种非常常用的数据结构。数组是一种固定长度的数据结构,它由连续的内存单元组成,每个内存单元存储一个元素。

1.2 数组的声明

// elementType[] arrayName;
// elementType 是数组中元素的类型,arrayName 是数组的名称
int[] myArray;

1.3 数组的初始化

静态初始化:指定数组元素的初始值。

// elementType[] arrayName = { value1, value2, value3, ... };
int[] myArray = { 1, 2, 3 };

动态初始化:指定数组的长度,然后为每个元素分配空间。

// elementType[] arrayName = new elementType[length];
int[] myArray = new int[3]; 

注意:在指定了数组的大小后,如果不复制数组中的所有元素,就不能重新设置数组的大小。如果事先不知道数组中包含多少个元素,就可以使用集合。

int[] myArray = new int[4];
int[] myArray = new int[4] {4, 5, 6, 7}; // 数组初始化器只能在声明数组变量时使用,不能在声明数组之后使用。
int[] myArray = new int[] {4, 5, 6, 7}; // 如果用花括号初始化数组,则还可以不指定数组的大小,因为编译器会自动统计元素的个数。
int[] myArray = {4, 5, 6, 7}; // 更简便的声明方式。

1.4 访问数组元素

通过索引器传递元素编号,就可以访问数组。索引器总是以0开头,表示第一个元素。可以传递给索引器的最大值是元素个数减1,因为索引从0开始。

int[] array = new[] { 4, 5, 6, 7 };
int v1 = array[0]; // 4
int v2 = array[1]; // 5
array[3] = 44; // { 4, 5, 6, 44 }

1.5 遍历数组

for (int i = 0; i < arrayName.Length; i++)
{
    // 访问 arrayName[i] 进行操作
}

foreach (elementType element in arrayName)
{
    // 对 element 进行操作
}

1.6 使用引用类型

public class Person
{
    public Person(string firstName, string lastName)
    {
        FirstName = firstName;
        LastName = lastName;
    }
    public string FirstName { get; }
    public string LastName { get; }
    public override string ToString() => $"{FirstName} {LastName}";
}

声明一个包含两个 Person 元素的数组与声明一个 int 数组类似:

Person[] myPersons = new Person[2];

但是必须注意,如果数组中的元素是引用类型,就必须为每个数组元素分配内存。如果使用了数组中未分配内存的元素,则会抛出 NullReferenceException 类型的异常。

1.7 数组方法和属性

数组类提供了一些有用的方法和属性,例如:

  • Array.Sort(arrayName): 对数组进行排序。

  • Array.Reverse(arrayName): 反转数组元素的顺序。

  • Array.IndexOf(arrayName, value): 返回指定值在数组中的索引。

  • Array.Copy(sourceArray, destinationArray, length): 将一个数组的元素复制到另一个数组中。

  • Array.Length: 获取数组的长度。

  • Array.Rank: 获取数组的维度数。

2. 多维数组

int[,] twoDim = new int[3, 3]; // 二维数组
twoDim[0, 0] = 1;
twoDim[0, 1] = 2;
twoDim[0, 2] = 3;
twoDim[1, 0] = 4;
twoDim[1, 1] = 5;
twoDim[1, 2] = 6;
twoDim[2, 0] = 7;
twoDim[2, 1] = 8;
twoDim[2, 2] = 9;

注意:声明数组后,就不能修改其阶数了。

int[,] twoDim = { { 1, 2, 3 }, { 4, 5, 6 }, { 7, 8, 9 } }; // 如果事先知道元素的值,就可以使用数组索引器来初始化多维数组。

注意:使用数组初始化器时,必须初始化数组的每个元素,不能把某些值的初始化放在以后完成。

3. 锯齿数组

int[][] jagged = new int[3][];
jagged[0] = new int[2] { 1, 2 };
jagged[1] = new int[6] { 1, 2, 3, 4, 5, 6 };
jagged[2] = new int[3] { 1, 2, 3 };
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]}");
    }
}
/*
row: 0,element: 0, value: 1
row: 0,element: 1, value: 2
row: 1,element: 0, value: 1
row: 1,element: 1, value: 2
row: 1,element: 2, value: 3
row: 1,element: 3, value: 4
row: 1,element: 4, value: 5
row: 1,element: 5, value: 6
row: 2,element: 0, value: 1
row: 2,element: 1, value: 2
row: 2,element: 2, value: 3
*/

4. Array 类

Array 类

用方括号声明数组是 C# 中使用 Array 类的表示法。在后台使用 C# 语法,会创建一个派生自抽象基类 Array 的新类。这样,就可以使用 Array 类为每个 C# 数组定义的方法和属性了。

4.1 创建数组

Array intArray1 = Array.CreateInstance(typeof(int), 5); // Array.CreateInstance: 初始化 Array 类的新实例。
for (int i = 0; i < 5; i++)
{
    intArray1.SetValue(33, i); // SetValue: 将值设置为一维 Array 中指定位置的元素。
}

for (int i = 0; i < 5; i++)
{
    Console.WriteLine(intArray1.GetValue(i));
}

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

4.2 复制数组

因为数组是引用类型,所以将一个数组变量赋予另一个数组变量,就会得到两个引用同一数组的变量。而复制数组,会使数组实现 ICloneable 接口。这个接口定义的 Clone() 方法会创建数组的浅表副本。

如果数组的元素是值类型,以下代码段就会复制所有值。

int[] intArray1 = { 1, 2 };
int[] intArray2 = (int[])intArray1.Clone();

如果数组包含引用类型,则不复制元素,而只复制引用。

除了使用 Clone() 方法之外,还可以使用 Array.Copy() 方法创建浅表副本。但 Clone() 方法和 Copy() 方法有一个重要区别:Clone() 方法会创建一个新数组,而 Copy() 方法必须传递阶数相同且有足够元素的已有数组。

注意:如果需要包含引用类型的数组的深层副本,就必须迭代数组并创建新对象。

4.2.1 Array.Clone() 和 Array.Copy()的区别

Array.Clone() 方法和 Array.Copy() 方法都是用于在 C# 中复制数组的方法,但它们之间有一些区别。

  • Array.Clone() 方法:

    • Array.Clone() 方法是 System.Array 类的一个实例方法。

    • 它用于创建当前数组的浅拷贝,即创建一个新数组,其中包含与原始数组相同的元素。

    • 返回的新数组对象是一个独立的实例,对新数组的修改不会影响原始数组,反之亦然。

    • 如果原始数组是多维数组,则返回的也是多维数组,维度和长度与原始数组相同。

    • 示例:

int[] originalArray = { 1, 2, 3, 4, 5 };
int[] clonedArray = (int[])originalArray.Clone();
  • # Array.Copy() 方法:

    • Array.Copy() 方法是 System.Array 类的一个静态方法。

    • 它用于将一个数组的元素复制到另一个数组中。

    • 可以选择性地指定源数组的起始索引和目标数组的起始索引以及要复制的元素数量。

    • 返回的是 void,即没有返回值,它直接修改目标数组。

    • 如果目标数组长度不足以容纳要复制的元素数量,则会引发 ArgumentException

    • 示例:

int[] sourceArray = { 1, 2, 3, 4, 5 };
int[] destinationArray = new int[5];
Array.Copy(sourceArray, destinationArray, sourceArray.Length);

需要注意的是,这两种方法都是浅拷贝,即只复制数组的元素,而不复制元素引用的对象。如果数组中的元素是引用类型(例如类对象),则原始数组和新数组将引用相同的对象。如果需要进行深拷贝,即复制对象的副本而不是引用,则需要手动处理每个元素的复制。

因此,Array.Clone() 方法适用于需要创建原始数组的副本且不希望修改原始数组的情况,而 Array.Copy() 方法适用于将元素从一个数组复制到另一个数组的情况。

4.2.2 浅拷贝和深拷贝

在 C# 中,浅拷贝(Shallow Copy)和深拷贝(Deep Copy)是两种不同的对象复制方式,它们有着不同的特点和应用场景。下面是对浅拷贝和深拷贝的详细介绍,并使用代码进行演示:

  • 浅拷贝(Shallow Copy):

    • 浅拷贝是指创建一个新对象,并将原始对象的字段值复制到新对象,但引用类型字段仍然指向相同的引用对象。

    • 这意味着新对象和原始对象共享相同的引用对象,对引用对象的修改会影响到两个对象。

    • 浅拷贝通常通过 MemberwiseClone() 方法实现,它会创建一个新对象并复制字段值。

    • 浅拷贝适用于对象之间相对简单的关联关系,或者在需要共享引用对象时。

下面是使用浅拷贝的示例代码:

// 创建一个引用类型的自定义类
public class MyReferenceClass
{
    public int ReferenceValue { get; set; }
}

// 创建一个自定义类
public class MyClass
{
    public int MyValue { get; set; }
    public MyReferenceClass MyReference { get; set; }

    public MyClass(int value, MyReferenceClass reference)
    {
        MyValue = value;
        MyReference = reference;
    }
}

// 创建原始对象
MyReferenceClass originalReference = new MyReferenceClass();
originalReference.ReferenceValue = 10;
MyClass originalObj = new MyClass(20, originalReference);

// 执行浅拷贝
MyClass clonedObj = (MyClass)originalObj.MemberwiseClone();

// 修改引用对象的值
clonedObj.MyReference.ReferenceValue = 30;

// 输出原始对象的值
Console.WriteLine(originalObj.MyReference.ReferenceValue);  // 输出: 30

在上述示例中,通过 MemberwiseClone() 方法执行浅拷贝,将原始对象的字段值复制到了新对象,但引用类型字段 MyReference 仍然引用同一个引用对象。当修改新对象中引用对象的字段值时,原始对象中引用对象的字段值也会被修改。

  • 深拷贝(Deep Copy):

    • 深拷贝是指创建一个新对象,并将原始对象的字段值复制到新对象,同时对引用类型字段也进行复制,创建独立的引用对象。

    • 这意味着新对象和原始对象拥有彼此独立的引用对象,对引用对象的修改不会影响到两个对象。

    • 深拷贝需要手动复制引用类型字段,通常通过自定义的复制逻辑或使用序列化和反序列化来实现。

    • 深拷贝适用于对象之间有复杂的关联关系,或者需要独立的引用对象副本的情况。

下面是使用深拷贝的示例代码:

// 创建一个引用类型的自定义类
public class MyReferenceClass
{
    public int ReferenceValue { get; set; }
}

// 创建一个自定义类
public class MyClass
{
    public int MyValue { get; set; }
    public MyReferenceClass MyReference { get; set; }

    public MyClass(int value, MyReferenceClass reference)
    {
        MyValue = value;
        MyReference = reference;
    }

    public MyClass DeepCopy()
    {
        // 创建新对象并复制字段值
        MyClass newObject = new MyClass(MyValue, null);

        // 复制引用类型字段
        newObject.MyReference = new MyReferenceClass();
        newObject.MyReference.ReferenceValue = MyReference.ReferenceValue;

        return newObject;
    }
}

// 创建原始对象
MyReferenceClass originalReference = new MyReferenceClass();
originalReference.ReferenceValue = 10;
MyClass originalObj = new MyClass(20, originalReference);

// 执行深拷贝
MyClass clonedObj = originalObj.DeepCopy();

// 修改引用对象的值
clonedObj.MyReference.ReferenceValue = 30;

// 输出原始对象的值
Console.WriteLine(originalObj.MyReference.ReferenceValue);  // 输出: 10

在上述示例中,通过自定义的 DeepCopy() 方法执行深拷贝,创建了一个新对象并复制了字段值,同时对引用类型字段 MyReference 也进行了复制,创建了一个独立的引用对象。当修改新对象中引用对象的字段值时,原始对象中引用对象的字段值不受影响。

5. 排序

在 C# 中,可以使用内置的排序算法对数组进行排序。C# 提供了 Array.Sort()Array.Reverse() 方法来实现对数组的排序和逆转。

  • Array.Sort() 方法:

    • Array.Sort() 方法用于按升序对数组的元素进行排序。

    • Array.Sort() 方法使用的是快速排序算法,它是一种高效的排序算法。

    • 对于基本类型数组和实现了 IComparable 接口的引用类型数组,可以直接使用 Array.Sort() 方法进行排序。

    • 对于自定义类型的数组,可以通过实现 IComparable 接口来定义排序规则。

    • Array.Sort() 方法会直接修改原始数组,而不会创建新的数组。

以下是使用 Array.Sort() 方法对整数数组进行排序的示例代码:

int[] numbers = { 5, 3, 8, 2, 1, 7, 6, 4 };
Array.Sort(numbers);

foreach (int num in numbers)
{
    Console.WriteLine(num);
}

/*
1
2
3
4
5
6
7
8
*/
  • Array.Reverse() 方法:

    • Array.Reverse() 方法用于反转数组的元素的顺序。

    • Array.Reverse() 方法会直接修改原始数组,而不会创建新的数组。

以下是使用 Array.Reverse() 方法对整数数组进行逆转的示例代码:

int[] numbers = { 1, 2, 3, 4, 5 };
Array.Reverse(numbers);

foreach (int num in numbers)
{
    Console.WriteLine(num);
}

/*
5
4
3
2
1
*/

需要注意的是,以上示例是对整数数组进行排序和逆转的演示。对于其他类型的数组,可以根据需要进行相应的调整。对于自定义类型的数组,可以通过实现 IComparable 接口来定义排序规则。

6. 数组作为参数

在 C# 中,数组可以作为方法的参数进行传递。通过将数组作为参数传递给方法,可以在方法内部对数组进行操作或处理。

  • 传递数组作为参数:

    • 在方法定义中,可以使用数组类型作为参数类型来接收传递的数组。

    • 可以使用数组名作为参数,或者指定数组类型和数组名作为参数。

    • 数组作为参数传递时,实际上是将数组的引用传递给方法,而不是数组的副本。因此,对传递的数组进行修改会影响原始数组。

  • 修改传递的数组:

    • 在方法内部,可以通过参数接收到的数组进行修改。

    • 对传递的数组进行修改,会直接修改原始数组,因为传递的是数组的引用。

    • 修改数组的元素、修改数组的长度或重新分配数组的内存等操作都会影响原始数组。

以下是一个示例代码,演示了如何将数组作为参数传递给方法,并在方法内部修改数组:

class Program
{
    static void ModifyArray(int[] arr)
    {
        // 修改数组元素
        arr[0] = 100;
        
        // 修改数组长度
        Array.Resize(ref arr, 3);
    }

    static void Main(string[] args)
    {
        int[] numbers = { 1, 2, 3, 4, 5 };

        Console.WriteLine("Before modification:");
        foreach (int num in numbers)
        {
            Console.WriteLine(num);
        }

        // 调用方法并传递数组作为参数
        ModifyArray(numbers);

        Console.WriteLine("After modification:");
        foreach (int num in numbers)
        {
            Console.WriteLine(num);
        }
    }
}

/*
Before modification:
1
2
3
4
5
After modification:
100
2
3
*/

在上述示例中,数组 numbers 被作为参数传递给 ModifyArray() 方法。在方法内部,我们修改了数组的第一个元素,并通过 Array.Resize() 方法修改了数组的长度。这些修改直接影响了原始的 numbers 数组,因为传递的是数组的引用。

需要注意的是,如果在方法内部重新分配数组的内存空间(如使用 Array.Resize()),原始数组的引用可能会失效,因此在修改数组长度时需要小心处理。

7. 数组协变

在 C# 中,数组协变(Array Covariance)是指可以将一个类型的数组赋值给其基类型的数组。这意味着可以将派生类型的数组赋值给基类型的数组,而不会导致编译错误。数组协变提供了一种灵活的方式来处理数组的赋值和传递。

  • 数组协变的规则:

    • 数组协变仅适用于引用类型的数组,不适用于值类型数组。

    • 只有在派生类型可以隐式转换为基类型的情况下,数组协变才适用。

    • 数组协变不适用于具有值类型元素的数组,因为值类型之间不能发生继承关系。

  • 数组协变的示例: 以下是一个示例代码,演示了数组协变的用法:

class Animal { }
class Dog : Animal { }
class Cat : Animal { }

class Program
{
    static void Main(string[] args)
    {
        Dog[] dogs = new Dog[] { new Dog(), new Dog() };
        Animal[] animals = dogs;  // 数组协变

        animals[1] = new Cat();  // 将 Cat 对象赋值给 Animal 数组

        foreach (Animal animal in animals)
        {
            Console.WriteLine(animal.GetType().Name);
        }
    }
}

/*
Dog
Cat
*/

在上述示例中,我们创建了一个 Dog 类和一个 Cat 类,它们都继承自 Animal 类。然后,我们创建了一个 Dog 类型的数组 dogs,并将其赋值给一个 Animal 类型的数组 animals。这里的数组协变使得我们可以将 Dog 数组赋值给 Animal 数组。

接下来,我们将一个 Cat 对象赋值给了 animals 数组的第二个元素,这是因为 Cat 类型也继承自 Animal 类型。最后,我们遍历 animals 数组,发现第一个元素仍然是 Dog 类型,而第二个元素已经变成了 Cat 类型。

需要注意的是,数组协变只在编译时进行类型检查,而在运行时不会进行类型转换。如果尝试将不兼容的类型赋值给数组元素,将在运行时抛出异常。因此,在使用数组协变时,要确保只将派生类型赋值给基类型的数组,并避免出现类型不匹配的情况。

在数组协变中,将派生类的数组赋值给基类的数组,对派生类的元素进行修改不会直接影响基类的元素。数组协变只影响数组的赋值和类型转换,而不会改变数组元素的行为。

8. 枚举

8.1 IEnumerator 接口

在 C# 中,IEnumerator 接口是用于支持集合类的迭代的接口。它定义了用于访问集合中元素的成员,并提供了一种统一的方式来遍历集合中的元素。

  • IEnumerator 接口的成员: IEnumerator 接口定义了以下成员:

    • Current 属性:获取集合中当前位置的元素。

    • MoveNext() 方法:将迭代器推进到集合中的下一个元素。

    • Reset() 方法:将迭代器重置到集合的开头。

  • 使用 IEnumerator 接口遍历集合:

    • 要使用 IEnumerator 接口遍历集合,首先需要获取集合的 IEnumerator 对象,通常使用集合的 GetEnumerator() 方法来实现。

    • 调用 MoveNext() 方法将迭代器推进到集合中的下一个元素,并返回一个布尔值指示是否还有更多的元素可供遍历。

    • 使用 Current 属性获取当前位置的元素值。

    • 如果需要重新开始遍历,可以调用 Reset() 方法将迭代器重置到集合的开头。

以下是一个示例代码,演示了如何使用 IEnumerator 接口遍历集合:

using System;
using System.Collections;

class Program
{
    static void Main(string[] args)
    {
        ArrayList list = new ArrayList();
        list.Add("Apple");
        list.Add("Banana");
        list.Add("Orange");

        IEnumerator enumerator = list.GetEnumerator();

        while (enumerator.MoveNext())
        {
            string fruit = (string)enumerator.Current;
            Console.WriteLine(fruit);
        }

        enumerator.Reset();

        while (enumerator.MoveNext())
        {
            string fruit = (string)enumerator.Current;
            Console.WriteLine(fruit);
        }
    }
}
/*
Apple
Banana
Orange
Apple
Banana
Orange
*/

在上述示例中,我们创建了一个 ArrayList 集合,并向其中添加了几个元素。然后,我们使用集合的 GetEnumerator() 方法获取一个 IEnumerator 对象,用于遍历集合。

通过调用 MoveNext() 方法,我们将迭代器推进到集合中的下一个元素。在每次循环中,我们使用 Current 属性获取当前位置的元素值,并将其转换为 string 类型。

在第一个循环结束后,我们调用 Reset() 方法将迭代器重置到集合的开头。然后,再次使用 MoveNext()Current 遍历集合,输出相同的元素。

需要注意的是,迭代器模式在遍历过程中通常会检测集合的结构是否发生了变化,以确保遍历的安全性。如果在遍历过程中修改了集合的结构(如添加、删除元素),可能会引发 InvalidOperationException 异常。因此,在使用迭代器遍历集合时,应注意避免修改集合的结构。

8.2 foreach 语句

在 C# 中,foreach 语句是用于遍历集合或数组的循环结构。它提供了一种简洁的方式来逐个访问集合中的元素,而无需使用索引或迭代器。

  • foreach 语句的语法:

    • foreach 语句的基本语法如下:
foreach (var item in collection)
{
// 执行操作
}
  • item 是一个变量,用于存储集合中的当前元素值。

    • collection 是一个实现了 IEnumerable 或 IEnumerable 接口的集合或数组。

    • 在循环体内,可以对 item 进行操作,访问集合中的元素。

  • foreach 语句的特点:

    • foreach 语句提供了一种简洁的方式来遍历集合或数组中的元素,无需使用索引或迭代器。

    • foreach 语句会自动处理集合的迭代过程,使得代码更加清晰和易读。

    • foreach 语句在循环开始时会自动获取集合的迭代器,并在每次迭代时更新当前元素。

  • foreach 语句的底层原理:

    • foreach 语句的底层实现依赖于 IEnumerable 接口和迭代器模式。

    • 集合类或数组类实现了 IEnumerable 或 IEnumerable 接口,该接口定义了 GetEnumerator() 方法,用于返回一个实现了 IEnumerator 或 IEnumerator 接口的迭代器对象。

    • foreach 语句在编译时会被转换为使用迭代器的代码,以实现对集合的遍历。

    • 在循环开始时,foreach 语句会调用集合的 GetEnumerator() 方法获取一个迭代器对象。

    • 然后,使用迭代器的 MoveNext() 方法将迭代器推进到集合中的下一个元素,并使用 Current 属性获取当前元素的值。

    • 在每次循环迭代中,循环体内的代码会对当前元素进行操作。

    • 当迭代结束时,或者遇到 break 语句时,循环退出。

需要注意的是,使用 foreach 语句遍历集合时,不应在循环过程中修改集合的结构(如添加、删除元素),否则可能会引发异常。foreach 语句在循环开始时获取了集合的迭代器,如果在迭代过程中修改了集合的结构,会导致迭代器失效,从而引发 InvalidOperationException 异常。

8.3 yield 语句

在 C# 中,yield 语句用于创建迭代器(iterator),它提供了一种简洁的方式来实现可枚举类型(enumerable types)的迭代。通过使用 yield 语句,可以在迭代过程中逐个返回元素,而无需显式地实现迭代器的接口和方法。

  • yield 语句的语法:

    • yield 语句通常与一个迭代器方法(iterator method)一起使用。迭代器方法是一种特殊的方法,它使用 yield 语句来指示迭代过程的逻辑。

    • IEnumerable<T>:迭代器方法返回类型应为 IEnumerable<T>,表示它可以产生一个序列的元素。

    • yield return:使用 yield return 语句返回序列中的元素。可以在迭代过程中多次使用 yield return 语句返回多个元素。

    • yield break:使用 yield break 语句提前终止迭代,返回到调用代码。

迭代器方法的基本语法如下:

public IEnumerable<T> MyIterator()
{
    // 迭代逻辑
    yield return element;
}
  • yield 语句的特点:

    • 使用 yield 语句可以使迭代过程更加简洁和易读,避免了手动实现迭代器的复杂性。

    • yield 语句将迭代逻辑与产生序列的元素分离,使代码更加模块化和可维护。

  • yield 语句的底层原理:

    • yield 语句的底层原理基于状态机(state machine)的概念。

    • 当编译器遇到包含 yield 语句的迭代器方法时,它将生成一个状态机类(state machine class)来处理迭代逻辑。

    • 状态机类实现了 IEnumerator<T> 接口,并使用状态来追踪迭代过程。

    • 每次调用迭代器方法时,会创建一个状态机对象的实例,并返回该实例的迭代器。

    • 在每次调用迭代器的 MoveNext() 方法时,状态机根据当前状态执行相应的逻辑,并返回下一个元素的值。

    • 使用 yield return 语句返回元素时,状态机会将当前状态保存下来,并在下一次调用 MoveNext() 时继续执行。

    • 当遇到 yield break 语句或迭代结束时,状态机停止迭代,并将迭代器标记为结束。

下面是一个使用 yield 语句的简单示例,演示了如何使用 yield 语句来创建一个简单的迭代器方法:

using System;
using System.Collections.Generic;

public class Program
{
    public static void Main()
    {
        foreach (var number in GetNumbers())
        {
            Console.WriteLine(number);
        }
    }

    public static IEnumerable<int> GetNumbers()
    {
        yield return 1;
        yield return 2;
        yield return 3;
    }
}

/*
1
2
3
*/

在上述示例中,GetNumbers() 方法是一个迭代器方法,返回类型为 IEnumerable<int>。通过使用 yield return 语句,每次迭代时返回一个整数元素。在 Main() 方法中,使用 foreach 循环来遍历迭代器方法返回的序列,并输出每个元素的值。

需要注意的是,使用 yield 语句创建的迭代器是惰性求值的。即在迭代过程中,元素是按需生成的,只有在调用者请求时才会计算和返回元素值。这种惰性求值的特性可以提高性能和节省内存,尤其在处理大型数据集时。

8.3.1 迭代集合的不同方式

在 C# 中,可以使用多种方式迭代集合(collection),具体取决于集合的类型和使用的需求。以下是几种常见的迭代集合的方式:

  • 使用 foreach 循环:

    • foreach 循环是最常用的迭代集合的方式,适用于实现了 IEnumerableIEnumerable<T> 接口的集合类型。它提供了一种简单且直观的方式来遍历集合中的每个元素。

以下是使用 foreach 循环迭代集合的示例代码:

List<int> numbers = new List<int> { 1, 2, 3, 4, 5 };

foreach (var number in numbers)
{
Console.WriteLine(number);
}
  • 使用迭代器(iterator):

    • 迭代器是一种自定义的迭代方式,通过实现 IEnumerableIEnumerable<T> 接口以及 IEnumeratorIEnumerator<T> 接口来定义集合的迭代逻辑。

以下是使用迭代器迭代集合的示例代码:

public class MyCollection<T> : IEnumerable<T>
{
    private List<T> items = new List<T>();

    public void Add(T item)
    {
        items.Add(item);
    }

    public IEnumerator<T> GetEnumerator()
    {
        return items.GetEnumerator();
    }

    IEnumerator IEnumerable.GetEnumerator()
    {
        return GetEnumerator();
    }
}

// 使用迭代器遍历自定义集合
MyCollection<int> collection = new MyCollection<int>();
collection.Add(1);
collection.Add(2);
collection.Add(3);

foreach (var item in collection)
{
    Console.WriteLine(item);
}
  • 使用索引访问:

    • 对于实现了索引器(indexer)的集合类型(如数组),可以使用索引来访问集合中的元素。通过循环遍历索引,可以逐个访问集合中的元素。

以下是使用索引访问迭代集合的示例代码:

int[] numbers = { 1, 2, 3, 4, 5 };

for (int i = 0; i < numbers.Length; i++)
{
Console.WriteLine(numbers[i]);
}
  • 使用 LINQ 查询:

    • 使用 LINQ(Language-Integrated Query)查询语法可以对集合进行查询和筛选,并以迭代的方式访问查询结果。

以下是使用 LINQ 查询迭代集合的示例代码:

List<int> numbers = new List<int> { 1, 2, 3, 4, 5 };

var evenNumbers = numbers.Where(n => n % 2 == 0);

foreach (var number in evenNumbers)
{
    Console.WriteLine(number);
}

9. 数组池

在 C# 中,数组池(Array Pool)是一种内存管理机制,用于重复使用数组以减少垃圾回收的频率,提高性能。通过使用数组池,可以减少频繁创建和销毁数组对象的开销,特别适用于需要频繁分配和释放大量相同大小的数组的情况。

数组池的主要目的是减少频繁创建和销毁数组对象的开销,特别适用于需要频繁分配和释放大量相同大小的数组的场景,例如网络编程、缓冲区管理和其他需要高性能的应用。

使用数组池的主要步骤如下:

  • 从数组池中获取数组:使用 ArrayPool<T>.Shared.Rent(int length) 方法从数组池中获取一个长度为 length 的数组。如果池中没有可用的适当大小的数组,将会创建一个新数组。

  • 使用数组:将数据存储在获取的数组中,执行相关的操作。

  • 将数组归还给数组池:使用 ArrayPool<T>.Shared.Return(T[] array) 方法将数组归还给数组池。归还的数组可以被后续的请求重复使用。

以下是使用数组池的示例代码:

using System;
using System.Buffers;

public class Program
{
    public static void Main()
    {
        // 从数组池中获取一个长度为 10 的 int 数组
        int[] array = ArrayPool<int>.Shared.Rent(10);

        // 在数组中存储一些数据
        for (int i = 0; i < array.Length; i++)
        {
            array[i] = i;
        }

        // 使用数组中的数据
        for (int i = 0; i < array.Length; i++)
        {
            Console.WriteLine(array[i]);
        }

        // 将数组归还给数组池
        ArrayPool<int>.Shared.Return(array);
    }
}

在上述示例中,我们使用 ArrayPool<int>.Shared 来获取和归还数组。首先,我们从数组池中获取一个长度为 10 的 int 数组。然后,我们在数组中存储了一些数据,并使用循环遍历和输出数组中的数据。最后,我们将数组归还给数组池,以便后续的请求可以重复使用该数组。

需要注意的是,在使用数组池时需要谨慎管理数组的生命周期,确保在不再需要数组时及时归还给数组池。如果忘记归还数组,可能会导致资源泄漏和潜在的内存问题。另外,数组池不适用于需要长时间保留数组的情况,因为长时间持有数组可能会影响垃圾回收的性能。

9.1 创建数组池

通过调用静态 Create 方法,可以创建 ArrayPool。为了提高效率,数组池在多个桶中为大小类似的数组管理内存。使用 Create 方法,可以在需要一个桶之前,在另一个桶中定义最大的数组长度和数组的数量:

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

maxArrayLength 的默认值是 1024 * 1024 字节,maxArraysPerBucket 的默认值是50。数组池使用多个桶,以便在使用多个数组时更快地访问数组。只要还没有到达数组的最大数量,大小类似的数组就尽可能保存在同一个桶中。

还可以通过访问 ArrayPool 类的共享属性,来使用预定义的共享池:

ArrayPool<int> sharedPool = ArrayPool<int>.Shared;

9.2 从池中租用内存

调用 Rent 方法可以请求池中的内存。Rent 方法接受应请求的最小数组长度。如果池中已经有内存,则返回该内存。如果它不可用,就给池分配内存,然后返回。

static void UseSharedPool()
{
    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 receive {arr.Length}");
    }
}

Rent 方法返回一个数组,其中至少包含所请求的元素个数。返回的数组可能有更多的可用内存。共享池中至少有16个元素。托管数组的元素计数总是重复的——例如,16、32、64、128、256、512、1024、2048、4096、8192个元素等。

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

9.3 将内存返回给池

不在需要数组时,可以将其返回到池中。数组返回后,可以稍后再用另一个 Rent 来重用它。

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

当需要存储多个相同类型的元素时,C# 数组是一种非常常用的数据结构。数组是一种固定长度的数据结构,它由连续的内存单元组成,每个内存单元存储一个元素。

#知识点##技术分享##学习笔记##学习##CSharp#
C# 知识库 文章被收录于专栏

C#知识库用于学习和复习C#知识~

全部评论

相关推荐

ArisRobert:统一解释一下,第4点的意思是,公司按需通知员工,没被通知到的员工是没法去上班的,所以只要没被通知到,就自动离职。就是一种比较抽象的裁员。
点赞 评论 收藏
分享
2 3 评论
分享
牛客网
牛客企业服务