• C#语法糖系列 —— 第二篇:聊聊 ref,in 修饰符底层玩法


    自从 C# 7.3 放开 ref 之后,这玩法就太花哨了,也让 C# 这门语言变得越来越多范式,越来越重,这篇我们就来聊聊 ref,本质上来说 ref 的放开就是把 C/C++ 指针的那一套又拿回来了,而且还封装成一套自己的玩法,下面一一解读下。

    一:方法参数上的 ref

    我想设计者的初心把 ref 的功能限制的死死的,可能也考虑到 C# 是一门面向业务开发的语言,讲究的是做项目快狠准,性能反而不是第一要素,这个时候的 ref 很简单,看一下代码:

    
        class Program
        {
            static void Main(string[] args)
            {
                long price = 0;
    
                GetPrice(ref price);
    
                Console.WriteLine($"output: price={price}");
            }
    
            public static void GetPrice(ref long price)
            {
                price = 10;
            }
        }
    
    output: price=10
    
    

    我相信很有朋友都知道,方法参数中的 ref long price 拿的是栈地址,对栈地址上的值进行修改,自然就修改了指向这些地址上的变量,和引用类型原理一致,接下来我们从汇编角度去验证,在 Price 方法上下一个断点。

    
    D:\net5\ConsoleApp4\ConsoleApp3\Program.cs @ 16:
    026b048e 8d4dec          lea     ecx,[ebp-14h]
    026b0491 ff15a0ebc800    call    dword ptr ds:[0C8EBA0h] (ConsoleApp3.Program.GetPrice(Int64 ByRef), mdToken: 06000002)
    026b0497 90              nop
    0:000> bp 026b0491
    0:000> g
    Breakpoint 1 hit
    ChangeEngineState
    eax=00000000 ebx=0057f354 ecx=0057f2d4 edx=783aaa50 esi=02979e7c edi=0057f2dc
    eip=026b0491 esp=0057f2c4 ebp=0057f2e8 iopl=0         nv up ei pl zr na pe nc
    cs=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00000246
    026b0491 ff15a0ebc800    call    dword ptr ds:[0C8EBA0h] ds:002b:00c8eba0=00c2be10
    
    

    从汇编的 lea ecx,[ebp-14h] 就能看到,将 ebp-14 这个单元的内存地址给了 ecx,这个 ecx 也就是作为参数传递给了 Price 方法,后续的赋值将会影响这个栈位置 上的内容。

    2. 方法返回值上的 ref

    这就有意思了,进入的时候传地址,回来的时候也想传地址,很显然方法线程栈上的 值类型 是传不出去的,毕竟方法返回后,esp,ebp 所控制的方法栈帧空间是要销毁的,所以只能是堆上对象才能实现。

    为了方便理解,看如下代码:

    
        class Program
        {
            static void Main(string[] args)
            {
                ref long price = ref GetCurrentPrice();
    
                price = 12;
    
                Console.WriteLine($"output: price={price}");
            }
    
            public static ref long GetCurrentPrice()
            {
                long[] nums = { 10, 20, 30 };
    
                return ref nums[1];
            }
        }
    
    output: price=12
    
    

    可以看到当前的 price=12,同时 nums 这个数组也被修改了,可以用 windbg 验证一下。

    
    0:000> !dumpheap  -type System.Int64[] 
     Address       MT     Size
    027ca7b0 04c39d00       36     
    
    Statistics:
          MT    Count    TotalSize Class Name
    04c39d00        1           36 System.Int64[]
    Total 1 objects
    0:000> dq 027ca7b0 L4
    027ca7b0  00000003`04c39d00 00000000`0000000a
    027ca7c0  00000000`0000000c 00000000`0000001e
    
    

    可以看到上面的 000000000000000c 被修改成 price=12 ,这时候有人就不爽了,我不希望外面的代码能修改 price 内容,那怎么办呢? 还得在 ref 后面加上 readonly ,改造后如下:

    到此时写法就有点疯狂了,对 C# 开发者来说很难理解,对熟悉 C/C++ 指针的朋友来说又很不习惯,太纠结了,下面是一段翻译过来的 C/C++指针代码

    
    const long long* getcurrentprice();
    
    int main()
    {
    	int i = 0;
    
    	const long long* price = getcurrentprice();
    
    	price = 12;
    
    	printf("num=%d, price=%d \n", i, *price);
    
    }
    
    const long long* getcurrentprice() {
    
    	long long* num = new long long[3]{ 10,20,30 };
    	return num + 1;
    }
    
    

    说实话,这代码看起来就清爽多了。

    2. 对 ref 变量的 in 操作

    这又是一套 C/C++ 的玩法,有时候不希望某一个方法对 ref 变量进行修改,注意:是不希望某一个方法进行修改,其他方法是可以的,那这个怎么实现呢?这就需要在入参上加 in 前缀,把代码修改一下。

    
        class Program
        {
            static void Main(string[] args)
            {
                ref long price = ref GetCurrentPrice();
    
                ModifyPrice(in price);
    
                Console.WriteLine($"output: price={price}");
            }
    
            public static ref long GetCurrentPrice()
            {
                long[] nums = { 10, 20, 30 };
    
                return ref nums[1];
            }
    
            public static void ModifyPrice(in long price)
            {
                price = 12;
                Console.WriteLine(price);
            }
        }
    
    

    可以看到,这时候报错了,如果换成 C++ 就很简单了,只需要在参数上把 in 改成 const 即可。

    
    void modifyprice(const long long* price) {
    	*price = 12;
    	printf("%d", *price);
    }
    
    

    总的来说,ref 这一套玩法太另类了 🤣🤣🤣

    图片名称
  • 相关阅读:
    oauth2.0授权码模式详解
    MySQL基础
    QT----写完的程序打包为APK在自己的手机上运行
    SSM--关联关系映射
    springboot高校学生宿舍水电费报修考勤管理系统
    Python自动读取PDF,推荐用pdfplumber库!
    Git创建干净分支,本地操作不依赖任何分支
    424. 替换后的最长重复字符 ●●
    【Flutter 面试题】main()和runApp()函数在Flutter的作用分别是什么?有什么关系吗?
    各省份12.5m地形数据
  • 原文地址:https://www.cnblogs.com/huangxincheng/p/16188769.html