C# 8: 可变结构体中的只读实例成员
在之前的文章中我們介紹了 C# 中的?只讀結(jié)構(gòu)體(readonly struct)[1]?和與其緊密相關(guān)的?in?參數(shù)[2]。
今天我們來(lái)討論一下從 C# 8 開始引入的一個(gè)特性:可變結(jié)構(gòu)體中的只讀實(shí)例成員(當(dāng)結(jié)構(gòu)體可變時(shí),將不會(huì)改變結(jié)構(gòu)體狀態(tài)的實(shí)例成員聲明為?readonly)。
引入只讀實(shí)例成員的原因
簡(jiǎn)單來(lái)說(shuō),還是為了提升性能。
我們已經(jīng)知道了只讀結(jié)構(gòu)體(readonly struct)和?in?參數(shù)可以通過減少創(chuàng)建副本,來(lái)提高代碼運(yùn)行的性能。當(dāng)我們創(chuàng)建只讀結(jié)構(gòu)體類型時(shí),編譯器強(qiáng)制所有成員都是只讀的(即沒有實(shí)例成員修改其狀態(tài))。但是,在某些場(chǎng)景,比如您有一個(gè)現(xiàn)有的 API,具有公開可訪問字段或者兼有可變成員和不可變成員。在這種情形下,不能將類型標(biāo)記為?readonly?(因?yàn)檫@關(guān)系到所有實(shí)例成員)。
通常,這沒有太大的影響,但是在使用?in?參數(shù)的情況下就例外了。對(duì)于非只讀結(jié)構(gòu)體的?in?參數(shù),編譯器將為每個(gè)實(shí)例成員的調(diào)用創(chuàng)建參數(shù)的防御性副本,因?yàn)樗鼰o(wú)法保證此調(diào)用不會(huì)修改其內(nèi)部狀態(tài)。這可能會(huì)導(dǎo)致創(chuàng)建大量副本,并且比直接按值傳遞結(jié)構(gòu)體時(shí)的總體性能更差(因?yàn)榘粗祩鬟f只會(huì)在傳參時(shí)創(chuàng)建一次副本)。
看一個(gè)例子您就明白了,我們定義這樣一個(gè)一般結(jié)構(gòu)體,然后將其作為 in 參數(shù)傳遞:
public struct Rect {public float w;public float h;public float Area{get{return w * h;}} } public class SampleClass {public float M(in Rect value){return value.Area + value.Area;} }編譯后,類?SampleClass?中的方法?M?代碼運(yùn)行邏輯實(shí)際上是這樣的:
public float M([In] [IsReadOnly] ref Rect value) {Rect rect = value; //防御性副本float area = rect.Area;rect = value; //防御性副本return area + rect.Area; }可變結(jié)構(gòu)體中的只讀實(shí)例成員
我們把上面的可變結(jié)構(gòu)體?Rect?修改一下,添加一個(gè)?readonly?方法?GetAreaReadOnly,如下:
public struct Rect {public float w;public float h;public float Area{get{return w * h;}}public readonly float GetAreaReadOnly(){return Area; //警告 CS8656 從 "readonly" 成員調(diào)用非 readonly 成員 "Rect.Area.get" 將產(chǎn)生 "this" 的隱式副本。} }此時(shí),代碼是可以通過編譯的,但是會(huì)提示一條這樣的的警告:從 "readonly" 成員調(diào)用非 readonly 成員 "Rect.Area.get" 將產(chǎn)生 "this" 的隱式副本。
翻譯成大白話就是說(shuō),我們?cè)谥蛔x方法?GetAreaReadOnly?中調(diào)用了非只讀?Area?屬性將會(huì)產(chǎn)生 "this" 的防御性副本。用代碼演示一下編譯后方法?GetAreaReadOnly?的方法體運(yùn)行邏輯實(shí)際上是這樣的:
所以為了避免創(chuàng)建多余的防御性副本而影響性能,我們應(yīng)該給只讀方法體中調(diào)用的屬性或方法都加上?readonly?修飾符(在本例中,即給屬性?Area?加上?readonly?修飾符)。
調(diào)用結(jié)構(gòu)體中的只讀實(shí)例成員
我們將上面的示例再修改一下:
public struct Rect {public float w;public float h;public readonly float Area{get{return w * h;}}public readonly float GetAreaReadOnly(){return Area;}public float GetArea(){return Area;} }public class SampleClass {public static float CallGetArea(Rect vector){return vector.GetArea();}public static float CallGetAreaIn(in Rect vector){return vector.GetArea();}public static float CallGetAreaReadOnly(in Rect vector){//調(diào)用可變結(jié)構(gòu)體中的只讀實(shí)例成員return vector.GetAreaReadOnly();} }類?SampleClass?中定義三個(gè)方法:
第一個(gè)方法是以前我們常見的調(diào)用方式;
第二個(gè)以?in?參數(shù)傳入可變結(jié)構(gòu)體,調(diào)用非只讀方法(可能修改結(jié)構(gòu)體狀態(tài)的方法);
第三個(gè)以?in?參數(shù)傳入可變結(jié)構(gòu)體,調(diào)用只讀方法。
我們來(lái)重點(diǎn)看一下第二個(gè)和第三個(gè)方法有什么區(qū)別,還是把它們的 IL 代碼邏輯翻譯成易懂的執(zhí)行邏輯,如下所示:
public static float CallGetAreaIn([In] [IsReadOnly] ref Rect vector) {Rect rect = vector; //防御性副本return rect.GetArea(); }public static float CallGetAreaReadOnly([In] [IsReadOnly] ref Rect vector) {return vector.GetAreaReadOnly(); }可以看出,CallGetAreaReadOnly?在調(diào)用結(jié)構(gòu)體的(只讀)成員方法時(shí),相對(duì)于?CallGetAreaIn?(調(diào)用結(jié)構(gòu)體的非只讀成員方法)少創(chuàng)建了一次本地的防御性副本,所以在執(zhí)行性能上應(yīng)該是有優(yōu)勢(shì)的。
只讀實(shí)例成員的性能分析
性能的提升在結(jié)構(gòu)體較大的時(shí)候比較明顯,所以在測(cè)試的時(shí)候?yàn)榱四軌蛲怀鋈齻€(gè)方法性能的差異,我在?Rect?結(jié)構(gòu)體中添加了 30 個(gè) decimal 類型的屬性,然后在類?SampleClass?中添加了三個(gè)測(cè)試方法,代碼如下所示:
public struct Rect {public float w;public float h;public readonly float Area{get{return w * h;}}public readonly float GetAreaReadOnly(){return Area;}public float GetArea(){return Area;}public decimal Number1 { get; set; }public decimal Number2 { get; set; }//...public decimal Number30 { get; set; } }public class SampleClass {const int loops = 50000000;Rect rectInstance;public SampleClass(){rectInstance = new Rect();}[Benchmark(Baseline = true)]public float DoNormalLoop(){float result = 0F;for (int i = 0; i < loops; i++){result = CallGetArea(rectInstance);}return result;}[Benchmark]public float DoNormalLoopByIn(){float result = 0F;for (int i = 0; i < loops; i++){result = CallGetAreaIn(in rectInstance);}return result;}[Benchmark]public float DoReadOnlyLoopByIn(){float result = 0F;for (int i = 0; i < loops; i++){result = CallGetAreaReadOnly(in rectInstance);}return result;}public static float CallGetArea(Rect vector){return vector.GetArea();}public static float CallGetAreaIn(in Rect vector){return vector.GetArea();}public static float CallGetAreaReadOnly(in Rect vector){return vector.GetAreaReadOnly();} }在沒有使用?in?參數(shù)的方法中,意味著每次調(diào)用傳入的是變量的一個(gè)新副本; 而在使用?in?修飾符的方法中,每次不是傳遞變量的新副本,而是傳遞同一副本的只讀引用。
DoNormalLoop?方法,參數(shù)不加修飾符,傳入一般結(jié)構(gòu)體,調(diào)用可變結(jié)構(gòu)體的非只讀方法,這是以前比較常見的做法。
DoNormalLoopByIn?方法,參數(shù)加?in?修飾符,傳入一般結(jié)構(gòu)體,調(diào)用可變結(jié)構(gòu)體的非只讀方法。
DoReadOnlyLoopByIn?方法,參數(shù)加?in?修飾符,傳入一般結(jié)構(gòu)體,調(diào)用可變結(jié)構(gòu)體的只讀方法。
使用 BenchmarkDotNet 工具測(cè)試三個(gè)方法的運(yùn)行時(shí)間,結(jié)果如下:
| Method | Mean | Error | StdDev | Ratio | |-------------------:|--------:|---------:|---------:|------:| | DoNormalLoop | 1.978 s | 0.0140 s | 0.0125 s | 1.00 | | DoNormalLoopByIn | 3.363 s | 0.0280 s | 0.0262 s | 1.70 | | DoReadOnlyLoopByIn | 1.032 s | 0.0200 s | 0.0187 s | 0.52 |從結(jié)果可以看出,當(dāng)結(jié)構(gòu)體可變時(shí),使用?in?參數(shù)調(diào)用結(jié)構(gòu)體的只讀方法,性能高于其他兩種; 使用?in?參數(shù)調(diào)用可變結(jié)構(gòu)體的非只讀方法,運(yùn)行時(shí)間最長(zhǎng),嚴(yán)重影響了性能,應(yīng)該避免這樣調(diào)用。
總結(jié)
當(dāng)結(jié)構(gòu)體為可變類型時(shí),應(yīng)將不會(huì)引起變化(即不會(huì)改變結(jié)構(gòu)體狀態(tài))的成員聲明為?readonly。
當(dāng)僅調(diào)用結(jié)構(gòu)體中的只讀實(shí)例成員時(shí),使用?in?參數(shù),可以有效提升性能。
readonly?修飾符在只讀屬性上是必需的。編譯器不會(huì)假定 getter 訪問者不修改狀態(tài)。因此,必須在屬性上顯式聲明。
自動(dòng)屬性可以省略?readonly?修飾符,因?yàn)椴还?readonly?修飾符是否存在,編譯器都將所有自動(dòng)實(shí)現(xiàn)的 getter 視為只讀。
不要使用?in?參數(shù)調(diào)用結(jié)構(gòu)體中的非只讀實(shí)例成員,因?yàn)闀?huì)對(duì)性能造成負(fù)面影響。
相關(guān)鏈接:
https://mp.weixin.qq.com/s/wwVZbdY7m7da1nmIKb2jCA?C# 中的只讀結(jié)構(gòu)體???
https://mp.weixin.qq.com/s/L73y4zdJmeT7zGTwGEJDZg?C# 中的 in 參數(shù)和性能分析???
作者 :技術(shù)譯民??
出品 :技術(shù)譯站(https://ITTranslator.cn/)
END
總結(jié)
以上是生活随笔為你收集整理的C# 8: 可变结构体中的只读实例成员的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 11座城市,58个.NET最新岗位速览,
- 下一篇: 简单聊聊C#中lock关键字