本章内容:
初始化数组元素
● 数组转型
● 所有数组都隐式派生自 System.Array
● 所有数组都隐式实现IEnumerable,ICollection 和 IList
● 数组的传递和返回
● 创建下限非零的数组
● 数组的内部工作原理
● 不安全的数组访问和固定大小的数组
数组是允许将多个数据项作为集合来处理的机制。CLR 支持一维、多维和交错数组(即数组)构成的数组)。所有数组类型都隐式地从 System.Array 抽象类派生,后者又派生自System.Object。这意味着数组始终是引用类型,是在托管堆上分配的。在应用程序的变量或字段中,包含的是对数组的引用,而不是包含数组本身的元素。下面的代码更清楚地说
明了这一点:
Int32[] myIntegers; // 声明一个数组引用
myIntegers = new Int32[100];// 创建含有 100 个 Int32 的数组
第一行代码声明 myIntegers 变量,它能指向包含 Int32 值的一维数组。myIntegers 刚开始被设为 null,因为当时还没有分配数组。第二行代码分配了含有 100 个 Int32 值的数组,所有 Int32 都被初始化为 0。由于数组是引用类型,所以会在托管堆上分配容纳 100 个未装箱 Int32 所需的内存块。实际上,除了数组元素,数组对象占据的内存块还包含一个类型对象指针、一个同步块索引和一些额外的成员(这是额外的成员称为overhead字段或者说“开销字段”)。该数组的内存块地址被返回并保存到myIntegers 变量中。
多维数组
// 创建一个三维数组,由String引用构成
String[,,] myStrings = new String[5, 3, 10];
交错数组(jagged array)
数组构成的数组。
// 创建由多个Point数组构成的一维数组
Point[][] myPolygons = new Point[3][];
// 下面语句等效 String[] names = new String[] {"Aidan", "Grant"}; String[] names = {"Aidan", "Grant"}; var names = new String[] {"Aidan", "Grant"}; var names = new[] {"Aidan", "Grant"}; //var names = {"Aidan", "Grant"}; // 错误
// 使用C#的隐式类型的局部变量、隐式类型的数组和匿名类型功能 var kids = new [] {new { Name="Aidan" }, new { Name="Grant" }}; foreach (var kid in kids) Console.WriteLine(kid.Name);
数组转型
// 创建二维FileStream数组 FileStream[,] fs2dim = new FileStream[5, 10];// 隐式转型为二维Object数组 Object[,] o2dim = fs2dim;// 显式转型为二维Stream数组 Stream[,] s2dim = (Stream[,]) o2dim;// 创建一维Int32数组(元素是值类型) Int32 ildim = new Int32[5]; //错误: 不能将值类型的数组转型为其他任何类型 //Object[] oldim = (Object[]) ildim;// 创建一个新数组,使用Array.Copy将源数组中的每个元素 // 转型为目标数组中的元素,并把它们复制过去。 // 下面的代码创建元素为引用类型的数组, // 每个元素都是对已装箱Int32的引用 // Array.Copy是浅复制 Object[] obldim = new Object[ildim.Length]; Array.Copy(ildim, obldim, ildim.Length);
Array.Copy 的作用不仅仅是将元素从一个数组复制到另一个。Copy 方法还能正确处理内存的重叠区域,就像C的 memmove 函数一样。有趣的是,C的 memcpy 函数反而不能正确处理重叠的内存区域。Copy 方法还能在复制每个数组元素时进行必要的类型转换,具体如下所述:
● 将值类型的元素装箱为引用类型的元素,比如将一个Int32[]复制到一个Object[]中。
● 将引用类型的元素拆箱为值类型的元素,比如将一个Object[]复制到一个Int32[]中。
● 加宽 CLR 基元值类型,比如将一个Int32的元素复制到一个Double[]中。
● 在两个数组之间复制时,如果仅从数组类型证明不了两者的兼容性,比如从 Object[]转型为 IFormattable[],就根据需要对元素进行向下类型转换。如果Object[]中的每个对象都实现了 IFormattable,Copy 方法就能成功执行。
// 定义实现了一个接口的值类型 internal struct MyValueType : IComparable { public Int32 CompareTo(Object obj) { } } public static class Program { public static void Main() { MyValueType[] src = new MyValueType[100]; IComparable[] dest = new IComparable[src.Length];//初始化IComparable数组中的元素, //使它们引用源数组元素的已装箱版本 Array.Copy(src, dest, src.Length); } }
有时确实需要将数组从一种类型转换为另一种类型。这种功能称为数组协变性(array covariance)。但在利用它时要清楚由此而来的性能损失。
“按位兼容”因为英文原文是 bitwise-compatible,所以人们发明了 blittable 一词来表示这种类型。这 种类型在托管和非托管内存中具有相同的表示。一部分基元类型是 blittable 类型。如果一维数组包含 的是 blittable 类型,该数组也是 blittable 类型。另外,格式化的值类型如果只包含 blittable 类型,该值 类型也是 blittable 类型。欲知评情。请在MSDN文档中搜索"可直接复制到本机结构中的类型和非直接复制到本机结构中的类型"这一主题。——译注
所有数组都隐式派生自System.Array
所有数组都隐式实现IEnumerable, ICollection和IList
创建一维0基数组类型时,CLR自动使数组类型实现IEnumerable<T>, ICollection<T>和IList<T>(T是数组元素类型)。同时,还为数组类型的所有基类型实现这三个接口,只要它们是引用类型。
注意,如果数组包含值类型的元素,数组类型不会为元素的基类型实现接口。例如,如果执行以下代码:
DateTime[] dtArray; // 一个值类型的数组
那么,DateTime[]类型只会实现IEnumerable<DateTime>, ICollection<DateTime>和IList<DateTime>接口,不会为DateTime的基类型(包括System.ValueType和System.Object)实现这些泛型接口。这是因为值类型的数组在内存中的布局与引用类型的数组不同。
创建下限非零的数组
using System; namespace ConsoleApp32 { class Program { static void Main(string[] args) { Int32[] lowerBounds = { 2005, 1 }; Int32[] lengths = { 5, 4}; Decimal[,] quarterlyRevenue = (Decimal[,]) Array.CreateInstance(typeof(Decimal), lengths, lowerBounds); Console.WriteLine("{0,4} {1,9} {2,9} {3,9} {4,9}", "Year", "Q1", "Q2", "Q3", "Q4"); //获取第1维第1个元素的索引 Int32 firstYear = quarterlyRevenue.GetLowerBound(0); //获取第1维最后一个元素的索引 Int32 lastYear = quarterlyRevenue.GetUpperBound(0); //获取第2维第1个元素的索引 Int32 firstQuarter = quarterlyRevenue.GetLowerBound(1); //获取第2维最后一个元素的索引 Int32 lastQuarter = quarterlyRevenue.GetUpperBound(1); for (Int32 year = firstYear; year <= lastYear; year++) { Console.Write(year + " "); for (Int32 quarter = firstQuarter; quarter <= lastQuarter; quarter++) { Console.Write("{0,9:C} ", quarterlyRevenue[year, quarter]); } Console.WriteLine(); } Console.ReadLine(); } } }
访问一维0基数组的元素比访问非0基一维或多维数组的元素稍快。
以下代码演示了访问二维数组的三种方式(安全、交错和不安全)
using System; using System.Diagnostics; namespace ConsoleApp32 { class Program { private const Int32 c_numElements = 10000; static void Main(string[] args) { // 声明二维数组 Int32[,] a2Dim = new int[c_numElements, c_numElements]; // 将二维数组声明为交错数组(向量构成的向量) Int32[][] aJagged = new Int32[c_numElements][]; for (Int32 x = 0; x < c_numElements; x++) aJagged[x] = new Int32[c_numElements]; // 1: 用普通的安全技术访问数组中的元素 Safe2DimArrayAccess(a2Dim); // 2: 用交错数组技术访问数组中的元素 SafeJaggedArrayAccess(aJagged); // 3: 用unsafe技术访问数组中的所有无线 Unsafe2DimArrayAccess(a2Dim); Console.ReadLine(); } private static Int32 Safe2DimArrayAccess(Int32[,] a) { Int32 sum = 0; for (Int32 x = 0; x < c_numElements; x++) { for (Int32 y = 0; y < c_numElements; y++) sum += a[x, y]; } return sum; } private static Int32 SafeJaggedArrayAccess(Int32[][] a) { Int32 sum = 0; for (Int32 x = 0; x < c_numElements; x++) { for (Int32 y = 0; y < c_numElements; y++) sum += a[x][y]; } return sum; } //C#编译器需指定/unsafe开关 ([项目属性页]->[生成]->允许不安全代码) //访问标记了unsafe修饰符,这是fixed语句所必须的。 private static unsafe Int32 Unsafe2DimArrayAccess(Int32[,] a) { Int32 sum = 0; fixed (Int32* pi = a) { for (Int32 x = 0; x < c_numElements; x++) { Int32 baseOfDim = x * c_numElements; for (Int32 y = 0; y < c_numElements; y++) sum += pi[baseOfDim + y]; } } return sum; } } }
Unsafte2DimArrayAccess方法标记了 unsafe 修饰符,这是使用 C#的 fixed 语句所必须的。编译这段代码要求在运行 C#编译器时指定/unsafe 开关,或者在 Microsoft Visual Studio 的项目属性页的“生成”选项卡中勾选“允许不安全代码”。
写代码时,不安全数据访问技术有时或许是你的最佳选择,但要注意该技术的三处不足。
● 相较于其他技术,处理数组元素的代码更复杂,不容易读和写,因为要使用 C# fixed 语句,要执行内存地址计算。
● 计算过程中出错,可能访问到不属于数组的内存。这会造成计算错误,损坏内存数据,破坏类型安全性,并可能造成安全漏洞。
● 因为这些潜在的问题,CLR 禁止在降低了安全级别的环境(如 Microsoft Silverlight)中运行不安全代码。
不安全的数组访问和固定大小的数组
不安全的数组访问非常强大,因为它允许访问以下元素。
● 堆上的托管数组对象中的元素(上一节对此进行了演示)。
● 非托管堆上的数组中的元素。SecureString 示例演示了如何调用 System.Runtime.InteropServices.Marshal 类的 SecureStringToCoTaskMemUnicode 方法来返回一个数组,并对这个数组进行不安全的数组访问。
● 线程栈上的数组中的元素。
如果性能是首要目标,请避免在堆上分配托管的数组对象。相反,在线程栈上分配数组。这是通过C#的 stackalloc 语句来完成的(它在很大程度上类似于C的 alloca 函数), stackalloc语句只能创建一维 0 基、由值类型元素构成的数组,而且值类型绝对不能包含任何引用类型的字段。实际上,应该把它的作用看成是分配一个内存块,这个内存块可以使用不安全的指针来操纵。所以,不能将这个内存缓冲区的地址传给大部分 FCL 方法。当然,栈上分配的内存(数组)会在方法返回时自动释放;这对增强性能起了一定作用。使用这个功能要求为 C#编译器指定/unsafe 开关。
以下代码演示了如何使用C# stackalloc 语句
using System; namespace ConsoleApp32 { class Program { static void Main(string[] args) { StackallocDemo(); InlineArrayDemo(); Console.ReadLine(); } private static void StackallocDemo() { unsafe { const Int32 width = 20; Char* pc = stackalloc Char[width]; // 在栈上分配数组 String s = "Jeffrey Richter"; // 15个字符 for (Int32 index = 0; index < width; index++) { pc[width - index - 1] = (index < s.Length) ? s[index] : '.'; } // 下面这行代码显示".....rethciR yerffeJ" Console.WriteLine(new String(pc, 0, width)); } } private static void InlineArrayDemo() { unsafe { CharArray ca; // 在栈上分配数组 Int32 widthInBytes = sizeof(CharArray); Int32 width = widthInBytes / 2; String s = "Jeffrey Richter"; // 15个字符 for (Int32 index = 0; index < width; index++) { ca.Characters[width - index - 1] = (index < s.Length) ? s[index] : '.'; } // 下面这行代码显示".....rethciR yerffeJ" Console.WriteLine(new String(ca.Characters, 0, width)); } } } internal unsafe struct CharArray { // 这个数组内联(嵌入)到结构中 public fixed Char Characters[20]; } }
通常,由于数组是引用类型,所以结构中定义的数组字段实际只是指向数组的指针或引用;数组本身在结构的内存的外部。不过,也可像上述代码中的 CharArray结构那样,直接将数组嵌入结构。在结构中嵌入数组需满足以下几个条件。
● 类型必须是结构(值类型);不能在类(引用类型)中嵌入数组。
● 字段或其定义结构必须用 unsafe 关键字标记。
● 数组字段必须用 fixed 关键字标记。
● 数组必须是一维 0 基数组。
● 数组的元素类型必须是以下类型之一:Boolean,Char,SByte,Byte, Int16,Int32, UIntl6,UInt32,Int64,UInt64,Single 或 Double。
要和非托管代码进行互操作,而且非托管数据结构也有一个内联数组,就特别适合使用内联的数组。但内联数组也能用于其他地方。上述代码中的 InlineArrayDemo 方法提供了如何使用内联数组的一个例子。它执行和 StackallocDemo 方法一样的功能,只是用了不一样的方式。