C# 中的 ref 已经被放开,或许你已经不认识了

一:背景

1. 讲故事

最近在翻 netcore 源码看,发现框架中有不少的代码都被 ref 给修饰了,我去,这还是我认识的 ref 吗?就拿 Span 来说,代码如下:


    public readonly ref struct Span<T>
    {
        public ref T GetPinnableReference()
        {
            ref T result = ref Unsafe.AsRef<T>(null);
            if (_length != 0)
            {
                result = ref _pointer.Value;
            }
            return ref result;
        }

        public ref T this[int index]
        {
            get
            {
                return ref Unsafe.Add(ref _pointer.Value, index);
            }
        }             
    }

是不是到处都有 ref,在 struct 上有,在 local variable 也有,在 方法签名处 也有,在 方法调用处 也有,在 属性 上也有, 在 return处 也有,简直是应有尽有,太????啦,那这一篇我们就来聊聊这个奇葩的 ref。

二:ref 各场景下的代码解析

1. 动机

不知道大家有没有发现,在 C# 7.0 之后,语言团队对性能这一块真的是前所未有的重视,还专门为此出了各种类和底层支持,比如说 Span, Memory,ValueTask,还有本篇要介绍的ref。

在大家传统的认知中 ref 是用在方法参数上,用于给 值类型 做引用传值,一个是为了大家业务上需要多次原地修改的情况,二个是为了避免值类型的copy引发的性能开销,不知道是哪一位大神脑洞大开,将 ref 应用在你所知道的代码各处,最终目的都是尽可能的提升性能。

2. ref struct 分析

从小就被教育 值类型分配在栈上,引用类型是在堆上,这话也是有问题的,因为值类型也可以分配在堆上,比如下面代码的 Location。


    public class Program
    {
        public static void Main(string[] args)
        {
            var person = new Person() { Name = "张三", Location = new Point() { X = 10, Y = 20 } };

            Console.ReadLine();
        }
    }

    public class Person
    {
        public string Name { get; set; }

        public Point Location { get; set; }  //分配在堆上
    }

    public struct Point
    {
        public int X { get; set; }
        public int Y { get; set; }
    }

其实这也是很多新手朋友学习值类型疑惑的地方,可以用 windbg 到托管堆找一下 Person 问问看,如下代码:


0:000> !dumpheap -type Person
         Address               MT     Size
0000010e368aadb8 00007ffaf50c2340       32     

0:000> !do 0000010e368aadb8
Name:        ConsoleApp2.Person
MethodTable: 00007ffaf50c2340
EEClass:     00007ffaf50bc5e8
Size:        32(0x20) bytes
File:        E:\net5\ConsoleApp1\ConsoleApp2\bin\Debug\netcoreapp3.1\ConsoleApp2.dll
Fields:
              MT    Field   Offset                 Type VT     Attr            Value Name
00007ffaf5081e18  4000001        8        System.String  0 instance 0000010e368aad98 <Name>k__BackingField
00007ffaf50c22b0  4000002       10    ConsoleApp2.Point  1 instance 0000010e368aadc8 <Location>k__BackingField

0:000> dp 0000010e368aadc8
0000010e`368aadc8  00000014`0000000a 00000000`00000000

上面代码最后一行 00000014`0000000a 中的 14 和 a 就是 y 和 x 的值,稳稳当当的存放在堆中,如果你还不信就看看 gc 0代堆的范围。


0:000> !eeheap -gc
Number of GC Heaps: 1
generation 0 starts at 0x0000010E368A1030
generation 1 starts at 0x0000010E368A1018
generation 2 starts at 0x0000010E368A1000
ephemeral segment allocation context: none
         segment             begin         allocated              size
0000010E368A0000  0000010E368A1000  0000010E368B55F8  0x145f8(83448)

从最后一行可看出,刚才的 0000010e368aadc8 确实是在 0 代堆 0x0000010E368A1030 - 0000010E368B55F8 的范围内。

接下来的问题就是能不能给 struct 做一个限制,就像泛型约束一样,不准 struct 分配在堆上,有没有办法呢? 办法就是加一个 ref 限定即可,如下图:

从错误提示中可以看出,有意让 struct 分配到堆上的操作都是严格禁止的,要想过编译器只能将 class person 改成 ref struct person,也就是文章开头 Span 和 this[int index] 这样,动机可想而知,一切都是为了性能。

3. ref method 分析

给方法的参数传引用地址,我想很多朋友都已经轻车熟路了,比如下面这样:


        public static int GetNum(ref int i)
        {
            return i;
        }

现在大家可以试着跳出思维定势,既然可以往方法内仍 引用地址 ,那能不能往方法外抛 引用地址 呢? 如果这也能实现就比较有意思了,我可以对集合内的某一些数据进行引用地址返回,在方法外照样可以修改这些返回值,毕竟传来传去都是引用地址,如下代码所示:


    public class Program
    {
        public static void Main(string[] args)
        {
            var nums = new int[3] { 10, 20, 30 };

            ref int num = ref GetNum(nums);

            num = 50;

            Console.WriteLine($"nums= {string.Join(",",nums)}");

            Console.ReadLine();
        }

        public static ref int GetNum(int[] nums)
        {
            return ref nums[2];
        }
    }

可以看到,数组的最后一个值已经由 30 -> 50 了,有些朋友可能会比较惊讶,这到底是怎么玩的,不用想就是引用地址到处漂,不信的话,看看 IL 代码咯。


.method public hidebysig static 
    int32& GetNums (
        int32[] nums
    ) cil managed 
{
    // Method begins at RVA 0x209c
    // Code size 13 (0xd)
    .maxstack 2
    .locals init (
        [0] int32&
    )

    // {
    IL_0000: nop
    // return ref nums[2];
    IL_0001: ldarg.0
    IL_0002: ldc.i4.2
    IL_0003: ldelema [System.Runtime]System.Int32
    IL_0008: stloc.0
    // (no C# code)
    IL_0009: br.s IL_000b

    IL_000b: ldloc.0
    IL_000c: ret
} // end of method Program::GetNums

.method public hidebysig static 
    void Main (
        string[] args
    ) cil managed 
{
    IL_0013: ldloc.0
    IL_0014: call int32& ConsoleApp2.Program::GetNums(int32[])
    IL_0019: stloc.1
    IL_001a: ldloc.1
    IL_001b: ldc.i4.s 50
    IL_003e: pop
    IL_003f: ret
} // end of method Program::Main


可以看到,到处都是 & 取值运算符,更直观一点的话用 windbg 看一下。


0:000> !clrstack -a
OS Thread Id: 0x7040 (0)
000000D4E777E760 00007FFAF1C5108F ConsoleApp2.Program.Main(System.String[]) [E:\net5\ConsoleApp1\ConsoleApp2\Program.cs @ 28]
    PARAMETERS:
        args (0x000000D4E777E7F0) = 0x00000218c9ae9e60
    LOCALS:
        0x000000D4E777E7C8 = 0x00000218c9aeadd8
        0x000000D4E777E7C0 = 0x00000218c9aeadf0

0:000> dp 0x00000218c9aeadf0
00000218`c9aeadf0  00000000`00000032 00000000`00000000

上面代码处的 0x00000218c9aeadf0 就是 num 的引用地址,继续用 dp 看一下这个地址上的值为 16进制的32,也就是十进制的 50 哈。

三:总结

总的来说,netcore 就是在当初盛行的 云计算 和 虚拟化 时代诞生,基因和使命促使它必须要优化优化再优化,再小的蚂蚁也是肉,最后就是 C# 大法 ????

更多高质量干货:参见我的 GitHub: dotnetfly

?著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,172评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,346评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事?!?“怎么了?”我有些...
    开封第一讲书人阅读 159,788评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,299评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,409评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,467评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,476评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,262评论 0 269
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,699评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,994评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,167评论 1 343
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,827评论 4 337
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,499评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,149评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,387评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,028评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,055评论 2 352