C#集合:Array类

Array类是所有一维和多维数组的隐式基类,它是实现标准集合接口的最基本类型之一。Array类提供了类型统一性,所以所有的数组对象都能够访问同一套公共方法,而与它们的声明或实际的元素类型无关。
当使用C#语法声明数组时,CLR会将其隐式转换为Array类的子类,合成一个对应该数组维度和元素类型的伪类型。这个伪类型实现了类型化的泛型集合接口,例如IList
CLR也会特别处理数组类型的创建,它将数组类型分配到一块连续的内存空间中。这样数组的索引就非常高效了,但不允许在创建后修改数组的大小。
Array实现了泛型接口IList及其非泛型版本。但是IList是显式实现的,以保证Array的公共接口中不包含其中的一些方法,如Add和Remove。

数组可以包含值类型或引用类型的元素。值类型元素存储在数组中,所以一个有三个long整数(每一个8字节)的数组将会占用24个字节的连续内存空间。然而,引用类型在数组中只占用一个引用所需的空间(32位环境是4字节,64位环境则为8字节)。
下图说明了下面这个程序在内存中的作用效果:

StringBuilder[] builders = new StringBuilder [5];
builders [0] = new StringBuilder ("builder1");
builders [1] = new StringBuilder ("builder2");
builders [2] = new StringBuilder ("builder3");

long[] numbers = new long[3];
numbers[0] = 12345;
numbers[1] = 54321;

image

Array本身是一个类,因此无论数组中的元素是什么类型,数组(本身)总是引用类型。这意味着语句arrayB = arrayA的结果是两个变量引用同一数组。类似地,除非使用结构化相等比较器来比较数组中的每一个元素,否则两个不同的数组在相等比较中总是会失败:

object[] a1 = { "string", 123, true };
object[] a2 = { "string", 123, true };

Console.WriteLine (a1 == a2);                          // False
Console.WriteLine (a1.Equals (a2));                    // False

IStructuralEquatable se1 = a1;

Console.WriteLine (se1.Equals (a2, 
    StructuralComparisons.StructuralEqualityComparer));   // True

数组可以通过调用Clone方法进行复制,例如arrayB = arrayA.Clone()。但是,其结果是一个浅表副本,即表示数组本身的内存会被复制。如果数组中包含的是值类型的对象,那么这些值也会被复制。但如果包含的是引用类型的对象,那么只有引用会被复制(结果就是两个数组的元素都引用了相同的对象)。

StringBuilder[] builders2 = builders;
StringBuilder[] shallowClone = (StringBuilder[])builders.Clone();

image

如果要进行深度复制(即复制引用类型子对象),必须遍历整个数组,然后手动克隆每一个元素。相同的规则也适用于其他.NET集合类型。

创建和索引

创建和索引数组的最简单方法是使用语言构造:

int[] myArray = { 1, 2, 3 };
int first = myArray [0];
int last = myArray [myArray.Length - 1];

此外,可以通过调用Array.CreateInstance动态创建一个数组实例。该方法可以在运行时指定元素类型、维数,以及通过指定数组下界来实现非零开始的数组。
GetValueSetValue方法可用于访问动态创建的数组元素(访问普通数组元素亦可):

Array a = Array.CreateInstance (typeof(string), 2);
a.SetValue ("hi", 0);                             //  → a[0] = "hi";
a.SetValue ("there", 1);                          //  → a[1] = "there";
string s = (string) a.GetValue (0);               //  → s = a[0];

string[] cSharpArray = (string[]) a;
string s2 = cSharpArray [0];

动态创建的索引从零开始的数组可以转换为匹配的或类型兼容的C#数组。例如,如果Apple是Fruit的子类,那么Apple[]可以转换为Fruit[]。这就产生了一个问题,即为什么不使用object[]作为统一的数组类型而使用Array类?原因就是object[]既不兼容多维数组,也不兼容值类型数组以及不从零开始索引的数组。int[]数组不能够转换为object[]。因此,我们需要Array类实现彻底的类型统一。
GetValue和SetValue也支持编译器创建的数组,并且它们能够在方法中处理任意类型和任意维数的数组。对于多维数组,它们接受索引器数组参数:

public object GetValue(params int[] indices)
public void SetValue(object value, params int[] indices)

如下打印出任意维度数组第一个元素值:

static void WriteFirstValue(Array a)
{
    Console.Write(a.Rank + "维数组;");

    int[] indexers = new int[a.Rank];
    var obj = a.GetValue(indexers);

    Console.WriteLine("第一个值是: " + obj);
}
static void Main(string[] args)
{
    int[] oneD = { 1, 2, 3 };
    int[,] twoD = { { 5, 6 }, { 8, 9 } };

    WriteFirstValue(oneD);   // 1维数组;第一个值是:1  -- 下标为 0
    WriteFirstValue(twoD);   // 2维数组;第一个值是:5  -- 下标为 0,0

    Console.ReadKey();
}

枚举

数组可以通过foreach语句进行枚举:

int[] myArray = { 1,2,3 };
foreach (int val in myArray)
    Console.WriteLine(val);

也可以使用静态方法Array.ForEach进行枚举,例如:public static void ForEach<T> (T[] array, Action<T> action);
这种方法使用Action委托和此签名:public delegate void Action<T> (T obj);
下面使用Array.ForEach重写了第一个例子:

Array.ForEach(new[] { 1, 2, 3 }, Console.WriteLine);

长度和维度

Array提供了如下的方法和属性来查询长度和维数:

// 返回一个指定维度的长度(0表示一维数组)
public int GetLength(int dimension);
public long GetLongLength(int dimension);

// 返回数组所有维度的元素总数
public int Length { get; }
public long LongLength { get; }

// 处理非零起始的数组时是很有用的
public int GetLowerBound(int dimension);
public int GetUpperBound(int dimension);

// 返回数组的维度
public int Rank { get; } 

搜索

Array类提供了许多搜索一维数组元素的方法:

  • BinarySearch方法(二分查找):快速在排序数组中找到特定元素。
  • IndexOf/LastIndexOf方法:搜索未排序数组中的特定元素
  • Find/FindLast/FindIndex/FindLastIndex/FindAll/Exists/TrueForAll:搜索未排序数组中满足指定的Predicate<T>的一个或多个元素。

当指定的值未找到时,上述搜索方法都不会抛出异常。相反,如果一个元素未找到,那么这些方法会返回整数-1,而返回泛型类型的方法则返回该类型的默认值(例如0对应int,null对应string)。
二分搜索方法速度快,但是仅适用于已排序数组,而且要求元素能够比较顺序,而不仅仅是比较是否相等。
IndexOf和LastIndexOf方法会对数组执行简单的枚举,返回与给定值匹配的第一个(或者最后一个)元素的位置。
基于谓词(predicate)的搜索方法允许使用一个方法委托或者Lambda表达式来判断给定的元素是否匹配。谓词是一个简单的委托,它接受对象并返回true或false:

string[] names = {"tom","jack","jerry"};
string match = Array.Find(names,n=>n.Contains("a"));

排序

Array类有内置的关于Sort的内置排序算法。
最简单的用法:int[] numbers = { 3,2,1 }; Array.Sort(numbers);
接受一对数组的排序方法将基于第一个数组的元素的排序结果对两个数组的元素进行相应的调整。在下面的例子中,数字和其对应的单词最终都将按数字顺序进行排列:

numbers = new[]  { 3, 2, 1 };
string[] words = { "three", "two", "one" };
Array.Sort (numbers, words);

反转数组元素

可以将数组的全部或部分元素的顺序进行反转:

public static void Reverse(Array array);
public static void Reverse(Array array, int index, int length);

复制数组

Array提供了4个对数组进行浅表复制的方法:Clone、CopyTo、Copy和Constrained-Copy。
前两个是实例方法,后两个为静态方法。
Clone方法返回一个全新的(浅表复制的)数组。
CopyTo和Copy方法则复制数组中的若干连续元素。若复制多维数组,则需要将多维数组的索引映射为线性索引。例如,一个3×3数组的中间矩阵(position[1, 1])的索引可以用4表示,其计算方法是1×3+1。这些方法允许源与目标范围重叠而不会造成任何问题。
ConstrainedCopy执行一个原子操作:如果所有请求的元素无法成功复制(例如,由于类型错误),那么操作将会回滚。
Array还提供了一个AsReadOnly方法来包装数组以防止其中的元素被重新赋值。

转换和调整大小

Array.ConvertAll会创建并返回一个包含指定元素类型TOutput的新数组,它会调用Converter委托来复制每一个元素。Converter的定义如下:public delegate TOutput Converter<TInput,TOutput>(TInput input)
下面把一个浮点数组转换为一个整数数组:

float[] reals = { 1.3f, 1.5f, 1.8f };
int[] wholes = Array.ConvertAll (reals, r => Convert.ToInt32 (r)); // 1,2,2

Resize方法则会创建一个新的数组,并将所有元素复制到新数组中,再通过引用参数返回这个新数组。但是,其他对象中原始数组的所有引用都不会发生任何变化。

posted @ 2022-08-26 17:20  一纸年华  阅读(1588)  评论(0编辑  收藏  举报