CLR笔记:13.数组
CLR支持一维/多维/交错数组。
两种声明方式:
a = new String[0, 1];
String[] s = new String[5];
注意,声明不要给与数组长度,因为此时还不分配内存;new时要指定长度。
将数组声明为Array和像String[]这样带中括号的,效果是一样的,只是前者更灵活,当然类型不安全,可能会有运行期错误。
所有数组都隐式派生于System.Array,后者派生自System.Object,所以数组是引用类型。
值类型的数组,new时,会创建实际的对象,所有元素都初始化为0;
引用类型的数组,new时,只是创建引用,不会创建实际的对象。
CLS要求数组必须是0基数组。
每个数组都关联了一些额外的信息,称为开销信息(Overhead)。
JITer只在执行一次循环之前检查一次数组边界,而不是每次循环都这么做。
13.1 数组的类型转换
对于引用类型的数组,两个数组类型必须维数相同,并且从源元素类型到目标元素类型,必须存在一个隐式(向父类转)或显示(向子类转)的类型转换。
对于值类型的数组,不能将其转换为其他任何一种类型——使用Array.Copy方法替代:
Object[] obldim = new Object[ildim.Length];
Array.Copy(ildim, obldim, ildim.Length);
Array.Copy还可以再复制每个数组元素时进行必要的类型转换:
*将值类型的元素装箱为引用类型的元素
*将引用类型的元素拆箱为值类型的元素
*加宽CLR基元值类型,Int32到Double
*如果两个数组的类型不兼容(从Object[]转型为IFormattable[]),就对元素进行向下类型转换。
数组的协变性:将数组从一种类型转换为另一种类型,——会有性能损失,如下代码,会在运行期报错:
Object[] oa = sa;
oa[5] = "Jax";
oa[3] = 1; //运行期错误
如果只是需要将数组中的某些元素复制到另一个数组,可以使用System.Buffer的BlockCopy()方法,执行速度比Array.Copy快,但是只支持基元类型,不具有转型能力。
13.3 所有数组都隐式实现IEnumerable,ICollection,IList
对于泛型接口,因为多维数组和非0基数组的问题,System.Array并不完全实现。
只有一维0基的引用类型的数组,在创建时,会自动实现了IEnumerable<T>,ICollection<T>,IList<T>,T为这个数组的类型;还为T的所有基类型B实现了IEnumerable<B>,ICollection<B>,IList<B>接口。
如FileStream[] fsArray,会自动实现了IEnumerable<FileStream>,ICollection<FileStream>,IList<FileStream>,同时会实现IEnumerable<Stream>,ICollection<Stream>,IList<Stream>,IEnumerable<Object>,ICollection<Object>,IList<Object>,所以fsArray可以作为参数传递给以下方法:
void M2(ICollection<Stream> sCollection) { }
void M3(IEnumerable<Object> oEnumerable) { }
但是,对于一维0基的值类型的数组,在创建时,会自动实现了IEnumerable<T>,ICollection<T>,IList<T>,T为这个数组的类型;但是不会为其基类实现接口。如DateTime[] dtArray;这个值类型dtArray不能传递给上面的M3方法。
13.4 数组的传递和返回
Array.Copy返回的是浅Copy。
数组最好是0长度,而不是null
13.5 非0基数组
使用Array.CreateInstance方法,可以指定数组中元素类型,数组维数,数组下界,数组每一维中元素个数,有若干重载,如下:
Decimal[] fsArray = (Decimal[])Array.CreateInstance(typeof(Decimal), 2);
//重载2,数组长度用一个数组表示,指定多维数组的维数和各维上的长度4和5
Int32[] lengths = { 4, 5 };
Decimal[,] fsArray = (Decimal[,])Array.CreateInstance(typeof(Decimal), lengths);
//重载3,最后一个参数指定数组下界
Decimal[] fsArray = (Decimal[])Array.CreateInstance(typeof(Decimal), 2, 1);
//重载4,最后一个数组参数,指定数组各维上的下界
Int32[] lengths = { 4, 5 };
Int32[] lowerBounds = { 1, 2 };
Decimal[,] fsArray = (Decimal[,])Array.CreateInstance(typeof(Decimal), lengths, lowerBounds);
//重载5,创建一个三维数组,后三个参数分别指定各维的长度
Decimal[,,] fsArray = (Decimal[,,])Array.CreateInstance(typeof(Decimal), 2, 3, 4);
可以使用GetLowerBound(维数);与GetUpperBound(维数);获取数组的边界
在一位数组fsArray中,可以使用fsArray数组对象的GetValue()和SetValue()方法来操作数组,但是比较慢。
13.6 数组访问性能
对于1维数组,0基的typeof为String.String[];非零基为String.String[*]
对于多维数组,都会显示String.String[,]。在运行时,CLR将多维数组视为非零基数组。
访问1维零基数组 比 非零基1维数组 和 多维数组 速度快很多。这是因为:
1.有特殊的IL指令,用于处理1维零基数组,而不用在索引中减去偏移量
2.JIT会将索引范围检查代码从循环中取出,从而只执行一次检查。
关于for循环遍历数组:
for (Int32 index = 0; index < a.Length; a++)
{
//对a[index]进行操作
}
Length是一个数组属性,调用一次后,JIT会将结果存入一个临时变量,以后每次循环迭代访问的都是这个变量,而不是再次计算长度,从而速度更快。
在循环前,JIT编译器会自动生成代码检测有效范围:if((Length-1) < a.GetUpperBound(0)),只会检测一次。
如果将Length保存在一个本地变量,在for循环时每次都会比较该变量,反而会损害性能。
以上只适用于0基数组。非零基数组中,JIT编译器在循环中,每次都要检查制指定索引范围是否有效,此外还要从指定索引减去数组下界,从而降低了性能。
提升性能的两个办法:
使用交错数组(数组的数组)来代替多维数组。
使用非安全数组代替非零基数组,在访问数组时关闭索引边界检查——只适用于基元值类型。
使用非安全代码访问2维数组:
public static unsafe void Unsafe2DimArrayAccess(Int32[,] a)
{
Int32 dim0LowIndex = 0; //等价于a.GetLowerBound(0)
Int32 dim0HighIndex = a.GetUpperBound(0);
Int32 dim1LowIndex = 0; //等价于a.GetLowerBound(1)
Int32 dim1HighIndex = a.GetUpperBound(1);
Int32 dim0Elements = dim0HighIndex - dim0LowIndex;
fixed (Int32* pi = &a[0, 0])
{
for (Int32 x = dim0LowIndex; x <= dim0HighIndex; x++)
{
Int32 baseOfDim = x * dim0Elements;
for (Int32 y = dim1LowIndex; y <= dim1HighIndex; y++)
{
Int32 element = pi[baseOfDim + y];
}
}
}
}
13.7 非安全数组和内嵌数组
非安全数组可以访问以下元素:
托管堆上的数组
非托管堆的数组
线程堆栈上的数组
在性能第一的前提下,避免在堆上分配数组对象,应该在线程堆栈上分配——使用stackalloc
stackalloc只适用于创建1维0基值类型数组,而且值类型中决不能包括任何引用类型。见以下方法,将一个字符串倒置:
{
unsafe
{
const Int32 width = 20;
//在堆栈上分配数组
Char* pc = stackalloc Char[width];
String s = "Jax Bao";
for (Int32 index = 0; index < width; index++)
{
pc[width - index - 1] = (index < s.Length) ? s[index] : ' ';
}
String newS = new String(pc, 0, width);
Console.WriteLine(newS.Trim());
}
}
在struct中定义数组,一般来说,数组本身在struct的外部。
要使数组内嵌在struct中,要遵从:
1.struct要用unsafe标记
2.数组字段要用fixed标记
3.数组必须是1维0基的
4.数组类型必须是基元值类型
采用内嵌数组实现的方法,将一个字符串倒置:
{
unsafe
{
CharArray ca; //在堆栈上分配数组
Int32 widthInBytes = sizeof(CharArray);
Int32 width = widthInBytes / 2;
String s = "Jax Bao";
for (Int32 index = 0; index < width; index++)
{
ca.Characters[width - index - 1] = (index < s.Length) ? s[index] : ' ';
}
String newS = new String(pc, 0, width);
Console.WriteLine(newS.Trim());
}
}
public unsafe struct CharArray
{
public fixed Char Characters[20];
}