自从 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 这一套玩法太另类了

C#语法糖系列 —— 第二篇:聊聊 ref,in 修饰符底层玩法的更多相关文章

  1. C#语法糖之第二篇: 参数默认值和命名参数 对象初始化器与集合初始化器

    今天继续写上一篇文章C#4.0语法糖之第二篇,在开始今天的文章之前感谢各位园友的支持,通过昨天写的文章,今天有很多园友们也提出了文章中的一些不足,再次感谢这些关心我的园友,在以后些文章的过程中不断的完 ...

  2. C#语法糖系列 —— 第一篇:聊聊 params 参数底层玩法

    首先说说为什么要写这个系列,大概有两点原因. 这种文章阅读量确实高... 对 IL 和 汇编代码 的学习巩固 所以就决定写一下这个系列,如果大家能从中有所收获,那就更好啦! 一:params 应用层玩 ...

  3. 聊聊 C# 方法重载的底层玩法

    最近在看 C++ 的方法重载,我就在想 C# 中的重载底层是怎么玩的,很多朋友应该知道 C 是不支持重载的,比如下面的代码就会报错. #include <stdio.h> int say( ...

  4. 深入理解javascript函数系列第二篇——函数参数

    × 目录 [1]arguments [2]内部属性 [3]函数重载[4]参数传递 前面的话 javascript函数的参数与大多数其他语言的函数的参数有所不同.函数不介意传递进来多少个参数,也不在乎传 ...

  5. 前端工程师技能之photoshop巧用系列第二篇——测量篇

    × 目录 [1]测量信息 [2]实战 [3]注意事项 前面的话 前端工程师使用photoshop进行的大量工作实际上是测量.本文是photoshop巧用系列第二篇——测量篇 测量信息 在网页制作中需要 ...

  6. 深入理解javascript作用域系列第二篇——词法作用域和动态作用域

    × 目录 [1]词法 [2]动态 前面的话 大多数时候,我们对作用域产生混乱的主要原因是分不清楚应该按照函数位置的嵌套顺序,还是按照函数的调用顺序进行变量查找.再加上this机制的干扰,使得变量查找极 ...

  7. 深入理解javascript作用域系列第二篇

    前面的话 大多数时候,我们对作用域产生混乱的主要原因是分不清楚应该按照函数位置的嵌套顺序,还是按照函数的调用顺序进行变量查找.再加上this机制的干扰,使得变量查找极易出错.这实际上是由两种作用域工作 ...

  8. C#语法糖系列 —— 第三篇:聊聊闭包的底层玩法

    有朋友好奇为什么将 闭包 归于语法糖,这里简单声明下,C# 中的所有闭包最终都会归结于 类 和 方法,为什么这么说,因为 C# 的基因就已经决定了,如果大家了解 CLR 的话应该知道, C#中的类最终 ...

  9. [ 高并发]Java高并发编程系列第二篇--线程同步

    高并发,听起来高大上的一个词汇,在身处于互联网潮的社会大趋势下,高并发赋予了更多的传奇色彩.首先,我们可以看到很多招聘中,会提到有高并发项目者优先.高并发,意味着,你的前雇主,有很大的业务层面的需求, ...

随机推荐

  1. @Controller 注解?

    该注解表明该类扮演控制器的角色,Spring不需要你继承任何其他控制器基类或引用Servlet API.

  2. maven常用命令含义

    今天在开发过程中,对一个mapper.xml文件的sql进行了改动,重启tomcat后发现没有生效,首先考虑是不是远程服务开启着,导致代码没有走本地,确认远程服务是关闭的,的确是本地修改没有生效,于是 ...

  3. Python - 文档格式转换(CSV与JSON)

  4. Docker镜像构建之docker commit

    我们可以通过公共仓库拉取镜像使用,但是,有些时候公共仓库拉取的镜像并不符合我们的需求.尽管已经从繁琐的部署工作中解放出来了,但是在实际开发时,我们可能希望镜像包含整个项目的完整环境,在其他机器上拉取打 ...

  5. TOP 10 开源的推荐系统简介

      最 近这两年推荐系统特别火,本文搜集整理了一些比较好的开源推荐系统,即有轻量级的适用于做研究的SVDFeature.LibMF.LibFM等,也有重 量级的适用于工业系统的 Mahout.Oryx ...

  6. 4.1 ROS元功能包

    4.1 ROS元功能包 场景:完成ROS中一个系统性的功能,可能涉及到多个功能包,比如实现了机器人导航模块,该模块下有地图.定位.路径规划...等不同的子级功能包.那么调用者安装该模块时,需要逐一的安 ...

  7. 51单片机头文件reg51.h详解

    转自:http://www.51hei.com/mcu/2670.html 我们在用c语言编程时往往第一行就是头文件,51单片机为reg51.h或reg52.h,51单片机相对来说比较简单,头文件里面 ...

  8. 顺利通过EMC实验(12)

  9. 纯干货数学推导_傅里叶级数与傅里叶变换_Part3_周期为2L的函数展开

  10. onsubmit阻止表单提交

    在实际开发中往往会遇到检查表单数据的合法性,如果数据不合法,就不让其提交. <!DOCTYPE html> <html> <head> <meta chars ...