数组是允许将多个数据项当作一个集合来处理的机制。CLR支持一维数组、多维数组和交错数据(即由数组构成的数组)。所有数组类型都隐式地从System.Array抽象类派生,后者又派生自System.Object。这意味着数组始终是引用类型,是在托管堆上分配的。在你应用程序的变量或字段中,包含的是对数组的引用,而不是包含数组本身的元素。下面的代码更清楚的说明了这一点:

Int32[] myIntegers; //声明一个数组引用
myIntegers = new int32[100] //创建含有100个Int32的数组

  在第一行代码中,myIntegers变量能指向一个一维数组(由Int32值构成)。myIntegers刚开始被设为null,因为当时还没有分配数组。第二行代码分配了含有100个Int32值的一个数组,所有Int32都被初始化为0。由于数组是引 用类型,所有托管堆上还包含一个未装箱Int32所需要的内存块。实际上,除了数组元素,数字对象占据的内存块还包含一个类型对象指针、一个同步块索引和一些额外的成员(overhead)。该数组的内存块地址被返回并保存到myIntegers变量中。

  C#也支持多维数组。下面演示了几个多维数组的例子:

// 创建一个二维数组,由Double值构成
Double[,] myDoubles = new Double[10,20];
// 创建一个三位数组,由String引用构成
String[,,] myStrings = new String[5,3,10]; 

  CLR还支持交错数组,即由数组构成的数组。下面例子演示了如何创建一个多边形数组,其中每一个多边形都由一个Point实例数组构成。

// 创建一个含有Point数组的一维数组
Point[][] myPolygons = new Point[3][];
// myPolygons[0]引用一个含有10个Point实例的数组
myPolygons[0] = new Point[10];
// myPolygons[1]引用一个含有20个Point实例的数组
myPolygons[1] = new Point[20];
// myPolygons[2]引用一个含有30个Point实例的数组
myPolygons[2] = new Point[30];
// 显示第一个多边形中的Point
for (Int32 x =0 ; x < myPolygons[0].Length; x++)
{Console.WriteLine(myPolygons[0][x]);
}

  注意:CLR会验证数组索引的有效性。换句话说,不能创建一个含有100个元素的数组(索引编号为0到99),又试图访问索引为-5或100的元素。

一、始化数组元素

  前面展示了如何创建一个数组对象,以及如何初始化数组中的元素。C#允许用一个语句来同时做两件事。例如:

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

  大括号中的以逗号分隔的数据成为数组初始化器。每个数据项都可以是一个任意复杂度的表达式;在多维数组的情况下,则可以是一个嵌套的数组初始化器。可利用C#的隐式类型的数组功能让编译器推断数组元素的类型。注意,下面这一行代码没有在new和[]之间指定类型:

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

  在上一行中,编译器检查数组中用于初始化数组元素的表达式的类型,并选择所有元素最接近的共同基类作为数组的类型。在本例中,编译器发现两个String和一个null。由于null可隐式转型成为任意引用类型(包括String),所以编译器推断应该创建和初始化一个由String引用构成的数组。

给定一下代码:

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

  编译器是会报错的,虽然String类和Int32共同基类是Object,意味着编译器不得不创建Object引用了一个数组,然后对123进行装箱,并让最后一个数组元素引用已装箱的,值为123的一个Int32。但C#团队认为,隐式对数组 元素进行装箱是一个代价昂贵的操作,所以要做编译时报错。

  在C#中还可以这样初始化数组:

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

  但是C#不允许在这种语法中使用隐式类型的局部变量:

var names = { "Aidan", "Grant" };

  最后来看下"隐式类型的数组"如何与"匿名类型"和"隐式类型的局部变量"组合使用。

// 使用C#的隐式类型的局部变量、隐式类型的数组和匿名类型
var kids = new[] {new { Name="Aidan" }, new { Name="Grant" }};
// 示例用法
foreach (var kid in kids)Console.WriteLine(kid.Name);

输出结果:

Aidan
Grant

二、数组转型
  对于元素为引用类型的数组,CLR允许将数组元素从一种类型隐式转型到另一种类型。为了成功转型,两个数组类型必须维数相等,而且从源类型到目标类型,必须存在一个隐式或显示转换。CLR不允许将值类型元素的数组转型为其他任何类型。(不过为了模拟实现这种效果,可利用Array.Copy方法创建一个新数组并在其中填充数据)。下面演示了数组转型过程:

private static void ArrayCasting() {
// 创建一个二维FileStream数组
FileStream[,] fs2dim = new FileStream[5, 10];// 隐式转型为一个二维Object数组
Object[,] o2dim = fs2dim;// 不能从二维数组转型为一维数组
//Stream[] s1dim = (Stream[]) o2dim;// 显式转型为二维Stream数组
Stream[,] s2dim = (Stream[,]) o2dim;// 显式转型为二维String数组
// 能通过编译,但在运行时会抛出异常
String[,] st2dim = (String[,]) o2dim;// 创建一个意味Int32数组(元素是值类型)
Int32[] i1dim = new Int32[5];// 不能将值类型的数组转型为其他任何类型
// Object[] o1dim = (Object[]) i1dim;// 创建一个新数组,使用Array.Copy将元数组中的每一个元素
// 转型为目标数组中的元素类型,并把它们复制过去
// 下面的代码创建一个元素为引用类型的数组,
// 每个元素都是对已装箱的Int32的引用
Object[] o1dim = new Object[i1dim.Length];
Array.Copy(i1dim, o1dim, 0);
}

  Array.Copy方法的作用不仅仅是将元素从一个数组复制到另一个数组。Copy方法还能正确处理内存的重叠区域。

Copy方法还能在复制每一个数组元素时进行必要的类型转换。Copy方法能执行以下转换:
1)将值类型的元素装箱为引用类型的元素,比如将一个Int32[]复制到一个Object[]中。
2)将引用类型的元素拆箱为值类型的元素,比如将一个Object[]复制到Int32[]中。
3)加宽CLR基元值类型,比如将一个Int32[]的元素复制到一个Double[]中。
4)在两个数组之间复制时,如果仅从数组类型证明不了两者的兼容性。

  在某些情况下,将数组从一种类型转换为另一种类型是非常有用的。这种功能称为数据协变性。利用数组协变性时,应该清楚由此带来的性能损失。

  注意:如果只需要把数组中某些元素复制到另一个数组,可以选择System.Buffer的BlockCopy方法,它的执行速度比Array.Copy方法快。不过,Buffer的BlockCopy方法只支持基元类型,不提供像Array的Copy方法那样的转型能力。方法的Int32参数代表的是数组中的字节偏移量,而非元素索引。如果需要可靠的将一个数组中的元素复制到另一个数组,应该使用System.Array的ConstrainedCopy方法,该方法能保证不破坏目标数组中的数组的前提下完成复制,或者抛出异常。另外,它不执行任何装箱、拆箱或向下类型转换。

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

  如果像下面这样声明一个数组变量:

FileStream[] fsArray;

  CLR会为AppDomain自动创建一个FileStream[]类型。这个类型将隐式派生自System.Array类型;因此,System.Array类型定义的所有实例方法和属性都将有FileStream[]继承,使这些方法和属性能通过fsArray变量调用。

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

  许多方法都能操作各种集合对象,因为在声明它们时,使用了IEnumerable,ICollection和IList等参数。可以将数组传给这些方法,因为System.Array也实现了这三个接口。System.Array之所以实现这些非泛型接口,是因为这些接口将所有元素都视为Systm.Object。然而,最好让System.Array实现这个接口的泛型形式,提供更好的编译时类型安全性和更好的性能。

五、数组的传递和返回
  数组作为实参传给一个方法时,实际传递的是对该数组的引用。因此,被调用的方法能修改数组中的元素。如果不想被修改,必须生成数组的一个拷贝,并将这个拷贝传给方法。注意,Array.Copy方法执行的是浅拷贝。

  有的方法返回一个对数组的引用。如果方法构造并初始化数组,返回数组引用是没有问题的。但假如方法返回的是对一个字段维护的内部数组的引用,就必须决定是否向让该方法的调用者直接访问这个数组及其元素。如果是就可以返回数组引用。但是通常情况下,你并不希望方法的调用这获得这个访问权限。所以,方法应该构造一个新数组,并调用Array.Copy返回对新数组的一个引用。

  如果定义一个返回数组引用的方法,而且该数组不包含元素,那么方法既可以返回null,又可以放回对包含另个元素的一个数组的引用。实现这种方法时,Microsoft强烈建议让它返回后者,因为这样做能简化调用该方法时需要的代码。

// 这段代码更容易写,更容易理解
Appointment[] app = GetAppointmentForToday();
for (Int32 a =0; a< app.Length; a++) {
// 对app[a]执行操作
}

如果返回null的话:

// 写起来麻烦,不容易理解
Appointment[] app = GetAppointmentForToday();
if( app !=null ) {
for (Int32 a =0; a< app.Length; a++) {
// 对app[a]执行操作}
}

六、创建下限非零的数组

  可以调用数组的静态CreateInstance方法来动态创建自己的数组。该方法有若干个重载版本,允许指定数组元素的类型、数组的维数、每一维的下限和每一维的元素数目。CreateInstance为数组分配内存,将参数信息保存到数组的内存块的额外开销(overhead)部分。然后返回对该数组的一个引用。

七、数组的访问性能
  CLR内部实际支持两种不同的数组
  1)下限为0的意味数组。这些数组有时称为SZ数组或向量。
  2)下限未知的一维或多维数组。
  可执行一下代码来实际地查看不同种类的输出

internal static class ArrayTypes {public static void Go() {
Array a;// 创建一个一维数组的0基数组,其中不包含任何元素
a = new String[0];Console.WriteLine(a.GetType());    // System.String[]// 创建一个一维数组的0基数组,其中不包含任何元素
a = Array.CreateInstance(typeof(String), new Int32[] { 0 }, new Int32[] { 0 });Console.WriteLine(a.GetType());    // System.String[]// 创建一个一维数组的1基数组,其中不包含任何元素
a = Array.CreateInstance(typeof(String), new Int32[] { 0 }, new Int32[] { 1 });Console.WriteLine(a.GetType());    // System.String[*] <-- 注意!

Console.WriteLine();// 创建一个二维数组的0基数组,其中不包含任何元素
a = new String[0, 0];Console.WriteLine(a.GetType());    // System.String[,]// 创建一个二维数组的0基数组,其中不包含任何元素
a = Array.CreateInstance(typeof(String), new Int32[] { 0, 0 }, new Int32[] { 0, 0 });Console.WriteLine(a.GetType());    // System.String[,]// 创建一个二维数组的1基数组,其中不包含任何元素
a = Array.CreateInstance(typeof(String), new Int32[] { 0, 0 }, new Int32[] { 1, 1 });Console.WriteLine(a.GetType());    // System.String[,]}
}

  对于一维数组,0基数组显示的类型名称是System.String[],但1基数组显示的是System.String[*]。*符号表示CLR知道该数组不是0基的。注意,C#不允许声明String[*]类型的变量,因此不能使用C#语法来访问一维的非0基数组。尽管可以调用Array的GetValue和SetValue方法来访问数组的元素,但速度会比较慢,毕竟有方法调用的开销。

  对于多维数组,0基和1基数组会显示同样的类型名称:System.String[,]。在运行时,CLR将对所有多维数组都视为非0基数组。这自然会人觉得应该显示为System.String[*,*]。但是,对于多维数组,CLR决定不用*符号,避免开发人员对*产生混淆。
  访问一维0基数组的元素比访问非0基数组或多维数组的元素稍快一些。首先,有一些特殊的IL指令,比如newarr,ldelem,ldelema等用于处理一维0基数组,这些特殊IL指令会导致JIT编译器生成优化代码。其次,JIT编译器知道for循环要反问0到Length-1之间的数组元素。所以,JIT编译器生成的代码会在运行时测试所有数组元素的访问都在数组有效访问内。

  如果很关系性能,请考虑由数组构成的数组(即交错数组)来替代矩形数组。

  下面C#代码演示了访问二维数组的三种方式:

internal static class MultiDimArrayPerformance
{
private const Int32 c_numElements = 10000;public static void Go()
{
const Int32 testCount = 10;
Stopwatch sw;// 声明一个二维数组
Int32[,] a2Dim = new Int32[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: 用普通的安全技术访问数组中的所有元素
sw = Stopwatch.StartNew();
for (Int32 test = 0; test < testCount; test++)
Safe2DimArrayAccess(a2Dim);
Console.WriteLine("{0}: Safe2DimArrayAccess", sw.Elapsed);// 2: 用交错数组技术访问数组中的所有元素
sw = Stopwatch.StartNew();
for (Int32 test = 0; test < testCount; test++)
SafeJaggedArrayAccess(aJagged);
Console.WriteLine("{0}: SafeJaggedArrayAccess", sw.Elapsed);// 3: 用unsafe访问数组中的所有元素
sw = Stopwatch.StartNew();
for (Int32 test = 0; test < testCount; test++)
Unsafe2DimArrayAccess(a2Dim);
Console.WriteLine("{0}: Unsafe2DimArrayAccess", sw.Elapsed);
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;
}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;
}
}

  本机结果是:

  可以看出,安全二维数组访问技术最慢。安全交错数组访问时间略少于安全二维数组。不过应该注意的是:创建交错数组所花的时间多于创建多维数组所花的时间,因为创建交错数组时,要求在堆上为每一维分配一个对象,造成垃圾回收器的周期性活动。所以你可以这样权衡:如果需要创建大量"多个维的数组",而不会频繁访问它的元素,那么创建多维数组就要快点。如果"多个维的数组"只需创建一次,而且要频繁访问它的元素,那么交错数组性能要好点。当然,大多数应用中,后一种情况更常见。

  最后请注意,不安全和安全二维数组访问技术的速度大致相同。但是,考虑到它访问是单个二维数组(产生一次内存分配),二不像交错数组那样需要许多次内存分配。所以它的速度是所有技术中最快的。

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

  如果性能是首要目标,请避免在堆上分配托管的数组对象。相反,应该在线程栈上分配数组,这是通过C#的 stackalloc语句来完成的。stackalloc语句只能创建一维0基、由值类型元素构成的数组,而且值类型绝对不能包 含任何引用类型的字段。当然,在栈上分配的内存(数组)会在方法返回时自动释放。

以下代码显示如何使用C#的stackalloc语句:

internal static class StackallocAndInlineArrays
{
public static void Go()
{
StackallocDemo();
InlineArrayDemo();
}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));
}
}private unsafe struct CharArray
{
// 这个数组以内联的方式嵌入结构
public fixed Char Characters[20];
}
}

  通常,因为数组是引用类型,所以在一个结构中定义的数组字段实际只是指向数组的一个指针;数组本身在结构的内存的外部。不过,也可以像上述代码中的CharArray结构那样,直接将数组嵌入结构中。要在结构中直接嵌入一个数组,需要满足以下几个要求:

  1)类型必须是结构(值类型);不能在类(引用类型)中嵌入数组。
  2)字段或其定义结构必须用unsafe关键字标记
  3)数组字段必须使用fixed关键字标记
  4)数组必须是一维0基数组。
  5)数组的元素类型必须是一下类型之一:Boolean,Char,SByte,Byte,Int16,Int32,UInt16,UInt32,Int64,UInt64,Single或Double。
  内联(内嵌)数组常用于和非托管代码进行互操作,而且非托管数据结构也有一个内联数组。不过,也可用于其他情况。

转载于:https://www.cnblogs.com/zxj159/p/3569500.html

[CLR via C#]16. 数组相关推荐

  1. thinking-in-java(16) 数组

     [16.1]数组有什么特殊  1)数组与其他类型容器的区别: 效率,类型和保持基本类型的能力:  数组是效率最高的存储和随机访问对象引用序列的方式:  数组大小固定,容器大小可以不固定,所以这增加了 ...

  2. Java的知识点16——数组概述和特点、数组声明、初始化、数组的遍历、for-each循环、数组的拷贝

    数组的定义 数组是相同数据类型的有序集合.数组描述的是相同类型的若干个数据,按照一定的先后次序排列组合而成.其中,每一个数据称作一个元素,每个元素可以通过一个索引(下标)来访问它们.数组的三个基本特点 ...

  3. 编程方法学16:数组

    前言 本笔记是斯坦福公开课,编程方法学的学习笔记. 总体而言,这门课讲了很多很基础的东西,具有很强的通用性. 正文 本次的笔记对应的是第十六节课,这堂课讲的是数组的相关知识 1数组 记录大量信息的一种 ...

  4. 跟小静读CLR via C#(16)--泛型

    泛型就像是一个模板,常常定义一些通用的算法,具体调用时再替换成实际的数据类型,提高了代码的可重用性. 一.初识泛型 1. 简单实例 以最常用的FCL中的泛型List<T >为例: stat ...

  5. javascript学习系列(16):数组中的every方法

    最好的种树是十年前,其次是现在.歌谣 每天一个前端小知识 提醒你改好好学习了 知乎博主 csdn博主 b站博主  放弃很容易但是坚持一定很酷     我是歌谣 喜欢就一键三连咯 你得点赞是对歌谣最大的 ...

  6. c语言找出递增子数组的长度,编程之美2.16 数组中最长递增子序列的长度

    改进的方法看的头大了却还是不清楚,哎...搞算法的苦啊,纠结啊. 编程之美这本书里面就有关于这道题的一些解法,求一个一位数组中的最长序列的长度.例如,在序列1,3,2中,最长递增序列是1,3. 这道题 ...

  7. JavaScript百炼成仙1.16 数组方法

    "首先是push方法,它可以把一个元素添加到数组里面.把数组想象成一个长长的盒子,我如果想要给数组添加新的元素,就可以用这个push方法."说着,叶小凡打出一段代码流: var b ...

  8. 杨桃的Python进阶讲座16——数组array(六)一维数组和二维数组的索引和取值(配详细图解)

    本人CSDN博客专栏:https://blog.csdn.net/yty_7 Github地址:https://github.com/yot777/ 在进阶讲座8中讲过数组(矩阵)的维度,我们再看看多 ...

  9. rust(16)-数组

    PS F:\learn\rustlearn> rustc learn14.rs PS F:\learn\rustlearn> ./learn14.exe 10 20 30 PS F:\le ...

最新文章

  1. Visual Studio 2005 Professional Released
  2. 20155308 《信息安全系统设计基础》课程总结
  3. 哈希表(散列查找)(c/c++)
  4. java:能否得到一个对象的内存地址?
  5. idea test包_6.Flinkx如何在idea中运行?
  6. ADO学习(三)Command 对象
  7. SAP odata get metadata in QHD - still has cache logic
  8. iOS:Core Data 中的简单ORM
  9. HTTP协议 (四) 缓存
  10. oracle apache服务占用80端口
  11. Network Delay Time
  12. 电脑bios进入方法介绍
  13. 破解第三课 关键跳和关键CALL
  14. C++ 已知两个时间(年月日)求日期差
  15. Android应用内社区SDK技术架构浅析
  16. 小甲鱼老师《带你学C带你飞》的后续课程补充
  17. 闵帆老师《论文写作》课程心得
  18. npm WARN checkPermissions Missing write access to ......解决方法
  19. substance的使用示例(转)
  20. 任鸟飞FPS类型游戏绘制和游戏安全,反外挂研究(一)

热门文章

  1. 递归算法1加到100_五种循环方法计算1加到100
  2. C语言求x和y的乘积,计算方程式,求x,C语言中怎么计算x,y的值?
  3. java自学笔记_JAVA自学笔记(4)
  4. 已知圆心,半径,角度,求圆上的点坐标
  5. Spring操作Redis
  6. MySQL 对查询结果进行排序
  7. C++类的定义和对象的创建
  8. 奇异值分解SVD(证明全部省略)
  9. 机器学习之线性回归(python)
  10. MATLAB基础教程(6)——使用matlab求解线性方程组