[周译见] C# 7 中的模范和实践
原文地址:https://www.infoq.com/articles/Patterns-Practices-CSharp-7
關(guān)鍵點(diǎn)
- 遵循 .NET Framework 設(shè)計(jì)指南,時(shí)至今日,仍像十年前首次出版一樣適用。
- API 設(shè)計(jì)至關(guān)重要,設(shè)計(jì)不當(dāng)?shù)腁PI大大增加錯(cuò)誤,同時(shí)降低可重用性。
- 始終保持"成功之道":只做正確的事,避免犯錯(cuò)。
- 去除 "line noise" 和 "boilerplate" 類型的代碼以保持關(guān)注業(yè)務(wù)邏輯
- 在為了性能犧牲而可讀性之前請(qǐng)保持清醒
C# 7 一個(gè)主要的更新是帶來了大量有趣的新特性。雖然已經(jīng)有很多文章介紹了 C# 7 可以做哪些事,但關(guān)于如何用好 C# 7 的文章還是很少。遵循?.NET Framework設(shè)計(jì)指南中?的原則,我們首先通過下面的策略,獲取這些新特性的最佳做法。
元組返回結(jié)果
在 C# 以往的編程中,從一個(gè)函數(shù)中返回多個(gè)結(jié)果可是相當(dāng)?shù)姆ξ丁utput 關(guān)鍵詞是一種方法,但如果對(duì)于異步方法不適用。Tuple<T>(元組) 盡管啰嗦,又要分配內(nèi)存,同時(shí)對(duì)于其字段又不能有描述性名稱。自定義的結(jié)構(gòu)優(yōu)于元組,但在一次性代碼中濫用會(huì)產(chǎn)生垃圾代碼。最后,匿名類型和動(dòng)態(tài)類型(dynamic) 的組合非常慢,又缺乏靜態(tài)類型檢查。
所有的這一切問題,在新的元組返回語法中得到了解決。下面是舊語法的例子:
?
這個(gè)函數(shù)實(shí)際的返回類型是 ValueTuple<string, string>。顧名思義,這是類似 Tuple<T> 類的輕量級(jí)結(jié)構(gòu)。這解決了類型膨脹的問題,但和 Tuple<T> 同樣缺失了描述性名稱。
public (string First, string Last) LookupName(long id) var names = LookupName(0); var firstName = names.First; var lastName = names.Last;返回的類型仍然是 ValueTuple<string, string>,但現(xiàn)在編譯器為函數(shù)添加了TupleElementNames 屬性,允許代碼使用描述性名稱而不是 Item1/Item2。
警告:TupleElementNames 屬性只能被編譯器使用。如果在返回類型上使用反射,則只能看到 ValueTuple<T> 結(jié)構(gòu)。因?yàn)檫@些屬性在函數(shù)返回結(jié)果的時(shí)候才會(huì)出現(xiàn),相關(guān)的信息是不存在的。
編譯器盡所能地為這些臨時(shí)的類型維持一種幻覺。例如,考慮下面這些聲明:
var a = LookupName(0); (string First, string Last) b = LookupName(0); ValueTuple<string, string> c = LookupName(0); (string make, string model) d = LookupName(0);從編譯器來看,a 是一種像 b 的 (string First, string Last) 類型。 由于 c 明確聲明為 ValueTuple<string, string>類型,所以沒有 c.First 的屬性。
d 說明了這種設(shè)計(jì)帶來的破壞,導(dǎo)致失去類型安全。很容易不小心重命名字段,會(huì)將一個(gè)元組分配給一個(gè)恰好具有相同形狀的元組。重申一下,這是因?yàn)榫幾g器不會(huì)認(rèn)為 (string First, string Last) 和 (string make, string model) 是不同的類型。
ValueTuple 是可變的
關(guān)于 ValueTuple 的一個(gè)有趣的看法:它是可變的。Mads Torgersen 解釋了原因:
下面的原因解釋了可變結(jié)構(gòu)為何經(jīng)常是壞的設(shè)計(jì),請(qǐng)不要用于元組。
如果您以常規(guī)方式封裝可變結(jié)構(gòu)體,使用私有、公共的訪問器,那么您將遇到一些意外驚嚇。原因是盡管這些結(jié)構(gòu)體被保存在只讀變量中,訪問器將悄悄在結(jié)構(gòu)體的副本中生效!
然而,元組只有公共的、可變的字段。由于這種設(shè)計(jì)沒有訪問器,因此不會(huì)有上述現(xiàn)象帶來的風(fēng)險(xiǎn)。
再且因?yàn)樗鼈兪墙Y(jié)構(gòu)體,當(dāng)它們被傳遞時(shí)會(huì)被復(fù)制。線程之間不直接共享,也不會(huì)有 “共享可變狀態(tài)” 的風(fēng)險(xiǎn)。這與 System.Tuple 系列的類型相反,為了線程安全需要保證其不可變。
[譯者]:Mutator的翻譯參考https://en.wikipedia.org/wiki/Mutator_method#C.23_example,為 C# 中的訪問器
注意他說的是“字段”,而不是“屬性”。這可能會(huì)導(dǎo)致基于反射的庫會(huì)有問題,這將對(duì)返回元組結(jié)果的方法造成毀滅。
元組返回結(jié)果指南
? 當(dāng)返回結(jié)果的列表字段很小且永不會(huì)改變時(shí),考慮使用元組返回結(jié)果而不是 out 參數(shù)。
? 在元組返回結(jié)果中使用帕斯卡(PascalCase)來命名描述性字段。這使得元組字段看起來像普通類和結(jié)構(gòu)體上的屬性。
? 在讀取元組返回值時(shí)不要使用var來解構(gòu)(deconstructing) ,避免意外搞錯(cuò)字段。
? 期望的返回值中用到反射的避免使用元組。
? 在公開的 APIs 中請(qǐng)不要使用元組返回結(jié)果,如果在將來的版本中需要返回其他字段,將字段添加到元組返回結(jié)果具有破壞性。
(譯者:deconstructing 的翻譯參考?https://zhuanlan.zhihu.com/p/25844861?中對(duì)deconstructing的翻譯,下面的部分名詞也是如此)
解構(gòu)多值返回結(jié)果
回到 LookupName 的示例, 創(chuàng)建一個(gè)名稱變量似乎有點(diǎn)惱人,只能在被局部變量單獨(dú)替換之前立即使用它。C#7 也使用所謂的 “解構(gòu)” 來解決這個(gè)問題。語法有幾種變形:
(string first, string last) = LookupName(0); (var first, var last) = LookupName(0); var (first, last) = LookupName(0); (first, last) = LookupName(0);在上面示例的最后一行,假定變量 first 和 last 已經(jīng)事先被聲明了。
解構(gòu)器
盡管名字很像 “析構(gòu)(destructor)”,但解構(gòu)器與對(duì)象銷毀無關(guān)。正如構(gòu)造函數(shù)將獨(dú)立的值組合成一個(gè)對(duì)象一樣,解構(gòu)器同樣是組合和分解對(duì)象。解構(gòu)器允許任何類提供上述的解構(gòu)語法。讓我們來分析一下 Rectangle 類,它有這樣的構(gòu)造函數(shù):
public Rectangle(int x, int y, int width, int height)當(dāng)你在一個(gè)新的實(shí)例中調(diào)用 ToString 時(shí),你會(huì)得到"{X=0,Y=0,Width=0,Height=0}"。結(jié)合這兩個(gè)事實(shí),我們知道了在自定義的解構(gòu)函數(shù)中對(duì)字段排序。
public void Deconstruct(out int x, out int y, out int width, out int height) {x = X;y = Y;width = Width;height = Height; } var (x, y, width, height) = myRectangle; Console.WriteLine(x); Console.WriteLine(y); Console.WriteLine(width); Console.WriteLine(height);你可能會(huì)好奇為什么使用 output 參數(shù),而不是元組。一部分原因是性能,這樣就減少了需要復(fù)制的數(shù)量。但最主要的原因是微軟還為重載打開了一道門。
繼續(xù)我們的研究,注意到 Rectangle 還有第二個(gè)構(gòu)造函數(shù):
我們同樣為它匹配一個(gè)解構(gòu)方法:
public void Deconstruct(out Point location, out Size size); var (location, size) = myRectangle;有多少個(gè)不同數(shù)量的構(gòu)造參數(shù)就有多少個(gè)解構(gòu)函數(shù)。即使你顯式地指出類型,編譯器也無法確定有哪些解構(gòu)方法可以使用。
在 API 設(shè)計(jì)中,結(jié)構(gòu)通常能從解構(gòu)中受益。類,特別是模型或者DTOs,如 Customer 和 Employee 可能不應(yīng)該有解構(gòu)方法,它們沒有方法解決諸如:"應(yīng)該是 (firstName, lastName, phoneNumber, email)" 還是 " (firstName, lastName, email, phoneNumber)" 的問題。某種程度來說,大家都應(yīng)該開心。
解構(gòu)器指南
? 考慮在讀取元組返回值時(shí)使用解構(gòu),但要注意避免搞錯(cuò)標(biāo)簽。
? 為結(jié)構(gòu)提供自定義的解構(gòu)方法。
? 記得匹配類的構(gòu)造函數(shù)中字段的順序,重寫 ToString 。
? 如果結(jié)構(gòu)具有多個(gè)構(gòu)造函數(shù),考慮提供對(duì)應(yīng)的解構(gòu)方法。
? 考慮立即解構(gòu)大值元組。大值元組的總大小超過16個(gè)字節(jié),這可能帶來多次復(fù)制的昂貴代價(jià)。請(qǐng)注意,引用類型的變量在32位操作系統(tǒng)中的大小總是4字節(jié),而在64位操作系統(tǒng)是8字節(jié)。
? 當(dāng)不知道在類中字段應(yīng)以何種方式排序時(shí),請(qǐng)不要使用解構(gòu)方法。
? 不要聲明多個(gè)具有同等數(shù)量參數(shù)的解構(gòu)方法。
Out 變量
C# 7 為 帶有 "out" 變量的調(diào)用函數(shù)提供了兩種新的語法選擇。現(xiàn)在可以在函數(shù)調(diào)用中這樣聲明變量。
if (int.TryParse(s, out var i)) {Console.WriteLine(i); }另一種選擇是完全使用"下劃線",忽略out 變量。
if (int.TryParse(s, out _)) {Console.WriteLine("success"); }如果你使用過 C# 7 預(yù)覽版,可能會(huì)注意到一點(diǎn):對(duì)被忽略的參數(shù)使用星號(hào)(*)已被更改為用下劃線。這樣做的部分原因是在函數(shù)式編程中通常出于同樣的目的使用了下劃線。其他類似的選擇包括諸如"void" 或者 "ignore" 的關(guān)鍵字。
使用下劃線很方便,同時(shí)意味著 API中的設(shè)計(jì)缺陷。在大多數(shù)情況中,更好的方法是對(duì)忽視的 out 參數(shù)簡(jiǎn)單地提供一個(gè)方法重載。
Out 變量指南
? 考慮用元組返回值替代 out參數(shù)。
? 盡量避免使用 out 或者 ref 參數(shù)。[詳情見?框架設(shè)計(jì)指南?]
? 考慮對(duì)忽視的 out 參數(shù)提供重載,這樣就不需要用下劃線了。
局部方法和迭代器
局部方法是一個(gè)有趣的概念。乍一看,就像是創(chuàng)建匿名方法的一種更易讀的語法。下面看看他們的不同。
public DateTime Max_Anonymous_Function(IList<DateTime> values) {Func<DateTime, DateTime, DateTime> MaxDate = (left, right) =>{return (left > right) ? left : right;};var result = values.First();foreach (var item in values.Skip(1))result = MaxDate(result, item);return result; }public DateTime Max_Local_Function(IList<DateTime> values) {DateTime MaxDate(DateTime left, DateTime right){return (left > right) ? left : right;}var result = values.First();foreach (var item in values.Skip(1))result = MaxDate(result, item);return result; }然而,一旦你開始深入了解,一些有趣的內(nèi)容將會(huì)浮現(xiàn)。
匿名方法 vs. 局部方法
當(dāng)你創(chuàng)建一個(gè)普通的匿名方法時(shí),總是會(huì)創(chuàng)建一個(gè)對(duì)應(yīng)的隱藏類來存儲(chǔ)該匿名方法。該隱藏類的實(shí)例將被創(chuàng)建并存儲(chǔ)在該類的靜態(tài)字段中。因此,一旦創(chuàng)建,沒有額外的開銷。
反觀局部方法,不需要隱藏類。相反,局部方法表現(xiàn)為其靜態(tài)父方法。
閉包
如果您的匿名方法或局部方法引用了外部變量,則產(chǎn)生"閉包"。下面是示例:
public DateTime Max_Local_Function(IList<DateTime> values) {int callCount = 0;DateTime MaxDate(DateTime left, DateTime right){callCount++; <--The variable callCount is being closed over.return (left > right) ? left : right;}var result = values.First();foreach (var item in values.Skip(1))result = MaxDate(result, item);return result; }對(duì)于匿名方法來說,隱藏類每次創(chuàng)建新實(shí)例時(shí)都要求外部父方法被調(diào)用。這確保每次調(diào)用時(shí),會(huì)在父方法和匿名方法共享數(shù)據(jù)副本。
這種設(shè)計(jì)的缺點(diǎn)是每次調(diào)用匿名方法需要實(shí)例化一個(gè)新對(duì)象。這就帶來了昂貴的使用成本,同時(shí)加重垃圾回收的壓力。
反觀局部方法,使用隱藏結(jié)構(gòu)取代了隱藏類。這就允許繼續(xù)存儲(chǔ)上一次調(diào)用的數(shù)據(jù),避免了每次都要實(shí)例化對(duì)象。與匿名方法一樣,局部方法實(shí)際存儲(chǔ)在隱藏結(jié)構(gòu)中。
委托
創(chuàng)建匿名方法或局部方法時(shí),通常會(huì)將其封裝到委托,以便在事件處理程序或者 LINQ 表達(dá)式中調(diào)用。
根據(jù)定義,匿名方法是匿名的。所以為了使用它,往往需要當(dāng)成委托存儲(chǔ)在一個(gè)變量或參數(shù)。
委托不可以指向結(jié)構(gòu)(除非他們被裝箱了,那就是奇怪的語義)。所以如果你創(chuàng)建了一個(gè)委托并指向一個(gè)局部方法,編譯器將會(huì)創(chuàng)建一個(gè)隱藏類代替隱藏結(jié)構(gòu)。如果該局部方法是一個(gè)閉包,那么每次調(diào)用父方法時(shí)都會(huì)創(chuàng)建一個(gè)隱藏類的新實(shí)例。
迭代器
在C#中,使用 yield 返回的 IEnumerable<T> 不能立即驗(yàn)證其參數(shù)。相反,直到在匿名枚舉器中調(diào)用 MoveNext,才可以對(duì)其參數(shù)進(jìn)行驗(yàn)證。
這在 VB 中不是問題,因?yàn)樗С?匿名迭代器。下面有一個(gè)來自MSDN的示例:
在當(dāng)前的 C# 版本中,GetSequence的迭代器需要完全獨(dú)立的方法。而在 C# 7中,可以使用局部方法實(shí)現(xiàn)。
public IEnumerable<int> GetSequence(int low, int high) {if (low < 1)throw new ArgumentException("low is too low");if (high > 140)throw new ArgumentException("high is too high");IEnumerable<int> Iterator(){for (int i = low; i <= high; i++)yield return i;}return Iterator(); }迭代器需要構(gòu)建一個(gè)狀態(tài)機(jī),所以它們的行為就像在隱藏類中作為委托返回閉包。
匿名方法和局部方法指南
? 當(dāng)不需要委托時(shí),使用局部方法代替匿名方法,尤其是涉及到閉包。
? 當(dāng)返回一個(gè)需要驗(yàn)證參數(shù)的 IEnumerator 時(shí),使用局部迭代器。
? 考慮將局部方法放到方法的開頭或結(jié)尾處,以便與父方法區(qū)分來。
? 避免在性能敏感的代碼中使用帶委托的閉包,這適用于匿名方法和局部方法。
引用返回、局部引用以及引用屬性
結(jié)構(gòu)具有一些有趣的性能特性。由于他們與其父數(shù)據(jù)結(jié)構(gòu)一起存儲(chǔ),沒有普通類的頭開銷。這意味著你可以非常密集地存儲(chǔ)在數(shù)組中,很少或不浪費(fèi)空間。除了減少內(nèi)存總體開銷外,還帶來了極大的優(yōu)勢(shì),使 CPU 緩存更高效。這就是為什么構(gòu)建高性能應(yīng)用程序的人喜歡結(jié)構(gòu)。
但是如果結(jié)構(gòu)太大的話,需要避免不必要的復(fù)制。微軟的指南建議為16個(gè)字節(jié),足夠存儲(chǔ)2個(gè) doubles 或者 4 個(gè) integers。這不是很多,盡管有時(shí)可以使用位域 (bit-fields)來擴(kuò)展。
局部引用
這樣做的一個(gè)方法是使用智能指針,所以你永遠(yuǎn)不需要復(fù)制。這里有一些我仍然使用的ORM性能敏感代碼。
for (var i = 0; i < m_Entries.Length; i++) {if (string.Equals(m_Entries[i].Details.ClrName, item.Key, StringComparison.OrdinalIgnoreCase)|| string.Equals(m_Entries[i].Details.SqlName, item.Key, StringComparison.OrdinalIgnoreCase)){var value = item.Value ?? DBNull.Value;if (value == DBNull.Value){if (!ignoreNullProperties)parts.Add($"{m_Entries[i].Details.QuotedSqlName} IS NULL");}else{m_Entries[i].ParameterValue = value;m_Entries[i].UseParameter = true;parts.Add($"{m_Entries[i].Details.QuotedSqlName} = {m_Entries[i].Details.SqlVariableName}");}found = true;keyFound = true;break;} }你會(huì)注意到的第一件事是沒有使用 for-each。為了避免復(fù)制,仍然使用舊式的 for 循環(huán)。即使如此,所有的讀和寫操作都是直接在 m_Entries 數(shù)組中操作。
使用 C# 7 的局部引用,明顯地減少混亂而不改變語義。
這是因?yàn)?"局部引用" 真的是一個(gè)安全的指針。我們之所以說它 “安全” ,是因?yàn)榫幾g器指向不允許任何臨時(shí)變量,諸如普通方法的結(jié)果。
如果你很想知道 " ref var entry = ref m_Entries[i];" 是不是有效的語法(是的),無論如何也不能這么做,會(huì)造成混亂。 ref 既是用于聲明,又不會(huì)被用到。(譯者:這里應(yīng)該是指 entry 的 ref 修飾吧)
引用返回
引用返回豐富了本地方法,允許創(chuàng)建無副本的方法。
繼續(xù)之前的示例,我們可以將搜索結(jié)果輸出推到其靜態(tài)方法。
在這個(gè)例子中,我們返回了一個(gè)數(shù)組元素的引用。你也可以返回對(duì)象中字段的引用,使用引用屬性(見下文)和引用參數(shù)。
ref int Echo(ref int input) {return ref input; } ref int Echo2(ref Foo input) {return ref Foo.Field; }引用返回的一個(gè)有趣的功能是調(diào)用者可以選擇是否使用它。下面兩行代碼同樣有效:
Entry copy = FindColumn(m_Entries, "FirstName"); ref Entry reference = ref FindColumn(m_Entries, "FirstName");引用返回和引用屬性
你可以創(chuàng)建一個(gè)引用返回風(fēng)格的屬性,但只能用于該屬性只讀的情況下。例如:
public ref int Test { get { return ref m_Test; } }對(duì)于不可變結(jié)構(gòu)來說,這種模式似乎毫不傷腦。調(diào)用者不需要花費(fèi)額外的功夫,就可以將其視為引用值或普通值。
對(duì)于可變的結(jié)構(gòu),事情變得有趣起來。首先,這修復(fù)了一不小心就會(huì)通過修改屬性而改變結(jié)構(gòu)返回值的老問題,只與值變化共進(jìn)退。
考慮以下的類:
在 C# 1中,size 將保持不變。在 C# 6中,將觸發(fā)一個(gè)編譯器錯(cuò)誤。在 C# 7 中,我們只是加了個(gè) ref 修飾,卻能跑起來。
public ref Rectangle Size { get { return ref m_Size; } }乍一看就像你一旦想覆蓋 size 的值就會(huì)被阻止。但事實(shí)證明,仍然可以編寫如下代碼:
var rect = new Rectangle(0, 0, 10, 20); s.Size = rect;即使該屬性是“只讀”,也將如期執(zhí)行。這個(gè)對(duì)象清楚自己不會(huì)返回一個(gè) Rectangle對(duì)象,而是保留指向 Rectangle對(duì)象所在位置的指針。
現(xiàn)在有了新的問題,不可變結(jié)構(gòu)不再是永恒的。即使單個(gè)字段不能被更改,值卻被引用屬性替換了。C# 將通過拒絕執(zhí)行該語法來警告你:
引用返回和索引器
對(duì)于引用返回和局部引用最大的限制可能就是需要一個(gè)固定的指針。
考慮這行代碼:
這樣的代碼無效,因?yàn)榱斜聿幌駭?shù)組,在讀取其值時(shí)會(huì)創(chuàng)建一個(gè)副本結(jié)構(gòu)。下面是對(duì) List<T> 實(shí)現(xiàn)?引用的源碼:
public T this[int index] {get {// Following trick can reduce the range check by oneif ((uint) index >= (uint)_size) {ThrowHelper.ThrowArgumentOutOfRangeException();}Contract.EndContractBlock();return _items[index]; <-- return makes a copy}這同樣適用于 ImmutableArray<T> 和 訪問 IList<T> 接口的普通數(shù)組。但是,您可以實(shí)現(xiàn)自己的List<T>,將其索引定義為引用返回。
public ref T this[int index] {get {// Following trick can reduce the range check by oneif ((uint) index >= (uint)_size) {ThrowHelper.ThrowArgumentOutOfRangeException();}Contract.EndContractBlock();return ref _items[index]; <-- return ref makes a reference}如果你這么做,需要明確實(shí)現(xiàn) IList<T> 和 IReadOnlyList<T> 接口。這是因?yàn)橐梅祷鼐哂信c普通返回值不同的簽名,因此不能滿足接口的要求。
由于索引器實(shí)際上只是專用屬性,它們與引用屬性具有相同的限制; 這意味著您無法顯式定義 setter,而索引器卻是可寫的。
引用返回、局部引用和引用屬性指南
? 在使用數(shù)組的方法中,考慮使用引用返回而不是索引值
? 在擁有結(jié)構(gòu)的自定義集合類中,對(duì)索引器考慮使用引用返回代替一般的返回結(jié)果。
? 將包含可變結(jié)構(gòu)體的屬性暴露為引用屬性。
? 不要將包含不可變結(jié)構(gòu)的屬性暴露為引用屬性。
? 不要在不可變或只讀類上暴露引用屬性。
? 不要在不可變或只讀集合類上暴露引用索引器。
ValueTask 和通用異步返回類型
當(dāng)Task類被創(chuàng)建時(shí),它的主要角色是簡(jiǎn)化多線程編程。它創(chuàng)建一種將長(zhǎng)時(shí)間運(yùn)行的操作推入線程池的通道,并在 UI線程上推遲讀取結(jié)果。而當(dāng)你使用 fork-join 模式并發(fā)時(shí),效果顯著。
隨著.NET 4.5中引入了 async/await ,一些缺陷也開始顯現(xiàn)。正如我們?cè)?011年的反饋(詳見?Task Parallel Library Improvements in .NET 4.5),創(chuàng)建一個(gè) Task對(duì)象所花費(fèi)的時(shí)間比可接受的時(shí)間長(zhǎng),因此必須重寫其內(nèi)部,結(jié)果是創(chuàng)建Task<Int32> 所需的時(shí)間縮短了49%至55%,并在大小上減小了52%。
這是很好的一步,但 Task 仍然分配了內(nèi)存。所以當(dāng)你在緊湊循環(huán)中使用它,如下所示將產(chǎn)生大量的垃圾。
而且如前所述, C# 高性能代碼的關(guān)鍵在于減少內(nèi)存分配和隨后的GC循環(huán)。微軟的Joe Duffy在?Asynchronous Everything?的文章中寫到:
首先,請(qǐng)記住,Midori 被整個(gè)操作系統(tǒng)用于內(nèi)存垃圾回收。我們必須學(xué)到了一些必要的經(jīng)驗(yàn)教訓(xùn),以便充分發(fā)揮作用。但我想說的主要是避免不必要的分配,分配越多麻煩越多,特別是短命對(duì)象。早期 .NET世界中流傳著一句口頭禪:Gen0 集合是無代價(jià)的。不幸的是,這形成了很多.NET的庫代碼濫用。Gen0 集合存在著中斷、弄臟緩存以及在高并發(fā)的系統(tǒng)中有高頻問題。
這里的真正解決方案是創(chuàng)建一個(gè)基于結(jié)構(gòu)的 task,而不是使用堆分配的版本。這實(shí)際上是以System.Threading.Tasks.Extensions?中的?ValueTask<T>創(chuàng)建。并且因?yàn)?await 已經(jīng)任何暴露的方法中工作了,所以你可以使用它。
手動(dòng)暴露ValueTask<T>
ValueTask<T>的基本用例是預(yù)期結(jié)果在大部分時(shí)間是同步的,并且想要消除不必要的內(nèi)存分配。首先,假設(shè)你有一個(gè)傳統(tǒng)的基于 task 的異步方法。
public async Task<Customer> ReadFromDBAsync(string key)然后我們將其封裝到一個(gè)緩存方法中:
public ValueTask<Customer> ReadFromCacheAsync(string key) {Customer result;if (_Cache.TryGetValue(key, out result))return new ValueTask<Customer>(result); //no allocationelsereturn new ValueTask<Customer>(ReadFromCacheAsync_Inner(key)); }并添加一個(gè)輔助方法來構(gòu)建異步狀態(tài)機(jī)。
async Task<Customer> ReadFromCacheAsync_Inner(string key) {var result = await ReadFromDBAsync(key);_Cache[key] = result;return result; }有了這一點(diǎn),調(diào)用者可以使用與 ReadFromDBAsync 完全相同的語法來調(diào)用ReadFromCacheAsync;
async Task Test() {var a = await ReadFromCacheAsync("aaa");var b = await ReadFromCacheAsync("bbb"); }通用異步
雖然上述模式并不困難,但實(shí)施起來相當(dāng)乏味。而且我們知道,編寫代碼越繁瑣,出現(xiàn)簡(jiǎn)單的錯(cuò)誤就越有可能。所以目前 C# 7 的提議是提供通用異步返回結(jié)果。
根據(jù)目前的設(shè)計(jì),你只能使用異步關(guān)鍵字,并且方法返回 Task、Task<T>或者 void。一旦實(shí)現(xiàn),通用異步返回結(jié)果將會(huì)擴(kuò)展到任何 tasklike 方法上去。一些人認(rèn)為 tasklike 需要有一個(gè) AsyncBuilder 屬性。這表明輔助類被用于創(chuàng)建 tasklike 對(duì)象。
在這個(gè)設(shè)計(jì)的注意事項(xiàng)中,微軟估計(jì)大概有五種人實(shí)際上會(huì)創(chuàng)建 tasklike 類,從而被普遍接受。其他人都很可能也像這五分之一。這是我們上面使用新語法的例子:
如您所見,我們已經(jīng)去除了輔助方法,除了返回類型,它看起來像任何其他異步方法一樣。
何時(shí)使用 ValueTask<T>
所以應(yīng)該使用 ValueTask<T> 代替 Task<T>? 完全不必要,這可能有點(diǎn)難以理解,所以我們將引用相關(guān)文檔:
方法可能會(huì)返回一個(gè)該值類型的實(shí)例,當(dāng)它們的操作可以同時(shí)執(zhí)行,同時(shí)被頻繁喚起(invoked)。這時(shí),對(duì)于Task<TResult>,每一次調(diào)用都是昂貴的成本,應(yīng)該被禁止。
使用 ValueTask<TResult> 代替 Task<TResult> 需要權(quán)衡利弊。例如,雖然 ValueTask<TResult> 可以避免分配,并且成功返回結(jié)果是可以同步返回的。然而它需要兩個(gè)字段,而 Task<TResult> 作為引用類型只是一個(gè)字段。這意味著調(diào)用方法最終返回的是兩個(gè)數(shù)據(jù)而不是一個(gè)數(shù)據(jù),這就會(huì)有更多的數(shù)據(jù)被復(fù)制。同時(shí)意味著如果在異步方法中需要等待時(shí),只返回其中一個(gè),這會(huì)導(dǎo)致該異步方法的狀態(tài)機(jī)變得更大。因?yàn)橐鎯?chǔ)兩個(gè)字段的結(jié)構(gòu)而不是一個(gè)引用。
再進(jìn)一步,使用者通過 await 來獲取異步操作的結(jié)果,ValueTask<TResult> 可能會(huì)導(dǎo)致更復(fù)雜的模型,實(shí)際上就會(huì)導(dǎo)致分配更多的內(nèi)存。例如,考慮到一個(gè)方法可能返回一個(gè)普通的已緩存 task 的結(jié)果Task<TResult>,或者是一個(gè) ValueTask<TResult>。如果調(diào)用者的預(yù)期結(jié)果是 Task<TResult>,可以被諸如 Task.WhenAll 和 Task.WhenAny 的方法調(diào)用,那么 ValueTask<TResult> 首先需要使用 ValueTask<TResult>.AsTask 將其自身轉(zhuǎn)換為 Task<TResult> ,如果 Task<TResult> 在第一次使用沒有被緩存了,將導(dǎo)致分配。
因此,Task的任何異步方法的默認(rèn)選擇應(yīng)該是返回一個(gè) Task 或Task<TResult>。除非性能分析證明使用 ValueTask<TResult> 優(yōu)于Task<TResult>。Task.CompletedTask 屬性可能被單獨(dú)用于傳遞任務(wù)成功執(zhí)行的狀態(tài), ValueTask<TResult> 并不提供泛型版本。
這是一段相當(dāng)長(zhǎng)的段落,所以我們?cè)谙旅娴闹改现锌偨Y(jié)了這一點(diǎn)。
ValueTask <T>指南
? 當(dāng)結(jié)果經(jīng)常被同步返回時(shí),請(qǐng)考慮在性能敏感代碼中使用 ValueTask<T>。
? 當(dāng)內(nèi)存壓力是個(gè)問題,且 Tasks 不能被緩存時(shí),考慮使用 ValueTask<T>。
? 避免在公共API中暴露 ValueTask<T>,除非有顯著的性能影響。
? 不要在調(diào)用 Task.WhenAll 或 WhenAny 中調(diào)用 ValueTask<T>。
表達(dá)式體成員
表達(dá)式體成員允許消除簡(jiǎn)單函數(shù)的括號(hào)。這通常是將一個(gè)四行函數(shù)減少到一行。例如:
public override string ToString() {return FirstName + " " + LastName; } public override string ToString() => FirstName + " " + LastName;必須注意不要過分。例如,假設(shè)當(dāng) FirstName 為空時(shí),您需要避免產(chǎn)生空格。你可能會(huì)這么寫:
public override string ToString() => !string.IsNullOrEmpty(FirstName) ? FirstName + " " + LastName : LastName;但是,你可能會(huì)遇到 last name 同時(shí)為空。
public override string ToString() => !string.IsNullOrEmpty(FirstName) ? FirstName + " " + LastName : (!string.IsNullOrEmpty(LastName) ? LastName : "No Name");如您所見,很容易得意忘形地使用這個(gè)功能。所以當(dāng)你遇到有多分支條件或者 null合并操作時(shí),請(qǐng)克制使用。
表達(dá)式體屬性
表達(dá)式體屬性是 C# 6 的新特性。在使用 Get/Set 方法處理 MVVM風(fēng)格的模型之類時(shí),非常有用。
這是C#6代碼:
還有 C# 7的替代方案:
public string FirstName {get => Get<string>(); set => Set(value); }雖然沒有減少代碼行數(shù),但大部分 line-noise 代碼已經(jīng)消失了。而且每個(gè)屬性都能這么做,積少成多。
有關(guān) Get/Set 在這些示例中的工作原理的更多信息,請(qǐng)參閱?C#, VB.NET To Get Windows Runtime Support, Asynchronous Methods。
表達(dá)式體構(gòu)造函數(shù)
表達(dá)式體構(gòu)造函數(shù)是C# 7 的新特性。下面有一個(gè)例子:
class Person {public Person(string name) => Name = name;public string Name { get; } }這里的用法非常有限。它只有在零個(gè)或者一個(gè)參數(shù)的情況下才有效。一旦需要將其他參數(shù)分配給字段/屬性時(shí),則必須用回傳統(tǒng)的構(gòu)造函數(shù)。同時(shí)也無法初始化其他字段,解析事件處理程序等(參數(shù)驗(yàn)證是可能的,請(qǐng)參見下面的“拋出表達(dá)式”。)
所以我們的建議是簡(jiǎn)單地忽略這個(gè)功能。它只是將單參數(shù)構(gòu)造函數(shù)看起來與一般的構(gòu)造函數(shù)不同而已,同時(shí)讓代碼大小減少而已。
析構(gòu)表達(dá)式
為了使 C# 更加一致,析構(gòu)被允許寫成和表達(dá)式的成員一樣,就像用在方法和構(gòu)造函數(shù)一樣。
對(duì)于那些忘記析構(gòu)的人來說,C# 中的析構(gòu)是在 Finalize 方法上重寫System.Object。雖然 C# 不這樣表達(dá):
這種語法的一個(gè)問題是它看起來很像一個(gè)構(gòu)造函數(shù),因此可以很容易地被忽略。另一個(gè)問題是它模仿 C ++中的析構(gòu)語法,卻是完全不同的語義。但是已經(jīng)被使用了這么久,所以我們只好轉(zhuǎn)向新的語法:
~UnmanagedResource() => ReleaseResources();現(xiàn)在我們有一行孤立的、容易忽略的代碼,用于終結(jié)對(duì)象生命周期。這不是一個(gè)簡(jiǎn)單的 屬性 或 ToString 方法,而是很重大的操作,需要顯眼一些。所以我建議不要使用它。
表達(dá)式體成員指南
? 為簡(jiǎn)單的屬性使用表達(dá)式體成員。
? 為方法重載使用表達(dá)式體成員。
? 簡(jiǎn)單的方法考慮使用表達(dá)式體成員。
? 不要在表達(dá)式體成員使用多分支條件(a?b:c)或 null 合并運(yùn)算符(x ?? y)。
? 不要為 構(gòu)造函數(shù) 和 析構(gòu)函數(shù) 中使用表達(dá)式成員。
拋出表達(dá)式
表面上,編程語言一般可以分為兩種:
- 一切都是表達(dá)式
- 語句、聲明和表達(dá)式都是獨(dú)立的概念
Ruby是前者的一個(gè)實(shí)例,甚至其聲明也是表達(dá)式。相比之下,Visual Basic代表后者,語句和表達(dá)式之間有很強(qiáng)的區(qū)別。例如,對(duì)于 "if" 而言,當(dāng)它獨(dú)立存在時(shí),以及作為表達(dá)式中的一部分時(shí),是完全不同的語法。
C#主要是第二陣營(yíng),但存在著 C語言的遺產(chǎn),允許你處理語句,當(dāng)成表達(dá)式一樣。可以編寫如下代碼:
首先,C#7 允許使用非賦值語句作為表達(dá)式。現(xiàn)在可以在表達(dá)式的任何地方放置 “throw” 語句,不用對(duì)語法做任何更改。以下是Mads Torgersen 新聞稿中的一些例子:
class Person {public string Name { get; }public Person(string name) => Name = name ?? throw new ArgumentNullException("name");public string GetFirstName(){var parts = Name.Split(' ');return (parts.Length > 0) ? parts[0] : throw new InvalidOperationException("No name!");}public string GetLastName() => throw new NotImplementedException(); }在這些例子中,很容易看出會(huì)發(fā)生什么情況。但是如果我們移動(dòng)拋出表達(dá)式的位置呢?
return (parts.Length == 0) ? throw new InvalidOperationException("No name!") : parts[0];這樣看來就不夠易讀了。而左右的語句是相關(guān)的,中間的語句與他們無關(guān)。從第一個(gè)版本看,左邊是預(yù)期分支,右邊是錯(cuò)誤分支。第二個(gè)版本的錯(cuò)誤分支將預(yù)期分支分成兩半,打破整條流程。
我們來看另一個(gè)例子。這里我們摻入一個(gè)函數(shù)調(diào)用。
void Save(IList<Customer> customers, User currentUser) {if (customers == null || customers.Count == 0) throw new ArgumentException("No customers to save");_Database.SaveEach("dbo.Customer", customers, currentUser); }void Save(IList<Customer> customers, User currentUser) {_Database.SaveEach("dbo.Customer", (customers == null || customers.Count == 0) ? customers : throw new ArgumentException("No customers to save"), currentUser); }我們已經(jīng)可以看到,寫到一塊是有問題的,盡管它的LINQ并不難看。但是為了更好地閱讀代碼,我們使用橙色標(biāo)記條件,藍(lán)色標(biāo)記函數(shù)調(diào)用,黃色標(biāo)記函數(shù)參數(shù),紅色標(biāo)記錯(cuò)誤分支。
這樣可以看到隨著參數(shù)改變位置,上下文如何變化。
拋出表達(dá)式指南
? 在分支/返回語句中,考慮將拋出表達(dá)式放在條件(a?b:c)和 null 合并運(yùn)算符(x ?? y)的右側(cè)。
? 避免將拋出表達(dá)式放到條件運(yùn)算的中間位置。
? 不要將拋出表達(dá)式放在方法的參數(shù)列表中。
有關(guān)異常如何影響 API設(shè)計(jì)的更多信息,請(qǐng)參閱?Designing with Exceptions in .NET。
模式匹配 和 加強(qiáng) Switch 語句
模式匹配(加強(qiáng)了 Switch 語句)對(duì)API設(shè)計(jì)沒有任何影響。所以雖然可以使異構(gòu)集合的處理變得更加容易,但最好的情況還是盡可能地使用共享接口和多態(tài)性。
也就是說,有些細(xì)節(jié)還是要注意的。考慮這個(gè)八月份發(fā)布的例子:
以前,case的順序并不重要。在 C# 7 中,像 Visual Basic一樣,switch語句幾乎嚴(yán)格按順序執(zhí)行。對(duì)于 when 表達(dá)式同樣適用。
實(shí)際上,您希望最常見的情況是 switch 語句中的第一種情況,就像在一系列 if-else-if 語句塊中一樣。同樣,如果任何檢查特別昂貴,那么它應(yīng)該越靠近底部,只在必要時(shí)才執(zhí)行。
順序規(guī)則的例外是默認(rèn)情況。它總是被最后處理,不管它的實(shí)際順序是什么。這會(huì)使代碼更難理解,所以我建議將默認(rèn)情況放在最后。
模式匹配表達(dá)式
雖然 switch 語句可能是 C# 中最常用的模式匹配; 但并不是唯一的方式。在運(yùn)行時(shí)求值的任何布爾表達(dá)式都可以包含模式匹配表達(dá)式。
下面有一個(gè)例子,它判斷變量 'o' 是否是一個(gè)字符串,如果是這樣,則嘗試將其解析為一個(gè)整數(shù)。
注意如何在模式匹配中創(chuàng)建一個(gè)名為's'的新變量,然后再用于TryParse。這種方法可以鏈?zhǔn)浇M合,構(gòu)建更復(fù)雜的表達(dá)式:
if ((o is int i) || (o is string s && int.TryParse(s, out i))) {Console.WriteLine(i); }為了方便比較, 將上述代碼重寫成 C# 6 風(fēng)格:
if (o is int) {Console.WriteLine((int)o); } else if (o is string && int.TryParse((string) o, out i)) {Console.WriteLine(i); }現(xiàn)在還不知道新的模式匹配代碼是否比以前的方式更有效,但它可能會(huì)消除一些冗余的類型檢查。
一起維護(hù)這個(gè)在線文檔
C# 7 的新特性仍然很新鮮,而且關(guān)于它們?cè)诂F(xiàn)實(shí)世界中如何運(yùn)行,還需要多多了解。所以如果你看到一些你不同意的東西,或者這些指南中沒有的話,請(qǐng)讓我們知道。
關(guān)于作者
喬納森·艾倫(Jonathan Allen)在90年代末期開始從事衛(wèi)生診所的MIS項(xiàng)目,從 Access 和 Excel 到企業(yè)解決方案。在為金融部門編寫自動(dòng)化交易系統(tǒng)五年之后,他成為各種項(xiàng)目的顧問,包括機(jī)器人倉庫的UI,癌癥研究軟件的中間層以及房地產(chǎn)保險(xiǎn)公司的大數(shù)據(jù)需求。在空閑時(shí)間,他學(xué)習(xí)和書寫16世紀(jì)以來的武術(shù)知識(shí)。
轉(zhuǎn)載于:https://www.cnblogs.com/chenug/p/6803649.html
總結(jié)
以上是生活随笔為你收集整理的[周译见] C# 7 中的模范和实践的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: python 类的功能,字符串字节,嵌套
- 下一篇: NYOJ--1236--挑战密室(第八届