把Array说透(续一)
1. 写在前面的
在前文中,我主要介绍了数组的一些相关知识,希望加深各位对Array的理解,不过,看过Ivony…同学的回复,我发觉自己离说透还有很大的距离,于是就有了下面的文章。在本文中,我也主要来围绕Ivony…同学提出的几点问题来作以说明,问题如下:
A、数组在托管堆内部是怎么存放的?数组元素的位置是连续的么?
B、非零基数组可以和零基数组转换么?
C、int[]与System.Array的关系到底是同一类型?还是基类与派生类的关系?
D、ldelem不检查下标越界么?
E、多维数组可以和零基数组转换么?
F、Array.Copy和CopyTo与手动拷贝性能有多大差距?
G、数组的协变是怎么做到的?
H、数组是如何实现泛型接口(如IList<T>)的?
I、多维数组每一维度长度必须相等么?必须零基么?
J、数组的Length属性到底指示的是什么?
2. 数组内存详解
在这里,我们依然把数组分为零基数组和非零基数组来讨论。
首先来看零基数组的内存分配,废话少说,我们先来看测试代码:
static unsafe void Main(string[] args) { int[] intArr = new int[3]; intArr[0] = 1; intArr[1] = 2; intArr[2] = 3; }
代码本身很简单,接下来单步执行向下看,首先我们来查看一下源代码的汇编代码:
int[] intArr = new int[3]; 00000035 mov edx,3 0000003a mov ecx,61CD4192h 0000003f call FFFB2140 00000044 mov dword ptr [ebp-44h],eax 00000047 mov eax,dword ptr [ebp-44h] 0000004a mov dword ptr [ebp-40h],eax intArr[0] = 1; 0000004d mov eax,dword ptr [ebp-40h] 00000050 cmp dword ptr [eax+4],0 00000054 ja 0000005B 00000056 call 624B6B29 0000005b mov dword ptr [eax+8],1 intArr[1] = 2; 00000062 mov eax,dword ptr [ebp-40h] 00000065 cmp dword ptr [eax+4],1 00000069 ja 00000070 0000006b call 624B6B29 00000070 mov dword ptr [eax+0Ch],2 intArr[2] = 3; 00000077 mov eax,dword ptr [ebp-40h] 0000007a cmp dword ptr [eax+4],2 0000007e ja 00000085 00000080 call 624B6B29 00000085 mov dword ptr [eax+10h],3 }
在这里,我们就可以清晰地发现,在0x0000005b,0x00000070和0x00000085中,mov操作的目标地址之间是相隔4个Bytes的,也就是一个整数位。接下来我们来进一步证实。
当我们为数组分配过内存地址后,打开即使窗口查看数组所在的内存地址。
接下来打开内存窗口还查看0x015cc790内存块的数据:
以上是对数组赋值前的情况,赋值后的内存数据如下:
在这里可以更清晰地看出,数组元素之间差的正好是4个Bytes,也就是一个整数位。由此,我们可以得出结论。零基数组的元素在内存中是连续排布的。
接下来我们来看一下非零基数组:
由于空间所限,过程如上,就不再发,截图证明:
总之,当我们在托管堆中为数组分配内存时,数组占据一段连续的内存空间。
我们知道,当我们在托管堆中初始化一个对象时,每个对象都需要维护一个指针,该指针的作用是指向下一块空闲内存空间,由于对数组的操作经常是循环遍历等操作,这样如果把数组分配到一个连续的内存空间有一下两个好处:
A. 减少内存碎片
B. 节省内存,不需要维护指针
C. 基地址不需要发生变化,只需要改变偏移量即可,在一定程度上也提高了访问的效率。
接下来,我们还需要来补充一下数组在栈上分配内存的情况:
还记得上文中提到的这个关键字吧,stackalloc,就是他了。补充一下,在上文的回复中,有人问到说栈空间上分配的内存是不是也被垃圾回收器回收?这里的栈空间和C语言中的栈一样,没有垃圾回收器,每个变量都有他自己的作用域,当出了作用域后,变量自动销毁,具体的函数执行过程,请参看《深入理解计算机系统》。
3. 再论零基数组和非零基数组
我们先来看这样一段代码:
static void Main(string[] args) { int[,] intArr = (int[,])(Array.CreateInstance(typeof(Int32), new int[] { 3,4 }, new int[] { 1,1 })); intArr[2, 3] = 1; }
这段代码没有问题,我们将Array显式地转换成了强类型的二维数组,然后直接访问索引对其复制。
但是我们知道,对弱类型的Array而言,我们不能通过其下标访问他的元素,而只能通过SetValue和GetValue来获得值,但是我们看到SetValue和GetValue访问和设置的值的类型都是Object,这就意味着我们需要对其进行一次装箱或者拆箱。那么我们有没有办法也生成一个强类型的非零基数组呢?
在上文中,我们提到过,.NET Framework的几种数组类型:
一维零基数组:System.Int32[]。一维非零基数组:System.Int32[*]。多维数组:System.Int32[,]。
那么也就是说,我们是否能通过这样的代码来把Array转换成一维非零基数组呢?
static void Main(string[] args) { int[*] intArr = (int[*])(Array.CreateInstance(typeof(Int32), new int[] { 3}, new int[] { 1 })); }
事实证明是错误的。在CLR via C#中Jeffery有这样一段话:
“C# does not allow you to declare a variable of type string[*],and therefore it is not possible to user C# syntax to access a single-dimensional ,non-zero-based array.”
这段话翻译成中文的意思就是:C#不允许声明一个string[*]类型的变量,因此,我们能够使用C#语法来访问一个非零基一维数组。
通过以上的解释,我们也许又额外明白了一点,Array究竟是数组类型,还是数组类型的基类?
通过上面的一些代码,我们不妨又把数组重新分类为“强类型数组”和“弱类型数组”。而Array就属于弱类型数组。为什么Array是所有数组类型的基类,我没想出办法来如何证明,只是看到Jeffery说了这样一句话:
“All Arrays are Implicitly Derived from System.Array”。
我想这句话可以说明问题了,不过还是希望各位大侠指点如果证明这一点。
未完持续…………