分享
 
 
 

Effective C# 5: 警惕隐式box和unbox操作对程序性能的影响

王朝c#·作者佚名  2006-01-08
窄屏简体版  字體: |||超大  

5.警惕隐式box和unbox操作对程序性能的影响

陈铭 Microsoft .NET MVP

“开心辞典、急智问答”——指出下面的程序段中包含的所有box和unbox操作:

public interface IMovable {

void Move(int newx, int newy);

}

public struct Point : IMovable {

public int X, Y;

public void Move(int newx, int newy) {

X = newx; Y = newy;

}

}

...

Point pt1;

pt1.X = 1; pt1.Y = 1;

ArrayList al = new ArrayList(); //1

al.Add(pt1);

Point pt2 = (Point)al[0];

Type t = pt2.GetType();

Point temp = (Point)al[0]; //2

temp.Move(5, 5);

((IMovable)al[0]).Move(4, 4);

DateTime dt = DateTime.Now; //3

string str2 = dt.ToString();

string str1 = pt1.ToString();

int m = 1; //4

string str3 = m + ", " + dt;

...

在条款X中我们曾经介绍过box操作包含了内存分配、对象拷贝等“繁重”的工作,因此正确识别出

程序中存在的隐式box/unbox操作对提高.NET程序的性能至关重要。

单就概念而言,box和unbox操作非常易于理解:当程序期待一个引用类型的对象,而实际得到的

是一个值类型对象的时候,就必须通过box操作将其转换成引用类型;反之,如果程序期待一个简单

的值类型对象,而得到的却是经过box包装的,那么就必须再通过unbox操作将它转换回来。

然而,在实际使用当中——尤其是与接口继承、多态等面向对象特性混合使用的时候,正确判断

box和unbox操作的存在就不那么容易了。因此,还是让我们逐段的分析上面提到的程序,看看你究

竟掉进了几个陷阱。J

ArrayList al = new ArrayList();

al.Add(pt1);

Point pt2 = (Point)al[0];

Type t = pt2.GetType();

这是box和unbox操作最简单基本的形式:由于ArrayList的Add方法需要一个Object类型的参数,

而程序实际提供的参数pt1是一个值类型对象,所以编译器必须产生相应的box指令,在调用Add方

法之前生成一个与pt1对应的引用类型对象,并以该对象做为Add方法的参数。

由于.NET在数组实现上使用了一些特殊的手法,值类型数组的各种操作并不需要box和unbox

(参见条款2)。

在接下来的一条语句中,al[0]返回一个Object类型的对象引用,程序需要将其强制转换成Point

类型,所以编译器也必须产生相应的unbox指令来完成这种转换。

最后,GetType是Object的成员函数,而ValueType和Point都没有改写该函数(由于GetType不

是虚函数,改写GetType需要使用new关键字),所以这里实际调用的是从Object继承而来的GetType

成员函数。显然,这个函数总是假设自己作用于一个引用类型的对象,所以,编译器必须在调用

GetType函数之前对pt2进行box操作。

相比之下,接下来的代码要稍显复杂一些了,我们需要调用Point类的Move方法,那么我们有两个选择:

一是先将al[0]转换成Point对象,然后调用其方法:

Point temp = (Point)al[0];

temp.Move(5, 5);

我们前面已经分析过,这里强制类型转换的过程中实际包含了unbox操作。由于Move是Point的成员函数,

所以可以直接调用temp对象的Move方法而不需要再进行box操作。

需要注意的是,由于C#编译器的实现,temp实际上是unbox操作之后在堆栈上建立的新的Point对象。

所以调用temp对象的Move函数并不会影响ArrayList中al[0]的值。即使将这两个语句合并起来也无济于事:

((Point)al[0]).Move(5, 5); //同样不能改变al[0]

如果需要实际修改al[0],就必须将temp写回到ArrayList中去:

Point temp = (Point)al[0];

temp.Move(5, 5);

al[0] = temp;

另外一种方法是将其转换成IMovable接口,再调用Move方法:

((IMovable)al[0]).Move(4, 4);

也许有些出乎你的意料,使用这种方法并不需要任何的box/unbox操作。al[0]返回一个Object对象的引用,

将这样一个引用转换成一个IMovable接口显然不需要box操作,而调用一个接口函数同样无需box或者unbox。

使用这种方法的另一个好处是避免了前面提到的拷贝工作。通过接口函数调用,我们“就地”修改了

ArrayList当中的Point对象的值,既减少了代码量,同时也提高了程序的性能。

接下来是关于值类型对象ToString方法的调用:

DateTime dt = DateTime.Now;

string str2 = dt.ToString();

string str1 = pt1.ToString();

我们前面已经分析过调用GetType方法的情况,这里的ToString和GetType一样是Object的成员函数,

那么调用ToString不也是显然需要box操作的么?先别急,还是让我们逐个仔细分析:

DateTime: 我们当然希望DateTime的ToString方法能够得到它所包含的时间的字符表示,

因此DateTime结构改写了从Object继承来的ToString方法。在DateTime结构的ToString方法当中,

编译器和Runtime确信当前操作的一定是DateTime对象的实例,而不是其他从DateTime继承的类型,

因为.NET不允许从值类型进一步的继承。既然编译器和Runtime都能正确处理这种值类型对象的情况,

那么在用box操作把它包装起来就显得多此一举了。所以,调用DateTime的ToString方法并不需要

box操作。

Point: Point的情况又不相同,由于我们在编写Point结构的时候忘记改写ToString方法,

Point.ToString会直接调用从Object继承的版本,返回自己的类型全名。显然,Object的ToString方

法是针对引用类型编写的,也就是在这个函数调用过程中编译器和Runtime期待的是一个对象引用,所以

必须经由box操作来转换当前的值类型对象,从而得到一个有效的对象引用。

还剩下最后一个语句了:

int m = 1;

string str3 = m + ", " + dt;

而这里真正困难的是这种对象的拼接工作究竟是如何完成的? 为了简化对象和字符串的拼接,C#编译器

可以直接接受上面的语句,并且把它转换成对String类的Concat方法的调用:

public static string Concat(params object[]);

public static string Concat(params string[]);

上面的语句实际上本转换成了:

string str3 = String.Concat(m, “,”, dt);

显然String类的Concat方法只能接受Object和String这样的引用类型对象,而m和dt这样的值类型对象

就必须先通过box操作转换才能作为String.Concat函数的参数。

通过对上面语句的简单改写,我们实际上完全可以避免这两次box操作:

string str3 = m.ToString() + ”,” + dt.ToString();

由于System.Int32和DateTime均改写了Object.ToString方法,调用ToString方法不再需要额外的

box操作了。考虑到避免box操作意味着减少了内存分配和拷贝工作,了解程序中潜在的box/unbox操作并

尽可能的避免它们对程序的性能大有裨益。

最后,让我们总结一下常见的隐式box/unbox操作以及可能采用的避免这些操作的方法:

函数参数:当函数参数声明为Object类型,而实际传递的参数是一个值类型对象的时候。这一

类函数在设计的时候应该尽可能为值类型提供重载调用形式,例如Console.Write,除了提供Object参数调

用形式以外,为各种内建的值类型也都提供了重载调用形式。

使用容器对象:除了数组以外,其它的.NET容器(例如ArrayList)都是直接容纳引用类型对

象的。用它们来容纳值类型对象需要额外的box/unbox操作。因此如果程序性能至关重要的话,应当尽量使用

值类型数组取代其它容器对象。另外,通过定义Interface,也可以减少不必要的box/unbox操作。

调用Object类方法:调用值类型对象从Object类继承来的方法虚要先对对象进行box操作。因

此最好为值类型对象提供Object中定义的虚函数的实现,这样除了较少了潜在的box/unbox操作以外,还使

得诸如ToString方法的语义更加合理。

调用接口函数:如果值类型实现了某个接口,将值类型对象转换成接口需要进行box操作。

如果你能够正确识别程序中隐藏的box/unbox操作,那么一些简单的优化手段在这里同样适用。例如下面的代码:

int k = 5;

for (int i = 0; i

 
 
 
免责声明:本文为网络用户发布,其观点仅代表作者个人观点,与本站无关,本站仅提供信息存储服务。文中陈述内容未经本站证实,其真实性、完整性、及时性本站不作任何保证或承诺,请读者仅作参考,并请自行核实相关内容。
2023年上半年GDP全球前十五强
 百态   2023-10-24
美众议院议长启动对拜登的弹劾调查
 百态   2023-09-13
上海、济南、武汉等多地出现不明坠落物
 探索   2023-09-06
印度或要将国名改为“巴拉特”
 百态   2023-09-06
男子为女友送行,买票不登机被捕
 百态   2023-08-20
手机地震预警功能怎么开?
 干货   2023-08-06
女子4年卖2套房花700多万做美容:不但没变美脸,面部还出现变形
 百态   2023-08-04
住户一楼被水淹 还冲来8头猪
 百态   2023-07-31
女子体内爬出大量瓜子状活虫
 百态   2023-07-25
地球连续35年收到神秘规律性信号,网友:不要回答!
 探索   2023-07-21
全球镓价格本周大涨27%
 探索   2023-07-09
钱都流向了那些不缺钱的人,苦都留给了能吃苦的人
 探索   2023-07-02
倩女手游刀客魅者强控制(强混乱强眩晕强睡眠)和对应控制抗性的关系
 百态   2020-08-20
美国5月9日最新疫情:美国确诊人数突破131万
 百态   2020-05-09
荷兰政府宣布将集体辞职
 干货   2020-04-30
倩女幽魂手游师徒任务情义春秋猜成语答案逍遥观:鹏程万里
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案神机营:射石饮羽
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案昆仑山:拔刀相助
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案天工阁:鬼斧神工
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案丝路古道:单枪匹马
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案镇郊荒野:与虎谋皮
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案镇郊荒野:李代桃僵
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案镇郊荒野:指鹿为马
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案金陵:小鸟依人
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案金陵:千金买邻
 干货   2019-11-12
 
推荐阅读
 
 
 
>>返回首頁<<
 
靜靜地坐在廢墟上,四周的荒凉一望無際,忽然覺得,淒涼也很美
© 2005- 王朝網路 版權所有