佛少 發表於 2020-11-7 09:21:00

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

<h2 id="一背景">一:背景</h2>
<h3 id="1-讲故事">1. 讲故事</h3>
<p>最近在翻 netcore 源码看,发现框架中有不少的代码都被 ref 给修饰了,我去,这还是我认识的 ref 吗?就拿 Span 来说,代码如下:</p>
<pre><code class="language-C#">
    public readonly ref struct Span&lt;T&gt;
    {
      public ref T GetPinnableReference()
      {
            ref T result = ref Unsafe.AsRef&lt;T&gt;(null);
            if (_length != 0)
            {
                result = ref _pointer.Value;
            }
            return ref result;
      }

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

</code></pre>
<p>是不是到处都有 ref,在 struct 上有,在 local variable 也有,在 方法签名处 也有,在 方法调用处 也有,在 属性 上也有, 在 return处 也有,简直是应有尽有,太🐂👃啦,那这一篇我们就来聊聊这个奇葩的 ref。</p>
<h2 id="二ref-各场景下的代码解析">二:ref 各场景下的代码解析</h2>
<h3 id="1-动机">1. 动机</h3>
<p>不知道大家有没有发现,在 C# 7.0 之后,语言团队对性能这一块真的是前所未有的重视,还专门为此出了各种类和底层支持,比如说 Span, Memory,ValueTask,还有本篇要介绍的ref。</p>
<p>在大家传统的认知中 ref 是用在方法参数上,用于给 值类型 做引用传值,一个是为了大家业务上需要多次原地修改的情况,二个是为了避免值类型的copy引发的性能开销,不知道是哪一位大神脑洞大开,将 ref 应用在你所知道的代码各处,最终目的都是尽可能的提升性能。</p>
<h3 id="2-ref-struct-分析">2. ref struct 分析</h3>
<p>从小就被教育 值类型分配在栈上,引用类型是在堆上,这话也是有问题的,因为值类型也可以分配在堆上,比如下面代码的 Location。</p>
<pre><code class="language-C#">
    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; }
    }

</code></pre>
<p>其实这也是很多新手朋友学习值类型疑惑的地方,可以用 windbg 到托管堆找一下 <code>Person</code> 问问看,如下代码:</p>
<pre><code class="language-C#">
0:000&gt; !dumpheap -type Person
         Address               MT   Size
0000010e368aadb8 00007ffaf50c2340       32   

0:000&gt; !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
00007ffaf5081e184000001      8      System.String0 instance 0000010e368aad98 &lt;Name&gt;k__BackingField
00007ffaf50c22b04000002       10    ConsoleApp2.Point1 instance 0000010e368aadc8 &lt;Location&gt;k__BackingField

0:000&gt; dp 0000010e368aadc8
0000010e`368aadc800000014`0000000a 00000000`00000000

</code></pre>
<p>上面代码最后一行 00000014`0000000a 中的 14 和 a 就是 y 和 x 的值,稳稳当当的存放在堆中,如果你还不信就看看 gc 0代堆的范围。</p>
<pre><code class="language-C#">
0:000&gt; !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
0000010E368A00000000010E368A10000000010E368B55F80x145f8(83448)

</code></pre>
<p>从最后一行可看出,刚才的0000010e368aadc8 确实是在 0 代堆 <code>0x0000010E368A1030 - 0000010E368B55F8</code> 的范围内。</p>
<p>接下来的问题就是能不能给 struct 做一个限制,就像泛型约束一样,不准 struct 分配在堆上,有没有办法呢? 办法就是加一个 ref 限定即可,如下图:</p>
<p><img src="https://img2020.cnblogs.com/other/214741/202011/214741-20201107092119382-323975077.png" alt="" loading="lazy"></p>
<p>从错误提示中可以看出,有意让 struct 分配到堆上的操作都是严格禁止的,要想过编译器只能将 class person 改成 ref struct person,也就是文章开头 Span和this 这样,动机可想而知,一切都是为了性能。</p>
<h3 id="3-ref-method-分析">3. ref method 分析</h3>
<p>给方法的参数传引用地址,我想很多朋友都已经轻车熟路了,比如下面这样:</p>
<pre><code class="language-C#">
      public static int GetNum(ref int i)
      {
            return i;
      }

</code></pre>
<p>现在大家可以试着跳出思维定势,既然可以往方法内仍 引用地址 ,那能不能往方法外抛 引用地址 呢? 如果这也能实现就比较有意思了,我可以对集合内的某一些数据进行引用地址返回,在方法外照样可以修改这些返回值,毕竟传来传去都是引用地址,如下代码所示:</p>
<pre><code class="language-C#">
    public class Program
    {
      public static void Main(string[] args)
      {
            var nums = new int { 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;
      }
    }

</code></pre>
<p><img src="https://img2020.cnblogs.com/other/214741/202011/214741-20201107092119715-505191788.png" alt="" loading="lazy"></p>
<p>可以看到,数组的最后一个值已经由 <code>30 -&gt; 50</code> 了,有些朋友可能会比较惊讶,这到底是怎么玩的,不用想就是引用地址到处漂,不信的话,看看 IL 代码咯。</p>
<pre><code class="language-C#">
.method public hidebysig static
        int32&amp; GetNums (
                int32[] nums
        ) cil managed
{
        // Method begins at RVA 0x209c
        // Code size 13 (0xd)
        .maxstack 2
        .locals init (
                int32&amp;
        )

        // {
        IL_0000: nop
        // return ref nums;
        IL_0001: ldarg.0
        IL_0002: ldc.i4.2
        IL_0003: ldelema 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&amp; 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


</code></pre>
<p>可以看到,到处都是 &amp; 取值运算符,更直观一点的话用 windbg 看一下。</p>
<pre><code class="language-C#">
0:000&gt; !clrstack -a
OS Thread Id: 0x7040 (0)
000000D4E777E760 00007FFAF1C5108F ConsoleApp2.Program.Main(System.String[])
    PARAMETERS:
      args (0x000000D4E777E7F0) = 0x00000218c9ae9e60
    LOCALS:
      0x000000D4E777E7C8 = 0x00000218c9aeadd8
      0x000000D4E777E7C0 = 0x00000218c9aeadf0

0:000&gt; dp 0x00000218c9aeadf0
00000218`c9aeadf000000000`00000032 00000000`00000000

</code></pre>
<p>上面代码处的 <code>0x00000218c9aeadf0</code> 就是 num 的引用地址,继续用 dp 看一下这个地址上的值为 16进制的32,也就是十进制的 50 哈。</p>
<h2 id="三总结">三:总结</h2>
<p>总的来说,netcore 就是在当初盛行的 云计算 和 虚拟化 时代诞生,基因和使命促使它必须要优化优化再优化,再小的蚂蚁也是肉,最后就是 C# 大法 🐂👃</p>
<p><strong>更多高质量干货:参见我的 GitHub: dotnetfly</strong></p>
<img src="https://img2020.cnblogs.com/blog/214741/202005/214741-20200522143723695-575216767.png" width="600" height="200" alt="图片名称" align="center"><br><br>
来源:https://www.cnblogs.com/huangxincheng/p/13939828.html
頁: [1]
查看完整版本: C# 中的 ref 已经被放开,或许你已经不认识了