C#学习笔记二:用实例深入理解装箱、拆箱
eshusheng(原作) 关键字 boxing unboxing
学习.NET的过程中,发现大多数的书都讲到了装箱(boxing)和拆箱(unboxing)的概念,至于为什么要理解装箱和拆箱?则总是一句话带过:优化程序的性能云云。至于为什么会对程序的性能产生影响,如何影响,我总感觉讲得并不透彻,当然也可能是我理解力有限。
这篇笔记,我并不打算对装箱和拆箱做全面的介绍,这些内容书上都有,csdn上也有很好的文章(请见kenli写的dotnet学习笔记一 - 装箱拆箱http://www.csdn.net/Develop/Read_Article.asp?Id=19575),我只做简单的总结,并在此基础上引入两个例子,一个例子用ILDASM.EXE查看装箱和拆箱的过程,另外一个例子我编制一个简单例子分析正确理解装箱和拆箱对程序性能的影响。
由于在下面的例子和以后的例子我们将再次用到ILDASM,但不再给出详细的解释,因此给出MSDN关于反汇编语言的帮助信息,要查找汇编语言的命令,请在MSDN中.NET Framework/参考/类库/System.Reflection.Emit 命名空间/OpCodes类中可以找到相关信息。
- 总结1:.NET中所有类型都是对象,所有类型的根是System.Object。
- 总结2:类型分为值类型(value)和引用类型(regerence type)。C#中定义的值类型包括:原类型(Sbyte、Byte、Short、Ushort、Int、Uint、Long、Ulong、Char、Float、Double、Bool、Decimal)、枚举(enum)、结构(struct)。引用类型包括:类、数组、接口、委托、字符串等。
实例一:读下列程序,你能说出其中进行了几次装箱和拆箱的操作吗?
using System;
class sample1
{
public static void Main()
{
int i=10;
object obj=i;
Console.WriteLine(i+","+(int)obj);
}
}
其中发生了三次装箱操作和一次拆箱操作。第一次object obj=i;将i装箱;而Console.WriteLine方法用的参数是String对象,因此,i+","+(int)obj中,i需要进行一次装箱(转换成String对象),(int)obj将obj对象拆箱成值类型,而根据WriteLine方法,比较将(int)obj值装箱成引用类型。说起来这么复杂,大家看看ildasm.exe的反汇编结果(如下图),就很容易理解了。注意图中红色圆圈的标识。
如果我们将Console.WriteLine(i+","+(int)obj);
改为:Console.WriteLine(obj+","+obj);
得到同样的效果,而其中仅进行一次装箱操作(object obj=i;),虽然这个程序并没有实际的意义,但是加深我们对概念的理解。
实例二:我这里我列出两个例子,装箱和拆箱对程序性能的影响不问自知。我的机器配置是P4 1.6A,512M内存。随后会列出测试的截图,你比我更快吗?当然是的?那么告诉我吧。😦
// 例子1:boxing1.cs
using System;
using System.Collections;
namespace test1
{
class Class1
{
static void Main(string[] args)
{
int count;
DateTime startTime = DateTime.Now;
ArrayList myArrayList = new ArrayList();
// 重复5次测试
for (int i = 5; i > 0; i--)
{
myArrayList.Clear();
// 将值类型加入myArrayList数组
for (count = 0; count < 5000000; count++)
myArrayList.Add(count); //装箱
// 重新得到值
int j;
for (count = 0; count < 5000000; count++)
j = (int)myArrayList[count]; //拆箱
}
// 打印结果
DateTime endTime = DateTime.Now;
Console.WriteLine("Start: {0}\nEnd: {1}\nSpend: {2}", startTime, endTime, endTime - startTime);
Console.WriteLine("Push ENTER to return commandline...");
Console.ReadLine();
}
}
}
下图是boxing1.exe的测试结果:
// 例子2:boxing2.cs
using System;
using System.Collections;
namespace test2
{
class Class2
{
static void Main(string[] args)
{
int count;
ArrayList myArrayList = new ArrayList();
// 构造 5000000 字符串数组
string[] strList = new string[5000000];
for (count = 0; count < 5000000; count++)
strList[count] = count.ToString();
// 重复5次测试
DateTime startTime = DateTime.Now;
for (int i = 5; i > 0; i--)
{
myArrayList.Clear();
// 将值类型加入myArrayList数组
for (count = 0; count < 5000000; count++)
myArrayList.Add(strList[count]);
// 重新得到值
string s;
for (count = 0; count < 5000000; count++)
s = (string)myArrayList[count];
}
// 打印结果
DateTime endTime = DateTime.Now;
Console.WriteLine("Start: {0}\nEnd: {1}\nSpend: {2}", startTime, endTime, endTime - startTime);
Console.WriteLine("Push ENTER to return commandline...");
Console.ReadLine();
}
}
}
下图是boxing2.exe的测试结果:
G:\myproject\c# inside\chap02>boxing2
Start: 2003-9-16 22:58:30
End: 2003-9-16 22:58:32
Spend: 00:00:02.0937500
Push ENTER to return commandline...
G:\myproject\c# inside\chap02>boxing2
Start: 2003-9-16 22:58:41
End: 2003-9-16 22:58:43
Spend: 00:00:02.0312500
Push ENTER to return commandline...
G:\myproject\c# inside\chap02>boxing2
Start: 2003-9-16 22:58:51
End: 2003-9-16 22:58:53
Spend: 00:00:02
Push ENTER to return commandline...
G:\myproject\c# inside\chap02>boxing2
Start: 2003-9-16 22:59:00
End: 2003-9-16 22:59:02
Spend: 00:00:02
Push ENTER to return commandline...
G:\myproject\c# inside\chap02>boxing2
Start: 2003-9-16 22:59:11
End: 2003-9-16 22:59:13
Spend: 00:00:02.0312500
Push ENTER to return commandline...
实例二说明:boxing1.cs的循环中包含一次装箱和一次拆箱(这里我均忽略两个程序打印时的装箱操作),boxing2.cs则没有相应的操作。当循环次数足够大的时候,性能差异是明显的。再次提醒你别忘了ILDASM.EXE这个工具哦,分别看看,才能一窥程序的本质。否则,粗看程序boxing2.cs比boxing1.cs多了不少代码,更多了一个5000000(5M)的循环,就以为boxing2会更慢。。。
另外一方面,装箱和拆箱对性能的影响更偏重于大型的程序和软件,这就是我用这么多循环的原因。但你能保证你不会进行大批量的数据处理吗?
MSDN上有更实用的例子:统计大量的英文单词,当然也更加复杂,故不在此详细讲解。http://www.microsoft.com/china/msdn/voices/csharp03152001.asp
文章的结尾处,我想你应该测试一下你对装箱和拆箱的理解:(同样来自MSDN)
看看各种方案中是否进行了装箱和拆箱的操作,各有多少次。
// 方案 1
int total = 35;
DateTime date = DateTime.Now;
string s = String.Format("Your total was {0} on {1}", total, date);
// 方案 2
Hashtable t = new Hashtable();
t.Add(0, "zero");
t.Add(1, "one");
// 方案 3
DateTime d = DateTime.Now;
String s = d.ToString();
// 方案 4
int[] a = new int[2];
a[0] = 33;
// 方案 5
ArrayList a = new ArrayList();
a.Add(33);
// 方案 6
MyStruct s = new MyStruct(15);
IProcess ip = (IProcess) s;
ip.Process();
今天就到这里吧,我也是初学,望多多指教。什么?上面测试的标答?呵呵,你应该找得到的,找不到?我会贴在评论中。
jiangt1980 (2003-10-14 13:32:37)
究竟装箱、拆箱是体高性能还是降低性能,第二个例子我糊涂了
eshusheng (2003-9-30 9:52:56)
to:zerozhao 对不起,解释这两段程序的失误了,第二段程序没有计算赋值,开始我测试的时候,是为了保证公平性。 这里说明一下,上面说的多5M次循环是有误的。谢谢zerozhao指正。
zerozhao (2003-9-28 23:58:19)
实例2的开始时间为什么不从给strList赋值之前开始
eshusheng (2003-9-19 19:18:21)
关于这段代码,是这样解释的:包装的目的之一是实现对值类型参数的虚函数调用。ToString() 是对象的虚函数,所以,看起来在调用 ToString() 时,d 将被包装。但是在转换对象时没有使用 d,所以不需要进行包装。编译器知道类型为 DateTime 的变量只能为该类型(因为没有导出的值类型,所以该变量不能为导出类型),所以它可以直接调用 DateTime.ToString(),并设置“这个”引用,使其指向堆栈中的 d。 所以你的理解是对的,并没有装箱。
chengdong77 (2003-9-19 10:39:38)
有个问题要问你,如果同是原类形转换时需要装箱和拆箱吗如 DateTime d = DateTime.Now; String s = d.ToString(); d、s都是引用类型,转换需要装箱和拆箱吗?
chengdong77 (2003-9-19 10:34:33)
谢谢你,让我知道了“隐示转换”和“显示转换”会给程序带来如此大的影响。
eshusheng (2003-9-17 21:25:54)
谢谢楼上的评论。确切的说理解了装箱和拆箱,对编制优秀的程序有一定的帮助。从而略微的可以改善程序的性能。
cnswdevnet (2003-9-17 14:58:00)
"至于为什么要理解装箱和拆箱?则总是一句话带过:优化程序的性能云云。" -- 是优化性能么? 不对吧.