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?PersonAddress???????????????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__BackingField0: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:?nonesegment?????????????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.0IL_0002:?ldc.i4.2IL_0003:?ldelema?[System.Runtime]System.Int32IL_0008:?stloc.0//?(no?C#?code)IL_0009:?br.s?IL_000bIL_000b:?ldloc.0IL_000c:?ret }?//?end?of?method?Program::GetNums.method?public?hidebysig?static?void?Main?(string[]?args)?cil?managed? {IL_0013:?ldloc.0IL_0014:?call?int32&?ConsoleApp2.Program::GetNums(int32[])IL_0019:?stloc.1IL_001a:?ldloc.1IL_001b:?ldc.i4.s?50IL_003e:?popIL_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)?=?0x00000218c9ae9e60LOCALS:0x000000D4E777E7C8?=?0x00000218c9aeadd80x000000D4E777E7C0?=?0x00000218c9aeadf00:000>?dp?0x00000218c9aeadf0 00000218`c9aeadf0??00000000`00000032?00000000`00000000上面代碼處的 0x00000218c9aeadf0 就是 num 的引用地址,繼續用 dp 看一下這個地址上的值為 16進制的32,也就是十進制的 50 哈。
三:總結
總的來說,netcore 就是在當初盛行的 云計算 和 虛擬化 時代誕生,基因和使命促使它必須要優化優化再優化,再小的螞蟻也是肉,最后就是 C# 大法 ????????
總結
以上是生活随笔為你收集整理的C# 中的 ref 已经被放开,或许你已经不认识了的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: C#刷剑指Offer | 【常考题】最小
- 下一篇: .NET5全面拥抱Azure云,微软市值