.Net填坑 C#中的按引用传递与按值传递

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#中应用类型变量本身就是指针的包装,所以按引用对托管堆对象操作时,会有两次取地址操作。

相关推荐
pchmi10 分钟前
C# OpenCV机器视觉:图像旋转(让生活的角度更美好!)
opencv·c#·生活
慧都小妮子16 分钟前
.NET 9微软新平台 + FastReport .NET:如何提升报告生成效率
microsoft·.net·报表控件·fastreport·.net9
SEO-狼术16 分钟前
DevExpress 24.2 release Crack,full .NET 9 support
.net
Jeffrey~~18 分钟前
.Net_比对Json文件是否一致
c#·json·.net·.netcore
冒泡P1 小时前
【Lua热更新】下篇 -- 更新中
开发语言·unity·c#·游戏引擎·lua
weixin_497845543 小时前
.NET Runtime 是什么?
.net
编程乐趣7 小时前
一款可以替代Navicat的数据库管理工具
数据库·c#
sukalot9 小时前
windows C#-如何实现和调用自定义扩展方法
开发语言·c#
坐井观老天10 小时前
使用C#在目录层次结构中搜索文件以查找目标字符串
开发语言·c#
军训猫猫头14 小时前
16.初识接口2.0 C#
开发语言·c#