C#中的按引用传递与按值传递
一、测试代码
csharp
class Program
{
static void Main(string[] args)
{
int a = 10;
TestRef(ref a);
TestA(a);
}
static void TestA(int a)
{
a = 5;
}
static void TestRef(ref int a)
{
a = 5;
}
}
二、反汇编分析
1.调用处反汇编
代码如下(示例):
c
int a = 10;
00007FF9676B41D3 mov dword ptr [rbp+28h],0Ah
TestRef(ref a);
00007FF9676B41DA lea rcx,[rbp+28h]
00007FF9676B41DE call CLRStub[MethodDescPrestub]@7ff96750e3f0 (07FF96750E3F0h)
00007FF9676B41E3 nop
TestA(a);
00007FF9676B41E4 mov ecx,dword ptr [rbp+28h]
00007FF9676B41E7 call CLRStub[MethodDescPrestub]@7ff96750e3e8 (07FF96750E3E8h)
可以看到与C++中按引用与按值传递一样,一个传递的是变量的地址,一个传递的是变量的值
2.被调用处的反汇编
按引用处理函数代码如下(示例):
c
static void TestRef(ref int a)
{
00007FF9676B4DD0 push rbp
00007FF9676B4DD1 push rdi
00007FF9676B4DD2 push rsi
00007FF9676B4DD3 sub rsp,20h
00007FF9676B4DD7 mov rbp,rsp
00007FF9676B4DDA mov qword ptr [rbp+40h],rcx
00007FF9676B4DDE cmp dword ptr [7FF9675BAD00h],0
00007FF9676B4DE5 je Program.TestRef(Int32 ByRef)+01Ch (07FF9676B4DECh)
00007FF9676B4DE7 call 00007FF9C71BD1C0
00007FF9676B4DEC nop
a = 5;
00007FF9676B4DED mov rax,qword ptr [rbp+40h]
00007FF9676B4DF1 mov dword ptr [rax],5
按引用处理,与C++中一样,赋值都是以两条指令,用地址的方式直接对main函数中的本地变量地址处的值进行修改。
按值传递处理如下:
c
static void TestA(int a)
{
00007FF9676C4E50 push rbp
00007FF9676C4E51 push rdi
00007FF9676C4E52 push rsi
00007FF9676C4E53 sub rsp,20h
00007FF9676C4E57 mov rbp,rsp
00007FF9676C4E5A mov dword ptr [rbp+40h],ecx
00007FF9676C4E5D cmp dword ptr [7FF9675CAD00h],0
00007FF9676C4E64 je Program.TestA(Int32)+01Bh (07FF9676C4E6Bh)
00007FF9676C4E66 call 00007FF9C71BD1C0
00007FF9676C4E6B nop
a = 5;
00007FF9676C4E6C mov dword ptr [rbp+40h],5
可以看到也与C++中处理一样,用mov指令直接操作本地变量赋值
二、引用类型的反汇编分析
测试代码:
csharp
class Program
{
class A
{
public int intTest = 10;
}
static void Main(string[] args)
{
A a = new A();
TestRef(ref a);
TestA(a);
}
static void TestA(A a)
{
a.intTest = 5;
}
static void TestRef(ref A a)
{
a.intTest = 5;
}
反汇编查看:
c
TestRef(ref a);
00007FF963E847CB lea rcx,[rbp+28h]
00007FF963E847CF call CLRStub[MethodDescPrestub]@7ff963cde3f0 (07FF963CDE3F0h)
00007FF963E847D4 nop
TestA(a);
00007FF963E847D5 mov rcx,qword ptr [rbp+28h]
00007FF963E847D9 call CLRStub[MethodDescPrestub]@7ff963cde3e8 (07FF963CDE3E8h)
函数处理:
按引用:
csharp
static void TestRef(ref A a)
{
00007FF963E85420 push rbp
00007FF963E85421 push rdi
00007FF963E85422 push rsi
00007FF963E85423 sub rsp,20h
00007FF963E85427 mov rbp,rsp
00007FF963E8542A mov qword ptr [rbp+40h],rcx
00007FF963E8542E cmp dword ptr [7FF963D8AD00h],0
00007FF963E85435 je Program.TestRef(A ByRef)+01Ch (07FF963E8543Ch)
00007FF963E85437 call 00007FF9C397D1C0
00007FF963E8543C nop
a.intTest = 5;
00007FF963E8543D mov rax,qword ptr [rbp+40h]
00007FF963E85441 mov rax,qword ptr [rax]
00007FF963E85444 mov dword ptr [rax+8],5
按值:
c
static void TestA(A a)
{
00007FF963E85470 push rbp
00007FF963E85471 push rdi
00007FF963E85472 push rsi
00007FF963E85473 sub rsp,20h
00007FF963E85477 mov rbp,rsp
00007FF963E8547A mov qword ptr [rbp+40h],rcx
00007FF963E8547E cmp dword ptr [7FF963D8AD00h],0
00007FF963E85485 je Program.TestA(A)+01Ch (07FF963E8548Ch)
00007FF963E85487 call 00007FF9C397D1C0
00007FF963E8548C nop
a.intTest = 5;
00007FF963E8548D mov rax,qword ptr [rbp+40h]
00007FF963E85491 mov dword ptr [rax+8],5
结论:
按引用传递,将引用类型变量的地址传给被调用的函数,被调用函数中对引用类型的操作,都转换为对引用类型变量地址值的操作,如上所示,调用a.intTest = 5时
c
00007FF963E8543D mov rax,qword ptr [rbp+40h]
00007FF963E85441 mov rax,qword ptr [rax]
00007FF963E85444 mov dword ptr [rax+8],5
先将本地变量中的值存到rax(指向mian函数中A a这个变量的地址 ),获取这个地址的内容(其指向托管堆中真正A实例的地址),再次放到rax中(此时rax中变成了托管堆中的实例地址),然后进行字段位置复制命令。
按值传递:
c
00007FF963E8548D mov rax,qword ptr [rbp+40h]
00007FF963E85491 mov dword ptr [rax+8],5
先将本地变量中的值存到rax(由mian传递进来的托管堆中的实例地址值的副本),直接对这个地址值进行操作就可以。
两者的区别就在于:按引用传递的是,引用类型变量本身的地址(用的时候,需要进行一次地址取值操作,得到引用变量内存着的托管推实例对象地址),按值传递,直接得到托管堆实例对象地址。
按引用传递,可以对托管对象变量上例中Main函数中的A的内容进行操作。比如实现swap(ref A a,ref A b)这种交换操作,直接操作a与b中存储的指向来实现交换。
从IL指令中看区别
csharp
.method private hidebysig static void
Main(
string[] args
) cil managed
{
.entrypoint
.maxstack 1
.locals init (
[0] class Program/A a
)
IL_0000: nop
// [16 5 - 16 34]
IL_0001: newobj instance void Program/A::.ctor()
IL_0006: stloc.0 // a
// [17 5 - 17 27]
IL_0007: ldloca.s a
IL_0009: call void Program::TestRef(class Program/A&)
IL_000e: nop
// [18 5 - 18 21]
IL_000f: ldloc.0 // a
IL_0010: call void Program::TestA(class Program/A)
IL_0015: nop
IL_0016: ret
}
可以看到IL指令也是有区别分别对应 ldloca.s,ldloc.0,分别就是加载本地变量的地址,与本地变量值,与汇编看到的结论相同。
再看函数内部的区别:
.method private hidebysig static void
TestA(
class Program/A a
) cil managed
{
.maxstack 8
IL_0000: nop
// [23 5 - 23 18]
IL_0001: ldarg.0 // a
IL_0002: ldc.i4.5
IL_0003: stfld int32 Program/A::intTest
IL_0008: ret
} // end of method Program::TestA
.method private hidebysig static void
TestRef(
class Program/A& a
) cil managed
{
.maxstack 8
IL_0000: nop
// [28 5 - 28 18]
IL_0001: ldarg.0 // a
IL_0002: ldind.ref
IL_0003: ldc.i4.5
IL_0004: stfld int32 Program/A::intTest
IL_0009: ret
} // end of method Program::TestRef
可以看到两者生成的IL代码也是有区别的,在于ldind.ref这个IL指令,是取地址对应的对象地址的指令,也与汇编中看到的一样。
总结
C#中的按引用与按值传递,与C++中底层原理一致,都是对变量本身传递其地址,还是传递其副本进行操作,在对这两种变量进行操作时,看起来一样的代码,a = 10,a.Test =10等,按引用会多生成一次取地址对应值的操作。备注:C#中应用类型变量本身就是指针的包装,所以按引用对托管堆对象操作时,会有两次取地址操作。