CLR via C#, 4th -- 【基本类型】 -- 第16章数组

所有数组类型都隐式地从System.Array抽象类派生,后者又派生自System.Object,这意味着数组始终是引用类型,是在托管堆上分配的。在应用程序的变量或字段中,包含的是对数组的引用,而不是包含数组本身的元素。

Int32[] myIntegers;          // Declares a reference to an array  
myIntegers = new Int32[100]; // Creates an array of 100 Int32s

第一行代码声明myIntegers变量,它能指向包含Int32值的一维数组。myIntegers刚开始被设为null,因为当时还没有分配数组。第二行代码分配了含有100个Int32值的数组,所有Int32都被初始化为0。

为了符合“公共语言规范”(Common Language Specification,CLS)的要求,所有数组都必须是0基数组(即最小索引为0)。

// Create a two­dimensional array of Doubles.  
Double[,] myDoubles = new Double[10, 20];  
 
// Create a three­dimensional array of String references.  
String[,,] myStrings = new String[5, 3, 10];

CLR还支持交错数组(jagged array),即数组构成的数组。0基一维交错数组的性能和普通向量一样好。不过,访问交错数组的元素意味着必须进行两次或更多次数组访问。

// Create a single ­dimensional array of Point arrays.  
Point[][] myPolygons = new Point[3][];  
 
// myPolygons[0] refers to an array of 10 Point instances.  
myPolygons[0] = new Point[10];   
 
// myPolygons[1] refers to an array of 20 Point instances.  
myPolygons[1] = new Point[20];   
 
// myPolygons[2] refers to an array of 30 Point instances.  
myPolygons[2] = new Point[30];   
 
// Display the Points in the first polygon.  
for (Int32 x = 0; x < myPolygons[0].Length; x++)   
   Console.WriteLine(myPolygons[0][x]);

CLR会验证数组索引的有效性。换句话说,不能创建含有100个元素的数组(索引编号0到99),然后试图访问索引为-5或100的元素。这样做会导致System.IndexOutOrRangeException异常。

16.1 初始化数组元素

String[] names = new String[] { "Aidan", "Grant" };

大括号中的以逗号分隔的数据项称为数组初始化器(array initializer)

可利用C#的隐式类型的数组功能让编译器推断数组元素的类型。注意,代码允许在new和[]之间不指定类型。

// Using C#’s implicitly typed local variable and implicitly typed array features: 
var names = new[] { "Aidan", "Grant", null };

C#团队认为,隐式对数组元素进行装箱是一个代价高昂的操作,所以要在编译时报错。

// Using C#’s implicitly typed local variable & implicitly typed array features: (error)  
var names = new[] { "Aidan", "Grant", 123 };

作为初始化数组时的一个额外的语法奖励,还可以像下面这样写:

String[] names = { "Aidan", "Grant" };

赋值操作符(=)右侧只给出了一个初始化器,没有new,没有类型,没有[],这个语法可读性很好,但遗憾的是,C#编译器不允许在这种语法中使用隐式类型的局部变量:

// This is a local variable now (error) 
var names = { "Aidan", "Grant" };

最后讲一下“隐式类型的数组”如何与“匿名类型”和“隐式类型的局部变量”组合使用。

// Using C#’s implicitly typed local, implicitly typed array, and anonymous type features: 
var kids = new[] {new { Name="Aidan" }, new { Name="Grant" }}; 
 
// Sample usage (with another implicitly typed local variable):  
foreach (var kid in kids) 
   Console.WriteLine(kid.Name);

这个例子在一个数组初始化器中添加了两个用于定义数组元素的表达式。每个表达式都代表一个匿名类型(因为new操作符后没有提供类型名称)。由于两个匿名类型具有一致的结构(有一个String类型的Name字段),所以编译器知道这两个对象具有相同的类型(类型的同一性),然后,我使用了C#的“隐式类型的数组”功能(在new和[]之间不指定类型),让编译器推断数组本身的类型,构造这个数组对象,并初始化它内部的两个引用,指向匿名类型的两个实例。最后,将对这个数组对象的引用赋给kids局部变量,该变量的类型通过C#的“隐式类型的局部变量”功能来推断。

16.2 数组转型

对于元素为引用类型的数组,CLR允许将数组元素从一种类型转型另一种。成功转型要求数组维数相同,而且必须存在从元素源类型到目标类型的隐式或显式转换。CLR不允许将值类型元素的数组转型为其他任何类型。(Array.Copy方法执行的是浅拷贝。换言之,如果数组元素是引用类型,新数组将引用现有的对象。)

// Create a two­dimensional FileStream array.  
FileStream[,] fs2dim = new FileStream[5, 10];   
 
// Implicit cast to a two­dimensional Object array  
Object[,] o2dim = fs2dim;   
 
// Can't cast from two­dimensional array to one­dimensional array   
// Compiler error CS0030: Cannot convert type 'object[*,*]' to   
// 'System.IO.Stream[]'  
Stream[] s1dim = (Stream[]) o2dim;  
 
// Explicit cast to two ­dimensional Stream array  
Stream[,] s2dim = (Stream[,]) o2dim;  
 
// Explicit cast to two ­dimensional String array    
// Compiles but throws InvalidCastException at runtime 
String[,] st2dim = (String[,]) o2dim;   
 
// Create a one­dimensional Int32 array (value types).  
Int32[] i1dim = new Int32[5];  
 
// Can't cast from array of value types to anything else  
// Compiler error CS0030: Cannot convert type 'int[]' to 'object[]'   
Object[] o1dim = (Object[]) i1dim;  
 
// Create a new array, then use Array.Copy to coerce each element in the   
// source array to the desired type in the destination array.  
// The following code creates an array of references to boxed Int32s.  
Object[] ob1dim = new Object[i1dim.Length];  
Array.Copy(i1dim, ob1dim, i1dim.Length);

Array.Copy的作用不仅仅是将元素从一个数组复制到另一个。Copy方法还能正确处理内存的重叠区域,Copy方法还能在复制每个数组元素时进行必要的类型转换

  • 将值类型的元素装箱为引用类型的元素,比如将一个Int32[]复制到一个Object[]中。将
  • 引用类型的元素拆箱为值类型的元素,比如将一个Object[]复制到一个Int32[]中。
  • 加宽CLR基元值类型,比如将一个Int32[]的元素复制到一个Double[]中。
  • 在两个数组之间复制时,如果仅从数组类型证明不了两者的兼容性,比如从Object[]转型为1Formattable[],就根据需要对元素进行向下类型转换。如果Object[]中的每个对象都实现了IFormattable,Copy方法就能成功执行。
// Define a value type that implements an interface.   
internal struct MyValueType : IComparable {   
   public Int32 CompareTo(Object obj) {  
      ...    
   }  
}  
 
public static class Program {  
   public static void Main() {   
      // Create an array of 100 value types.  
      MyValueType[] src = new MyValueType[100];   
 
      // Create an array of IComparable references.  
      IComparable[] dest = new IComparable[src.Length];   
 
      // Initialize an array of IComparable elements to refer to boxed   
      // versions of elements in the source array.   
      Array.Copy(src, dest, src.Length);   
   }  
}

有时确实需要将数组从一种类型转换为另一种类型。这种功能称为数组协变性(array covariance),但在利用它时要清楚由此而来的性能损失。

String[] sa = new String[100];   
Object[] oa = sa;  // oa refers to an array of String elements   
oa[5] = "Jeff";    // Perf hit: CLR checks oa's element type for String; OK   
oa[3] = 5;         // Perf hit: CLR checks oa's element type for Int32; throws  
                   // ArrayTypeMismatchException

在上述代码中,oa变量被定义为Object[]类型,但实际引用的是一个String[],编译器允许代码将5放到数组元素中,因为5是Int32,而Int32派生自Object。虽然编译能通过,但CLR必须保证类型安全。对数组元素赋值时,它必须保证赋值的合法性。所以,CLR必须在运行时检查数组包含的是不是Int32元素。在本例中,答案是否定的,所以不允许赋值;CLR抛出ArrayTypeMismatchException异常。

如果只是需要将数组的某些元素复制到另一个数组,可选择System.Buffer的BlockCopy方法,它比Array的Copy方法快。但Buffer的BlockCopy方法只支持基元类型,不提供像Array的Copy方法那样的转型能力。
要将一个数组的元素可靠地复制到另一个数组,应该使用System.Array的ConstrainedCopy方法。该方法要么完成复制,要么抛出异常,总之不会破坏目标数组中的数据。这就允许ConstrainedCopy在约束执行区域(Constrained Execution Region,CER)中执行。为了提供这种保证,ConstrainedCopy要求源数组的元素类型要么与目标数组的元素类型相同,要么派生自目标数组的元素类型。另外,它不执行任何装箱、拆箱或向下类型转换。

16.3 所有数组都隐式派生自System.Array

SystemArray定义了许多有用的实例方法和属性,比如Clone,CopyTo,GetLength,GetLongLength,GetLowerBound,GetUpperBound,Length,Rank等。
System.Array类型还公开了很多有用的、用于数组处理的静态方法。这些方法均获取一个数组引用作为参数。一些有用的静态方法包括:AsReadOnly,BinarySearch,Clear,ConstrainedCopy,ConvertAll,Copy,Exists,Find,FindAll,FindIndex,FindLast,FindLastIndex,ForEach,IndexOf,LastIndexOf,Resize,Reverse,Sort和TrueForAll.

16.4 所有数组都隐式实现IEnumerable,ICollection和IList

由于涉及多维数组和非0基数组的问题,CLR团队不希望System.Array实现IEnumerable<T>,ICollection<T>和IList<T)
创建一维0基数组类型时,CLR自动使数组类型实现IEnumerable<T>,ICollection<T>和List<T>(T是数组元素的类型)。同时,还为数组类型的所有基类型实现这三个接口,只要它们是引用类型。

Object  
   Array (non ­generic IEnumerable, ICollection, IList)   
      Object[]           (IEnumerable, ICollection, IList of Object)  
         String[]        (IEnumerable, ICollection, IList of String)  
         Stream[]        (IEnumerable, ICollection, IList of Stream)  
            FileStream[] (IEnumerable, ICollection, IList of FileStream)   
         .   
         .      (other arrays of reference types)  
         .
FileStream[] fsArray;

那么当CLR创建FileStream[]类型时,会自动为这个类型实现1Enumerable<FileStream>,ICollection<FileStream>和IList<FileStream>接口。此外,FileStreaml1类型还会为基类型实现接口:IEnumerable<Stream>,IEnumerable<Object>,ICollection<Stream>,ICollection<Object>,IList<Stream>和IList<Object。由于所有这些接口都由CLR自动实现,所以在存在这些接口的任何地方都可以使用fsArray变量。例如,可将fsArray变量传给具有以下任何一种原型的方法

void M1(IList<FileStream> fsList) { … }  
void M2(ICollection<Stream> sCollection) { … }  
void M3(IEnumerable<Object> oEnumerable) { … }

如果数组包含值类型的元素,数组类型不会为元素的基类型实现接口。

DateTime[] dtArray; // An array of value types

那么DateTimell类型只会实现1Enumerable<DateTime>,ICollection<DateTime>和IList<DateTime>接口,不会为DateTime的基类型(包括System.ValueType和System.Object)实现这些泛型接口。

16.5 数组的传递和返回

数组作为实参传给方法时,实际传递的是对该数组的引用。因此,被调用的方法能修改数组中的元素。如果不想被修改,必须生成数组的拷贝并将拷贝传给方法。注意,Array.Copy方法执行的是浅拷贝。换言之,如果数组元素是引用类型,新数组将引用现有的对象。
类似地,有的方法会返回对数组的引用。如果方法构造并初始化数组,返回数组引用是没有问题的。但假如方法返回的是对字段所维护的一个内部数组的引用,就必须决定是否想让该方法的调用者直接访问这个数组及其元素。如果是,就可以返回数组引用。但更常见的情况是,你并不希望方法的调用者获得这个访问权限。所以,方法应该构造一个新数组,并调用Array.Copy返回对新数组的引用。再次提醒,Array.Copy执行的是对原始数组的浅拷贝。
如果定义返回数组引用的方法,而且数组中不包含元素,那么方法既可以返回null,也可以返回对包含零个元素的一个数组的引用。实现这种方法时,Microsoft强烈建议让它返回后者,因为这样能简化调用该方法时需要写的代码。(因为不需要执行null值检测。)

// This code is easier to write and understand.   
Appointment[] appointments = GetAppointmentsForToday();   
for (Int32 a = 0; a < appointments.Length; a++) {  
   ...    
}
// This code is harder to write and understand.   
Appointment[] appointments = GetAppointmentsForToday();   
if (appointments != null) {   
   for (Int32 a = 0, a < appointments.Length; a++) {   
      // Do something with appointments[a]   
   }  
}

将方法设计为返回对含有0个元素的一个数组的引用,而不是返回null,该方法的调用者就能更轻松地使用该方法。顺便提一句,对字段也应如此。如果类型中有一个字段是数组引用,应考虑让这个字段始终引用数组,即使数组中不包含任何元素。

16.6 创建下限非零的数组

前面提到过,能创建和操作下限非0的数组。可以调用数组的静态CreatInstance方法来动态创建自己的数组。该方法有若干个重载版本,允许指定数组元素的类型、数组的维数、每一维的下限和每一维的元素数目。
如果数组维数是2或2以上,就可以把Createlnstance返回的引用转型为一个ElementType[]变量(ElementType要替换为类型名称),以简化对数组中的元素的访问。如果只有一维,C#要求必须使用该Array的GetValue和SetValue方法访问数组元素。

using System;  
 
public static class DynamicArrays {   
   public static void Main() {   
      // I want a two ­dimensional array [2005..2009][1..4].   
      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");  
      Int32 firstYear    = quarterlyRevenue.GetLowerBound(0);  
      Int32 lastYear     = quarterlyRevenue.GetUpperBound(0);  
      Int32 firstQuarter = quarterlyRevenue.GetLowerBound(1);  
      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();  
      }   
   }  
}

16.7 数组的内部工作原理

CLR内部实际支持两种不同的数组。

  • 下限为0的一维数组。这些数组有时称为SZ(single-dimensional,zero-based,一维0基)数组或向量(vector).
  • 下限未知的一维或多维数组。
using System;  
 
public sealed class Program {  
      public static void Main() {  
      Array a;  
 
      // Create a 1­dim, 0 ­based array, with no elements in it   
      a = new String[0];  
      Console.WriteLine(a.GetType());   // "System.String[]"   
 
      // Create a 1­dim, 0 ­based array, with no elements in it   
      a = Array.CreateInstance(typeof(String),    
         new Int32[] { 0 }, new Int32[] { 0 });   
      Console.WriteLine(a.GetType());   // "System.String[]"   
 
      // Create a 1­dim, 1 ­based array, with no elements in it   
      a = Array.CreateInstance(typeof(String),    
         new Int32[] { 0 }, new Int32[] { 1 });   
      Console.WriteLine(a.GetType());   // "System.String[*]"  <­­ INTERESTING!  
 
      Console.WriteLine();  
 
      // Create a 2­dim, 0 ­based array, with no elements in it   
      a = new String[0, 0];   
      Console.WriteLine(a.GetType());   // "System.String[,]"  
 
      // Create a 2­dim, 0 ­based array, with no elements in it   
      a = Array.CreateInstance(typeof(String),    
         new Int32[] { 0, 0 }, new Int32[] { 0, 0 });  
      Console.WriteLine(a.GetType());   // "System.String[,]"  
 
      // Create a 2­dim, 1 ­based array, with no elements in it   
      a = Array.CreateInstance(typeof(String),    
         new Int32[] { 0, 0 }, new Int32[] { 1, 1 });  
      Console.WriteLine(a.GetType());   // "System.String[,]"  
   }  
}

每个Console.WriteLine语句后都有一条指出其输出的注释。

访问一维0基数组的元素比访问非0基一维或多维数组的元素稍快。这是多方面的原因造成的。首先,有一些特殊IL指令,比如newarr,Idelem,Idelema,Idlen和stelem,用于处理一维0基数组,这些特殊IL指令会导致JIT编译器生成优化代码。其次,一般情况下,JIT编译器能将索引范围检查代码从循环中拿出,导致它只执行一次。

using System;  
 
public static class Program {  
   public static void Main() {   
      Int32[] a = new Int32[5];  
      for(Int32 index = 0; index < a.Length; index++) {   
         // Do something with a[index]  
      }   
   }  
}

对于以上代码,首先注意在for循环的测试表达式中对数组的Length属性的调用。由于Length是属性,所以查询长度实际是方法调用。但JIT编译器知道Length是Array类的属性,所以在生成的代码中,实际只调用该属性一次,结果存储到一个临时变量中。每次循环迭代检查的都是这个临时变量。这就加快了JT编译的代码的速度。但有的开发人员低估了JIT编译器的“本事”,试图自己写一些“高明”的代码来“帮助”JIT编译器。任何自作聪明的尝试几乎肯定会对性能造成负面影响,还会使代码更难阅读,妨碍可维护性。在上述代码中,最好保持对数组Length属性的调用,而不要自己用什么局部变量来缓存它的值。
其次要注意,JIT编译器知道for循环要访问0到Length-1的数组元素。所以,JIT编译器会生成代码,在运行时测试所有数组元素的访问都在数组有效范围内。具体地说,JIT编译器公生成代码来检查是否(0 >= a.GetLowerBound(0))&&(Length-1)<a.GetUpperBound(0))。这个检查在循环之前发生。如果在数组有效范围内,JIT编译器不会在循环内部生成代码验证每一次数组访问都在有效范围内。这样一来,循环内部的数组访问变得非常快。

C#和CLR还允许使用unsafe(不可验证)代码访问数组。这种技术实际能在访问数组时关闭索引上下限检查。这种不安全的数组访问技术适合以下元素类型的数组:SByte,Byte, Int16, UInt16, Int32, UInt32, Int64, UInt64, Char, Single, Double, Decimal, Boolean枚举类型或者字段为上述任何类型的值类型结构。

using System;  
using System.Diagnostics; 
 
public static class Program { 
   private const Int32 c_numElements = 10000; 
 
   public static void Main() { 
      // Declare a two­dimensional array 
      Int32[,] a2Dim = new Int32[c_numElements, c_numElements];  
 
      // Declare a two­dimensional array as a jagged array (a vector of vectors) 
      Int32[][] aJagged = new Int32[c_numElements][];  
      for (Int32 x = 0; x < c_numElements; x++) 
         aJagged[x] = new Int32[c_numElements]; 
 
      // 1: Access all elements of the array using the usual, safe technique  
      Safe2DimArrayAccess(a2Dim);  
 
      // 2: Access all elements of the array using the jagged array technique 
      SafeJaggedArrayAccess(aJagged); 
 
      // 3: Access all elements of the array using the unsafe technique  
      Unsafe2DimArrayAccess(a2Dim); 
   } 
 
   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; 
   } 
    
   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; 
   } 
}

16.8 不安全的数组访问和固定大小的数组

不安全的数组访问非常强大,因为它允许访问以下元素。

  • 堆上的托管数组对象中的元素(上一节对此进行了演示)。
  • 非托管堆上的数组中的元素。第14章的SecureString示例演示了如何调用System.Runtime.InteropServices.Marshal类的SecureStringToCoTaskMemUnicode方法来返回一个数组,并对这个数组进行不安全的数组访问。
  • 线程栈上的数组中的元素。

如果性能是首要目标,请避免在堆上分配托管的数组对象。相反,在线程栈上分配数组。这是通过C#的stackalloc语句来完成的(它在很大程度上类似于C的alloca函数)。stackalloc语句只能创建一维0基、由值类型元素构成的数组,而且值类型绝对不能包含任何引用类型的字段。使用这个功能要求为C#编译器指定/unsafe开关。

using System;  
 
public static class Program {  
   public static void Main() {   
      StackallocDemo();  
      InlineArrayDemo();  
   }  
 
   private static void StackallocDemo() {  
      unsafe {  
         const Int32 width = 20;   
         Char* pc = stackalloc Char[width]; // Allocates array on stack  
        
         String s = "Jeffrey Richter";      // 15 characters   
           
         for (Int32 index = 0; index < width; index++) {  
            pc[width  ­ index ­ 1] =   
               (index < s.Length) ? s[index] : '.';  
         }   
 
         // The following line displays ".....rethciR yerffeJ"   
         Console.WriteLine(new String(pc, 0, width));   
      }   
   }  
 
   private static void InlineArrayDemo() {   
      unsafe {  
         CharArray ca;                 // Allocates array on stack  
         Int32 widthInBytes = sizeof(CharArray);  
         Int32 width = widthInBytes / 2;   
 
         String s = "Jeffrey Richter"; // 15 characters   
 
         for (Int32 index = 0; index < width; index++) {  
            ca.Characters[width ­ index ­ 1] =   
               (index < s.Length) ? s[index] : '.';  
         }   
 
         // The following line displays ".....rethciR yerffeJ"   
         Console.WriteLine(new String(ca.Characters, 0, width));  
      }   
   }  
}  

internal unsafe struct CharArray {  
   // This array is embedded inline inside the structure  
   public fixed Char Characters[20];  
}

通常,由于数组是引用类型,所以结构中定义的数组字段实际只是指向数组的指针或引用:数组本身在结构的内存的外部。在结构中嵌入数组需满足以下几个条件。

  • 类型必须是结构(值类型):不能在类(引用类型)中嵌入数组。
  • 字段或其定义结构必须用unsafe关键字标记。
  • 数组字段必须用fixed关键字标记。
  • 数组必须是一维0基数组。
  • 数组的元素类型必须是以下类型之一:Boolean,Char,SByte,Byte,Int16,Int32,UInt16,UInt32,Int64,UInt64,Single或Double.

要和非托管代码进行互操作,而且非托管数据结构也有一个内联数组,就特别适合使用内联的数组。

posted @ 2019-12-15 23:36  FH1004322  阅读(218)  评论(0)    收藏  举报