.NET中数组的隐秘特性[13]

[入库:2005年8月18日] [更新:2007年3月25日]

本文简介:选择自 dy_2000_abc 的 blog

arraylist.adapter(ilist).getrange(start, count)

使用对分检索算法查找

arraylist.adapter(ilist).binarysearch()

排序

arraylist.adapter(ilist).sort()


数组子集
       下面的代码可以用来提取array数组的子集。
public static array getrange(array range, int start, int count)
{
    type type = array.type;
    array newarray = array.createinstance(type.getelementtype(), count);
    array.copy(array, start, newarray, 0, count);
    return newarray;       
}
       这种方法实际上是生成一个数组的子集拷贝,会占用很多的内存。arraylist提供了一个方法getrange来获取子集,如果数组子集的长度很大,建议使用getrange,因为它内部没有对数组子集进行复制,节约了大量的内存。getrange方法返回一个arraylist的子类,相当于数组视图。你可以对视图进行各种操作(添加,修改或者删除元素)。需要注意的是,我们只能通过getrange返回的视图对原数组进行操作。如下面代码所示,对al进行修改会导致视图alview失效,你再次使用视图alview时将会抛出invalidoperationexception异常。
int[] a=new int[5];
arraylist al=new arraylist(a);
arraylist alview=al.getrange(0,2);
al[0]=1;
int a=(int)alview[0];
封装支持
       arraylist提供了三个方法:fixedsize,readonly和synchronized,每个方法有两个重载版本,这三个方法接受ilist或者arraylist为参数。fixedsize方法返回具有固定大小的列表包装,其中的元素允许修改,但不允许添加或移除。readonly方法返回只读的列表包装。synchronized方法返回同步(线程安全)的列表包装。
       这些方法可以混合使用,比如:arraylist.synchronized(arraylist.readonly(list))返回一个只读的同步的数组。
       readonly方法在禁止修改数组方面很有用。你需要注意两种情况:一是数组总是通过引用的方式传递的;另外一种情况是,在marshal过程中,数组元素个数如果超过10,clr不是copy数组,而是“锁住”原数组(防止它被垃圾回收器重定位)。这两种情况下你很可能会不经意的修改数组,从而使程序的结果不可预期。
数组转换
数组变异(array covariance)
       在c++中可以将一种类型的指针数组转换成另一种指针类型。.net中,clr允许将引用类型数组的元素类型隐式或显式转换成另一种类型,这种性质被称为数组变异(array coveriance)。clr不允许将值类型数组转换为其它类型数组。你可以使用其他办法来实现值类型数组的转换,比如,使用array.copy方法来创建一个目标数组并将原数组元素转换到目标数组。
int[] a=new int[5];
a[0]=1;
double[] b=new double[5];
array.copy(a,0,b,0,1);
       不管是显式或者隐式的转换,在编译的时候,原数组类型被转换成为目标类型,转换的前提是两个数组必须有同样的维度数。在转换过程中,数组只是被重新解释,在内存占用上没有任何变化。
       如果转换是隐式的,在转换前,数组的元素类型被转换为它支持的接口或者它的一种基类型,这是不需要显示地cast,也不执行任何运行时检查。如果转换是显式的(转换是从一种接口转换为另一种接口,或者从基类转换为子类,或者从一种类型转换为一种它不直接支持的接口),这时需要显示地cast,并执行运行时检查。
       前面已经提到,引用类型数组有一个元素类型内部字段(elementtype)。这个字段在转换前后都是保持不变的。执行运行时检查主要是检查新类型与元素类型(elementtype)之间的兼容性。
下面的例子可以使你更好的了解数组转换。
public class animal {}

object [] data = new animal[2]; // animal[]被隐式地转换为object[]
animal [] animals1 = data; // error: 从object[]到animal[]需要显式转换
animal [] animals2 = (animal[]) data; // object[] 被显式转换成 animal []
   
string [] strings1 = (string[]) animals2; // 编译失败,因为string[]与animal[]之间不能相互转换
string [] strings2 = (string[]) data; // 编译成功,但是运行时会发生异常,因为animal[]不是从string[]继承而来
   
object [] data2 = new object[1];
data2[0] = new animal();
animal [] animals3 = (animal[]) data2; // 编译成功,但是运行时会发生异常。运行时检查将会验证元素类型(elementtype)与目标类型animal的兼容性。

animal[] animal4=new animal[1];
object[] data3=(object[])animal4;
animal[] animal5=(animal[])data3;//编译成功,运行时也不会发生异常。运行时检查将检测到data3的元素类型(elementtype)与目标类型animal的兼容。
       能够将一种类型的数组重新解释为其它类型,这种方式无论在内存使用还是时间上都大大提高了程序的效率。如果从一种类型数组转换为另一种类型数组需要重新构造一个数组的话,显然,程序的性能会受到极大的冲击。
public void test()
{
string[] data = new string [] { "a", "b", "c", "d", "e" };
setrange(data, 1, 3, "x" );
}
public void setrange(object [] array, int start, int count, object value)
{
for (int i=0; i < count; i++)
array[i+start] = value;
}
      上面的例子,新的字符串数组是{ "a", "x", "x", "x", "e" },如果将一个整数作为参数value,将会引起运行时异常,因为引用类型数组在分配数组元素时执行类型检查,所以在array[i+start]=value这一句会发生异常。
下面的例子显示了值类型数组和引用类型数组的一些区别。
write( new int [] { 1, 2, 3 } );  // 显示 "system.int32 []"
write( new string[] {"a", "b", "c" } ); // 显示 "a", "b", "c"
void write(params object [] args)
{
    for (int i=0; i<args.length; i++)
    console.writeline(args[i]);
}
       数组变异(array covariance)的不良影响是每次给数组元素分配对象都必须执行类型检查。
       你可以使用object[]或者array来创建一个"通用"数组。多数情况,object[]在性能上要优于array,因为object[]属于sz数组,有专门的il指令来设置或读取数组元素。但是,object[]不适用于值类型数组和多维数组,而array则可以。
数组元素转换(array.copy)
       array.copy方法可以在复制时转换数组元素。方法可以执行以下转换:
1 将值类型元素装箱到引用类型,比如:将int[]复制到object[]
2 将引用类型元素取消装箱,比如:将上面得到的object[]转换为int[]
3 拓宽转换,比如,可以将int[]复制到double[],但不能将double[]复制到int[]。
       复制引用类型数组时,先进行类型检查,然后执行浅表复制,如果类型不兼容,抛出arraytypemismatchexception异常。
public array convert(array array, type type)
{
    array newarray = array.createinstance(type, array.length);
    array.copy(array,0, newarray,0, array.length);
    return newarray;
}
通过反射机制访问内部成员
       你可以通过反射机制来访问、调用或者修改arraylist类的内部成员,不管它们被声明为private、protected或者internal。
       下面代码取得arraylist的内部成员_items:
object[] abc=new object[5];
arraylist al=new arraylist(5);
al.add("abc");
abc=(object[])al.gettype().getfield("_items",bindingflags.nonpublic|bindingflags.instance).getvalue(al);
console.writeline(abc[0]);
       你可以阅读微软公布的rotor包来了解类的内部成员,也可以使用il反编译器,比如reflector或者anakrino。
数组性能
       对数组进行索引一般都要进行范围检查。根据微软的说法,编译器进行了一些特殊的优化来改善遍历数组或者字符串的性能。我们先来比较一个下面三种遍历数组的方案,看看那一种更快。
a是一维int类型数组
1)
int hash = 0;
for (int i=0; i< a.length; i++)
{
 hash += a[i];
}
2)
int hash = 0;
int length = a.length;
for (int i=0; i< length; i++)
{
 hash += a[i];
}
3)
foreach (int i in a)
{
 hash += i;
}
       令人惊讶的是,在目前这个的jit编译器下,第一个例子是最快的,第三个例子是最慢的。在下一版本jit编译器,第三个例子将会跟第一个例子具有相同的速度。
为什么第一个例子比第二个例子快呢?这是因为编译器认识 for (int i=0; i<s.length; i++) 这种模式(仅限于字符串和数组)。编译器将会存储数组的长度,这样在每一次遍历时不用调用任何方法(因为jit编译器可以自动嵌入只包含简单的流程控制的非虚拟方法和不超过32个字节的il指令,在这里,编译器将数组长度的引用嵌入)。
另外,编译器还消除了每一次循环对s[i]的范围检查,因为i在for条件中已经被限制在0和数组长度之间。在第二个例子中,由于使用一个整数代替了数组长度,编译器就不会认为它是 for (int i=0; i<a.length; i++) 模式(不会假设i在0和数组长度之间),所以每一次循环都会执行一次范围检查。这就是为什么第二个例子要比第一个例子慢。
一些使用数组应该注意的问题
大数组问题

       大数组对性能有很大的影响。超出85k的对象被称为大对象,它们被分配在大对象堆。几乎所有的大对象都是数组,有一些是字符串。显然,很少的类包含有那么多的成员使得占用内存超过85k。大对象不能被压缩,同时,它只能在全垃圾回收(包含第2代的垃圾回收)中才能被回收。如果大对象包含了析构函数,那么至少要两次全垃圾回收才能回收它们。全垃圾回收发生的次数一般的是0代垃圾回收的1/100,显然,回收大垃圾对象占用的内存需要经过一段较长的时间。从内存分配角度看,在程序中频繁的分配临时使用的大对象是一个很糟糕的设计,甚至是最差的设计。

构造arraylist或其他集合类时,为它们指定足够的容量,避免扩大容量时数组复制造成的性能损失

本文关键:数组 Array ArrayList .net
 

本站最佳浏览方式为 分辨率 1024x768 IE 6.0(或更高版本的 IE浏览器)

go top