一:背景

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     Size0000010e368aadb8 00007ffaf50c2340       32     0:000> !do 0000010e368aadb8Name:        ConsoleApp2.PersonMethodTable: 00007ffaf50c2340EEClass:     00007ffaf50bc5e8Size:        32(0x20) bytesFile:        E:\net5\ConsoleApp1\ConsoleApp2\bin\Debug\netcoreapp3.1\ConsoleApp2.dllFields:              MT    Field   Offset                 Type VT     Attr            Value Name00007ffaf5081e18  4000001        8        System.String  0 instance 0000010e368aad98 <Name>k__BackingField00007ffaf50c22b0  4000002       10    ConsoleApp2.Point  1 instance 0000010e368aadc8 <Location>k__BackingField0:000> dp 0000010e368aadc80000010e`368aadc8  00000014`0000000a 00000000`00000000

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

0:000> !eeheap -gcNumber of GC Heaps: 1generation 0 starts at 0x0000010E368A1030generation 1 starts at 0x0000010E368A1018generation 2 starts at 0x0000010E368A1000ephemeral segment allocation context: none         segment             begin         allocated              size0000010E368A0000  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 -aOS 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 = 0x00000218c9aeadf00:000> dp 0x00000218c9aeadf000000218`c9aeadf0  00000000`00000032 00000000`00000000

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

三:总结

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

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