2008-07-13 13:47
横刀天笑
阅读(3105)
评论()
编辑
收藏
举报
装箱、转型、方法调用这些我们天天进行的日常工作之前到底有什么差别?
string aString = (string)objString;
string bString = objString.ToString();
之间到底有什么不同?
为什么那些类都要实现IConvertible接口?
1
struct UserInfoStruct
2

{
3
public int UserId;
4
public string UserName;
5
}
6
class UserInfoClass
7

{
8
private int UserId;
9
private string UserName;
10
}
11
class Program
12

{
13
14
static void Main(string[] args)
15
{
16
object objString = "abc";
17
18
string aString = (string)objString;
19
string bString = objString.ToString();
20
string cString = Convert.ToString(objString);
21
22
object objInt = 5;
23
int aInt = (int)objInt;
24
int bInt = Convert.ToInt32(objInt);
25
26
object objStruct = new UserInfoStruct();
27
UserInfoStruct aUserInfoStruct = (UserInfoStruct)objStruct;
28
29
object objClass = new UserInfoClass();
30
UserInfoClass aUserInfoClass = (UserInfoClass)objClass;
31
}
32
}
前几天在群里聊天,有人问
string aString = (string)objString;
string bString = objString.ToString();
有什么区别,我当时就回答“一个是转型、一个是方法调用”,刚说完就觉得自己是在说废话,其实我也不知道内部到底发生了什么,如是就reflector,ILDASM,google一起上,现在把弄出来的结果整理了一下,share出来,并把相似的几个都集在一起讨论,由于我不懂WinDbg,所以无法深入,就浅尝辄止吧。
下面是main方法的IL代码:
1
.method private hidebysig static void Main(string[] args) cil managed
2
{
3
.entrypoint
4
// Code size 97 (0x61)
5
.maxstack 1
6
.locals init ([0] object objString,
7
[1] string aString,
8
[2] string bString,
9
[3] string cString,
10
[4] object objInt,
11
[5] int32 aInt,
12
[6] int32 bInt,
13
[7] object objStruct,
14
[8] valuetype SomeKits.UserInfoStruct aUserInfoStruct,
15
[9] object objClass,
16
[10] class SomeKits.UserInfoClass aUserInfoClass,
17
[11] valuetype SomeKits.UserInfoStruct CS$0$0000)
18
IL_0000: nop
19
IL_0001: ldstr "abc"
20
IL_0006: stloc.0
21
IL_0007: ldloc.0
22
IL_0008: castclass [mscorlib]System.String
23
IL_000d: stloc.1
24
IL_000e: ldloc.0
25
IL_000f: callvirt instance string [mscorlib]System.Object::ToString()
26
IL_0014: stloc.2
27
IL_0015: ldloc.0
28
IL_0016: call string [mscorlib]System.Convert::ToString(object)
29
IL_001b: stloc.3
30
IL_001c: ldc.i4.5
31
IL_001d: box [mscorlib]System.Int32
32
IL_0022: stloc.s objInt
33
IL_0024: ldloc.s objInt
34
IL_0026: unbox.any [mscorlib]System.Int32
35
IL_002b: stloc.s aInt
36
IL_002d: ldloc.s objInt
37
IL_002f: call int32 [mscorlib]System.Convert::ToInt32(object)
38
IL_0034: stloc.s bInt
39
IL_0036: ldloca.s CS$0$0000
40
IL_0038: initobj SomeKits.UserInfoStruct
41
IL_003e: ldloc.s CS$0$0000
42
IL_0040: box SomeKits.UserInfoStruct
43
IL_0045: stloc.s objStruct
44
IL_0047: ldloc.s objStruct
45
IL_0049: unbox.any SomeKits.UserInfoStruct
46
IL_004e: stloc.s aUserInfoStruct
47
IL_0050: newobj instance void SomeKits.UserInfoClass::.ctor()
48
IL_0055: stloc.s objClass
49
IL_0057: ldloc.s objClass
50
IL_0059: castclass SomeKits.UserInfoClass
51
IL_005e: stloc.s aUserInfoClass
52
IL_0060: ret
53
}
将IL代码和源代码比较得知
string aString = (string)objString;的IL代码是 castclass [mscorlib]System.String
这个过程发生了什么?首先在这个指令之前ldloc.0是将第一个局部变量的引用压入堆栈中,然后从堆栈顶上弹出对象的引用,将这个引用转型为这个指令指定的类型,如果转型成功的话将转型的结果压入栈顶。那什么情况下转型成功,什么情况下转型将不成功呢?当这个栈顶的对象不是期望的类的子类的话那就转型失败了,就会抛出InvalidCastException异常。那如果栈顶的对象是null怎么办?会触发异常么?答案是不会,如果栈顶上的元素是null的时候,转型结果也是null,不会引发什么异常。
对于string bString = objString.ToString()就没有什么好说的了,从生成的代码callvirt instance string [mscorlib]System.Object::ToString()来看,它调用了object的ToString()方法,使用的是callvirt指令,那实际上调用的是string类里面重写object的那个ToString()。
string cString = Convert.ToString(objString)这种形式在内部到底发生了什么呢?我们看看Convert类的ToString(object)静态方法的实现:
public static string ToString(object value)


{
return ToString(value, null);
}

public static string ToString(object value, IFormatProvider provider)


{
IConvertible convertible = value as IConvertible;
if (convertible != null)

{
return convertible.ToString(provider);
}
IFormattable formattable = value as IFormattable;
if (formattable != null)

{
return formattable.ToString(null, provider);
}
if (value != null)

{
return value.ToString();
}
return string.Empty;
}

在Convert.ToString()方法里,首先将对象尝试转型为IConvertible接口,如果转型成功就会调用这个接口的ToString()方法了,所以你想想,如果我们要让我们自己写的类型支持Convert.ToString()这种写法怎么办?那就实现IConvertible接口吧。
object objInt = 5这个又发生了什么?它对应的IL指令是:box [mscorlib]System.Int32,box是装箱指令,具体分三步进行:
1.在托管堆上分配一块内存,内存的大小是值类型的大小然后加上两个所有引用类型都有的附加字段:SyncBlockIndex和一个放发表指针
2.将栈上的值类型拷贝到刚才申请的类型中
3.返回刚在托管堆上申请的对象引用,将其压入栈
从这里看装箱不仅仅耗费内存还将东西拷贝来拷贝去的,真是赔了夫人又折兵啊。
int aInt = (int)objInt又干了些什么呢?还是类型转换么?它对应的IL代码是
unbox.any [mscorlib]System.Int32
这个称之为拆箱,顾名思义就是将刚才的已装箱类型给“转换”为未装箱时候的值类型,从这个层面看拆箱好像是装箱的“逆过程”,实际上却不是,拆箱是通过这样的两步进行的:
1.从栈上获取托管堆中已装箱对象的地址
2.从已装箱对象中获取刚才那个拷贝过去的值类型的地址
看到没,拆箱比起装箱起来少了一步,这里并没有将已装箱类型中的值类型拷贝到栈上,看起来拆箱并没有涉及到内存的拷贝操作,它做的仅仅是做一下地址的提取,但是实际中拆箱后往往紧跟着的就是内存的拷贝。从上面的代码中我们可以看到装箱和拆箱是很消耗资源的操作,所以我们需要特别注意,特别是一些隐式的,我们常常忽略了。
按照上一小节的结论,string cString = Convert.ToString(objString)能够编译通过是因为int类型实现了IConvertible接口,通过Reflector查看代码果真如此。
上面是对.net基元类型的一些讨论,那么对于自己写的struct和class是怎样的呢?
通过IL代码,可知对于值类型的struct
object objStruct = new UserInfoStruct();
UserInfoStruct aUserInfoStruct = (UserInfoStruct)objStruct;
就是装箱拆箱的过程
对于引用类型的class UserInfoClass aUserInfoClass = (UserInfoClass)objClass就是castclass指令的操作。
由于本人对WinDbg一无所知,所以也无法在更深一层次讨论这些机制的最底层实现,实属遗憾,希望能有一些达人对底层做进一步解释。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· .NET周刊【3月第1期 2025-03-02】
· 分享 3 个 .NET 开源的文件压缩处理库,助力快速实现文件压缩解压功能!
· Ollama——大语言模型本地部署的极速利器