原文地址:https://www.infoq.com/articles/Patterns-Practices-CSharp-7
關(guān)鍵點
- 遵循 .NET Framework 設(shè)計指南旭斥,時至今日,仍像十年前首次出版一樣適用夯辖。
- API 設(shè)計至關(guān)重要琉预,設(shè)計不當(dāng)?shù)腁PI大大增加錯誤,同時降低可重用性蒿褂。
- 始終保持"成功之道":只做正確的事圆米,避免犯錯。
- 去除 "line noise" 和 "boilerplate" 類型的代碼以保持關(guān)注業(yè)務(wù)邏輯
- 在為了性能犧牲而可讀性之前請保持清醒
C# 7 一個主要的更新是帶來了大量有趣的新特性啄栓。雖然已經(jīng)有很多文章介紹了 C# 7 可以做哪些事娄帖,但關(guān)于如何用好 C# 7 的文章還是很少。遵循 .NET Framework設(shè)計指南中 的原則昙楚,我們首先通過下面的策略近速,獲取這些新特性的最佳做法。
元組返回結(jié)果
在 C# 以往的編程中堪旧,從一個函數(shù)中返回多個結(jié)果可是相當(dāng)?shù)姆ξ断鞔小utput 關(guān)鍵詞是一種方法,但如果對于異步方法不適用淳梦。Tuple<T>(元組) 盡管啰嗦析砸,又要分配內(nèi)存,同時對于其字段又不能有描述性名稱爆袍。自定義的結(jié)構(gòu)優(yōu)于元組首繁,但在一次性代碼中濫用會產(chǎn)生垃圾代碼。最后陨囊,匿名類型和動態(tài)類型(dynamic) 的組合非常慢弦疮,又缺乏靜態(tài)類型檢查。
所有的這一切問題蜘醋,在新的元組返回語法中得到了解決胁塞。下面是舊語法的例子:
public (string, string) LookupName(long id) // tuple return type
{
return ("John", "Doe"); // tuple literal
}
var names = LookupName(0);
var firstName = names.Item1;
var lastName = names.Item2;
這個函數(shù)實際的返回類型是 ValueTuple<string, string>。顧名思義压语,這是類似 Tuple<T> 類的輕量級結(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)。因為這些屬性在函數(shù)返回結(jié)果的時候才會出現(xiàn)舵揭,相關(guān)的信息是不存在的谤专。
編譯器盡所能地為這些臨時的類型維持一種幻覺。例如午绳,考慮下面這些聲明:
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è)計帶來的破壞,導(dǎo)致失去類型安全赎败。很容易不小心重命名字段秕衙,會將一個元組分配給一個恰好具有相同形狀的元組。重申一下僵刮,這是因為編譯器不會認(rèn)為 (string First, string Last) 和 (string make, string model) 是不同的類型据忘。
ValueTuple 是可變的
關(guān)于 ValueTuple 的一個有趣的看法:它是可變的。Mads Torgersen 解釋了原因:
下面的原因解釋了可變結(jié)構(gòu)為何經(jīng)常是壞的設(shè)計搞糕,請不要用于元組勇吊。
如果您以常規(guī)方式封裝可變結(jié)構(gòu)體,使用私有窍仰、公共的訪問器汉规,那么您將遇到一些意外驚嚇。原因是盡管這些結(jié)構(gòu)體被保存在只讀變量中辈赋,訪問器將悄悄在結(jié)構(gòu)體的副本中生效鲫忍!
然而膏燕,元組只有公共的钥屈、可變的字段。由于這種設(shè)計沒有訪問器坝辫,因此不會有上述現(xiàn)象帶來的風(fēng)險篷就。
再且因為它們是結(jié)構(gòu)體,當(dāng)它們被傳遞時會被復(fù)制近忙。線程之間不直接共享竭业,也不會有 “共享可變狀態(tài)” 的風(fēng)險。這與 System.Tuple 系列的類型相反及舍,為了線程安全需要保證其不可變未辆。
[譯者]:Mutator的翻譯參考https://en.wikipedia.org/wiki/Mutator_method#C.23_example,*為 C# 中的訪問器 *
注意他說的是“字段”锯玛,而不是“屬性”咐柜。這可能會導(dǎo)致基于反射的庫會有問題兼蜈,這將對返回元組結(jié)果的方法造成毀滅。
元組返回結(jié)果指南
? 當(dāng)返回結(jié)果的列表字段很小且永不會改變時拙友,考慮使用元組返回結(jié)果而不是 out 參數(shù)为狸。
? 在元組返回結(jié)果中使用帕斯卡(PascalCase)來命名描述性字段。這使得元組字段看起來像普通類和結(jié)構(gòu)體上的屬性遗契。
? 在讀取元組返回值時不要使用var來解構(gòu)(deconstructing) 辐棒,避免意外搞錯字段。
? 期望的返回值中用到反射的避免使用元組牍蜂。
? 在公開的 APIs 中請不要使用元組返回結(jié)果漾根,如果在將來的版本中需要返回其他字段,將字段添加到元組返回結(jié)果具有破壞性鲫竞。
(譯者:deconstructing 的翻譯參考 https://zhuanlan.zhihu.com/p/25844861 中對deconstructing的翻譯立叛,下面的部分名詞也是如此)
解構(gòu)多值返回結(jié)果
回到 LookupName 的示例, 創(chuàng)建一個名稱變量似乎有點惱人贡茅,只能在被局部變量單獨替換之前立即使用它菜拓。C#7 也使用所謂的 “解構(gòu)” 來解決這個問題。語法有幾種變形:
(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)器與對象銷毀無關(guān)驹沿。正如構(gòu)造函數(shù)將獨立的值組合成一個對象一樣艘策,解構(gòu)器同樣是組合和分解對象。解構(gòu)器允許任何類提供上述的解構(gòu)語法渊季。讓我們來分析一下 Rectangle 類朋蔫,它有這樣的構(gòu)造函數(shù):
public Rectangle(int x, int y, int width, int height)
當(dāng)你在一個新的實例中調(diào)用 ToString 時,你會得到"{X=0,Y=0,Width=0,Height=0}"却汉。結(jié)合這兩個事實驯妄,我們知道了在自定義的解構(gòu)函數(shù)中對字段排序。
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);
你可能會好奇為什么使用 output 參數(shù)合砂,而不是元組青扔。一部分原因是性能,這樣就減少了需要復(fù)制的數(shù)量翩伪。但最主要的原因是微軟還為重載打開了一道門微猖。
繼續(xù)我們的研究,注意到 Rectangle 還有第二個構(gòu)造函數(shù):
public Rectangle(Point location, Size size);
我們同樣為它匹配一個解構(gòu)方法:
public void Deconstruct(out Point location, out Size size);
var (location, size) = myRectangle;
有多少個不同數(shù)量的構(gòu)造參數(shù)就有多少個解構(gòu)函數(shù)缘屹。即使你顯式地指出類型凛剥,編譯器也無法確定有哪些解構(gòu)方法可以使用。
在 API 設(shè)計中轻姿,結(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)器指南
? 考慮在讀取元組返回值時使用解構(gòu)溺森,但要注意避免搞錯標(biāo)簽。
? 為結(jié)構(gòu)提供自定義的解構(gòu)方法窑眯。
? 記得匹配類的構(gòu)造函數(shù)中字段的順序屏积,重寫 ToString 。
? 如果結(jié)構(gòu)具有多個構(gòu)造函數(shù)磅甩,考慮提供對應(yīng)的解構(gòu)方法炊林。
? 考慮立即解構(gòu)大值元組。大值元組的總大小超過16個字節(jié)卷要,這可能帶來多次復(fù)制的昂貴代價渣聚。請注意,引用類型的變量在32位操作系統(tǒng)中的大小總是4字節(jié)僧叉,而在64位操作系統(tǒng)是8字節(jié)奕枝。
? 當(dāng)不知道在類中字段應(yīng)以何種方式排序時,請不要使用解構(gòu)方法瓶堕。
? 不要聲明多個具有同等數(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ù)覽版宛蚓,可能會注意到一點:對被忽略的參數(shù)使用星號(*)已被更改為用下劃線激捏。這樣做的部分原因是在函數(shù)式編程中通常出于同樣的目的使用了下劃線。其他類似的選擇包括諸如"void" 或者 "ignore" 的關(guān)鍵字苍息。
使用下劃線很方便缩幸,同時意味著 API中的設(shè)計缺陷壹置。在大多數(shù)情況中竞思,更好的方法是對忽視的 out 參數(shù)簡單地提供一個方法重載。
Out 變量指南
? 考慮用元組返回值替代 out參數(shù)钞护。
? 盡量避免使用 out 或者 ref 參數(shù)盖喷。[詳情見 框架設(shè)計指南 ]
? 考慮對忽視的 out 參數(shù)提供重載,這樣就不需要用下劃線了难咕。
局部方法和迭代器
局部方法是一個有趣的概念课梳。乍一看距辆,就像是創(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)容將會浮現(xiàn)椭懊。
匿名方法 vs. 局部方法
當(dāng)你創(chuàng)建一個普通的匿名方法時诸蚕,總是會創(chuàng)建一個對應(yīng)的隱藏類來存儲該匿名方法。該隱藏類的實例將被創(chuàng)建并存儲在該類的靜態(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;
}
對于匿名方法來說奕纫,隱藏類每次創(chuàng)建新實例時都要求外部父方法被調(diào)用。這確保每次調(diào)用時烫沙,會在父方法和匿名方法共享數(shù)據(jù)副本匹层。
這種設(shè)計的缺點是每次調(diào)用匿名方法需要實例化一個新對象。這就帶來了昂貴的使用成本锌蓄,同時加重垃圾回收的壓力升筏。
反觀局部方法,使用隱藏結(jié)構(gòu)取代了隱藏類瘸爽。這就允許繼續(xù)存儲上一次調(diào)用的數(shù)據(jù)您访,避免了每次都要實例化對象。與匿名方法一樣剪决,局部方法實際存儲在隱藏結(jié)構(gòu)中灵汪。
委托
創(chuàng)建匿名方法或局部方法時,通常會將其封裝到委托柑潦,以便在事件處理程序或者 LINQ 表達(dá)式中調(diào)用享言。
根據(jù)定義,匿名方法是匿名的渗鬼。所以為了使用它览露,往往需要當(dāng)成委托存儲在一個變量或參數(shù)。
委托不可以指向結(jié)構(gòu)(除非他們被裝箱了譬胎,那就是奇怪的語義)差牛。所以如果你創(chuàng)建了一個委托并指向一個局部方法命锄,編譯器將會創(chuàng)建一個隱藏類代替隱藏結(jié)構(gòu)。如果該局部方法是一個閉包偏化,那么每次調(diào)用父方法時都會創(chuàng)建一個隱藏類的新實例脐恩。
迭代器
在C#中,使用 yield 返回的 IEnumerable<T> 不能立即驗證其參數(shù)侦讨。相反被盈,直到在匿名枚舉器中調(diào)用 MoveNext,才可以對其參數(shù)進(jìn)行驗證搭伤。
這在 VB 中不是問題只怎,因為它支持 匿名迭代器。下面有一個來自MSDN的示例:
Public Function GetSequence(low As Integer, high As Integer) _
As IEnumerable
' Validate the arguments.
If low < 1 Then Throw New ArgumentException("low is too low")
If high > 140 Then Throw New ArgumentException("high is too high")
' Return an anonymous iterator function.
Dim iterateSequence = Iterator Function() As IEnumerable
For index = low To high
Yield index
Next
End Function
Return iterateSequence()
End Function
在當(dāng)前的 C# 版本中怜俐,GetSequence的迭代器需要完全獨立的方法身堡。而在 C# 7中,可以使用局部方法實現(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)建一個狀態(tài)機(jī)贴谎,所以它們的行為就像在隱藏類中作為委托返回閉包。
匿名方法和局部方法指南
? 當(dāng)不需要委托時季稳,使用局部方法代替匿名方法擅这,尤其是涉及到閉包。
? 當(dāng)返回一個需要驗證參數(shù)的 IEnumerator 時景鼠,使用局部迭代器仲翎。
? 考慮將局部方法放到方法的開頭或結(jié)尾處,以便與父方法區(qū)分來铛漓。
? 避免在性能敏感的代碼中使用帶委托的閉包溯香,這適用于匿名方法和局部方法。
引用返回浓恶、局部引用以及引用屬性
結(jié)構(gòu)具有一些有趣的性能特性玫坛。由于他們與其父數(shù)據(jù)結(jié)構(gòu)一起存儲,沒有普通類的頭開銷包晰。這意味著你可以非常密集地存儲在數(shù)組中湿镀,很少或不浪費空間。除了減少內(nèi)存總體開銷外伐憾,還帶來了極大的優(yōu)勢勉痴,使 CPU 緩存更高效。這就是為什么構(gòu)建高性能應(yīng)用程序的人喜歡結(jié)構(gòu)塞耕。
但是如果結(jié)構(gòu)太大的話蚀腿,需要避免不必要的復(fù)制。微軟的指南建議為16個字節(jié)扫外,足夠存儲2個 doubles 或者 4 個 integers莉钙。這不是很多,盡管有時可以使用位域 (bit-fields)來擴(kuò)展筛谚。
局部引用
這樣做的一個方法是使用智能指針磁玉,所以你永遠(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;
}
}
你會注意到的第一件事是沒有使用 for-each蚊伞。為了避免復(fù)制,仍然使用舊式的 for 循環(huán)吮铭。即使如此时迫,所有的讀和寫操作都是直接在 m_Entries 數(shù)組中操作。
使用 C# 7 的局部引用谓晌,明顯地減少混亂而不改變語義掠拳。
for (var i = 0; i < m_Entries.Length; i++)
{
ref Entry entry = ref m_Entries[i]; //create a reference
if (string.Equals(entry.Details.ClrName, item.Key, StringComparison.OrdinalIgnoreCase)
|| string.Equals(entry.Details.SqlName, item.Key, StringComparison.OrdinalIgnoreCase))
{
var value = item.Value ?? DBNull.Value;
if (value == DBNull.Value)
{
if (!ignoreNullProperties)
parts.Add($"{entry.Details.QuotedSqlName} IS NULL");
}
else
{
entry.ParameterValue = value;
entry.UseParameter = true;
parts.Add($"{entry.Details.QuotedSqlName} = {entry.Details.SqlVariableName}");
}
found = true;
keyFound = true;
break;
}
}
這是因為 "局部引用" 真的是一個安全的指針。我們之所以說它 “安全” 纸肉,是因為編譯器指向不允許任何臨時變量溺欧,諸如普通方法的結(jié)果。
如果你很想知道 " ref var entry = ref m_Entries[i];" 是不是有效的語法(是的)柏肪,無論如何也不能這么做姐刁,會造成混亂。 ref 既是用于聲明烦味,又不會被用到聂使。(譯者:這里應(yīng)該是指 entry 的 ref 修飾吧)
引用返回
引用返回豐富了本地方法,允許創(chuàng)建無副本的方法谬俄。
繼續(xù)之前的示例岩遗,我們可以將搜索結(jié)果輸出推到其靜態(tài)方法。
static ref Entry FindColumn(Entry[] entries, string searchKey)
{
for (var i = 0; i < entries.Length; i++)
{
ref Entry entry = ref entries[i]; //create a reference
if (string.Equals(entry.Details.ClrName, searchKey, StringComparison.OrdinalIgnoreCase)
|| string.Equals(entry.Details.SqlName, searchKey, StringComparison.OrdinalIgnoreCase))
{
return ref entry;
}
}
throw new Exception("Column not found");
}
在這個例子中凤瘦,我們返回了一個數(shù)組元素的引用宿礁。你也可以返回對象中字段的引用,使用引用屬性(見下文)和引用參數(shù)蔬芥。
ref int Echo(ref int input)
{
return ref input;
}
ref int Echo2(ref Foo input)
{
return ref Foo.Field;
}
引用返回的一個有趣的功能是調(diào)用者可以選擇是否使用它梆靖。下面兩行代碼同樣有效:
Entry copy = FindColumn(m_Entries, "FirstName");
ref Entry reference = ref FindColumn(m_Entries, "FirstName");
引用返回和引用屬性
你可以創(chuàng)建一個引用返回風(fēng)格的屬性,但只能用于該屬性只讀的情況下笔诵。例如:
public ref int Test { get { return ref m_Test; } }
對于不可變結(jié)構(gòu)來說返吻,這種模式似乎毫不傷腦。調(diào)用者不需要花費額外的功夫乎婿,就可以將其視為引用值或普通值测僵。
對于可變的結(jié)構(gòu),事情變得有趣起來。首先捍靠,這修復(fù)了一不小心就會通過修改屬性而改變結(jié)構(gòu)返回值的老問題沐旨,只與值變化共進(jìn)退。
考慮以下的類:
public class Shape
{
Rectangle m_Size;
public Rectangle Size { get { return m_Size; } }
}
var s = new Shape();
s.Size.Width = 5;
在 C# 1中榨婆,size 將保持不變磁携。在 C# 6中,將觸發(fā)一個編譯器錯誤良风。在 C# 7 中谊迄,我們只是加了個 ref 修飾,卻能跑起來烟央。
public ref Rectangle Size { get { return ref m_Size; } }
乍一看就像你一旦想覆蓋 size 的值就會被阻止统诺。但事實證明,仍然可以編寫如下代碼:
var rect = new Rectangle(0, 0, 10, 20);
s.Size = rect;
即使該屬性是“只讀”疑俭,也將如期執(zhí)行粮呢。這個對象清楚自己不會返回一個 Rectangle對象,而是保留指向 Rectangle對象所在位置的指針怠硼。
現(xiàn)在有了新的問題鬼贱,不可變結(jié)構(gòu)不再是永恒的。即使單個字段不能被更改香璃,值卻被引用屬性替換了这难。C# 將通過拒絕執(zhí)行該語法來警告你:
readonly int m_LineThickness;
public ref int LineThickness { get { return ref m_LineThickness; } }
引用返回和索引器
對于引用返回和局部引用最大的限制可能就是需要一個固定的指針。
考慮這行代碼:
ref int x = ref myList[0];
這樣的代碼無效葡秒,因為列表不像數(shù)組姻乓,在讀取其值時會創(chuàng)建一個副本結(jié)構(gòu)。下面是對 List<T> 實現(xiàn) 引用的源碼:
public T this[int index] {
get {
// Following trick can reduce the range check by one
if ((uint) index >= (uint)_size) {
ThrowHelper.ThrowArgumentOutOfRangeException();
}
Contract.EndContractBlock();
return _items[index]; <-- return makes a copy
}
這同樣適用于 ImmutableArray<T> 和 訪問 IList<T> 接口的普通數(shù)組眯牧。但是蹋岩,您可以實現(xiàn)自己的List<T>,將其索引定義為引用返回学少。
public ref T this[int index] {
get {
// Following trick can reduce the range check by one
if ((uint) index >= (uint)_size) {
ThrowHelper.ThrowArgumentOutOfRangeException();
}
Contract.EndContractBlock();
return ref _items[index]; <-- return ref makes a reference
}
如果你這么做剪个,需要明確實現(xiàn) IList<T> 和 IReadOnlyList<T> 接口。這是因為引用返回具有與普通返回值不同的簽名版确,因此不能滿足接口的要求扣囊。
由于索引器實際上只是專用屬性,它們與引用屬性具有相同的限制; 這意味著您無法顯式定義 setter绒疗,而索引器卻是可寫的侵歇。
引用返回、局部引用和引用屬性指南
? 在使用數(shù)組的方法中吓蘑,考慮使用引用返回而不是索引值
? 在擁有結(jié)構(gòu)的自定義集合類中惕虑,對索引器考慮使用引用返回代替一般的返回結(jié)果。
? 將包含可變結(jié)構(gòu)體的屬性暴露為引用屬性。
? 不要將包含不可變結(jié)構(gòu)的屬性暴露為引用屬性溃蔫。
? 不要在不可變或只讀類上暴露引用屬性健提。
? 不要在不可變或只讀集合類上暴露引用索引器。
ValueTask 和通用異步返回類型
當(dāng)Task類被創(chuàng)建時酒唉,它的主要角色是簡化多線程編程矩桂。它創(chuàng)建一種將長時間運行的操作推入線程池的通道沸移,并在 UI線程上推遲讀取結(jié)果痪伦。而當(dāng)你使用 fork-join 模式并發(fā)時,效果顯著雹锣。
隨著.NET 4.5中引入了 async/await 网沾,一些缺陷也開始顯現(xiàn)。正如我們在2011年的反饋(詳見 Task Parallel Library Improvements in .NET 4.5)蕊爵,創(chuàng)建一個 Task對象所花費的時間比可接受的時間長辉哥,因此必須重寫其內(nèi)部,結(jié)果是創(chuàng)建Task<Int32> 所需的時間縮短了49%至55%攒射,并在大小上減小了52%醋旦。
這是很好的一步,但 Task 仍然分配了內(nèi)存会放。所以當(dāng)你在緊湊循環(huán)中使用它饲齐,如下所示將產(chǎn)生大量的垃圾。
while (await stream.ReadAsync(buffer, offset, count) != 0)
{
//process buffer
}
而且如前所述咧最, C# 高性能代碼的關(guān)鍵在于減少內(nèi)存分配和隨后的GC循環(huán)捂人。微軟的Joe Duffy在 Asynchronous Everything 的文章中寫到:
首先,請記住矢沿,Midori 被整個操作系統(tǒng)用于內(nèi)存垃圾回收滥搭。我們必須學(xué)到了一些必要的經(jīng)驗教訓(xùn),以便充分發(fā)揮作用捣鲸。但我想說的主要是避免不必要的分配瑟匆,分配越多麻煩越多,特別是短命對象栽惶。早期 .NET世界中流傳著一句口頭禪:Gen0 集合是無代價的愁溜。不幸的是,這形成了很多.NET的庫代碼濫用媒役。Gen0 集合存在著中斷祝谚、弄臟緩存以及在高并發(fā)的系統(tǒng)中有高頻問題。
這里的真正解決方案是創(chuàng)建一個基于結(jié)構(gòu)的 task酣衷,而不是使用堆分配的版本交惯。這實際上是以System.Threading.Tasks.Extensions 中的 ValueTask<T>創(chuàng)建。并且因為 await 已經(jīng)任何暴露的方法中工作了,所以你可以使用它。
手動暴露ValueTask<T>
ValueTask<T>的基本用例是預(yù)期結(jié)果在大部分時間是同步的席爽,并且想要消除不必要的內(nèi)存分配意荤。首先,假設(shè)你有一個傳統(tǒng)的基于 task 的異步方法只锻。
public async Task<Customer> ReadFromDBAsync(string key)
然后我們將其封裝到一個緩存方法中:
public ValueTask<Customer> ReadFromCacheAsync(string key)
{
Customer result;
if (_Cache.TryGetValue(key, out result))
return new ValueTask<Customer>(result); //no allocation
else
return new ValueTask<Customer>(ReadFromCacheAsync_Inner(key));
}
并添加一個輔助方法來構(gòu)建異步狀態(tài)機(jī)玖像。
async Task<Customer> ReadFromCacheAsync_Inner(string key)
{
var result = await ReadFromDBAsync(key);
_Cache[key] = result;
return result;
}
有了這一點,調(diào)用者可以使用與 ReadFromDBAsync 完全相同的語法來調(diào)用ReadFromCacheAsync;
async Task Test()
{
var a = await ReadFromCacheAsync("aaa");
var b = await ReadFromCacheAsync("bbb");
}
通用異步
雖然上述模式并不困難齐饮,但實施起來相當(dāng)乏味捐寥。而且我們知道,編寫代碼越繁瑣祖驱,出現(xiàn)簡單的錯誤就越有可能握恳。所以目前 C# 7 的提議是提供通用異步返回結(jié)果。
根據(jù)目前的設(shè)計捺僻,你只能使用異步關(guān)鍵字乡洼,并且方法返回 Task、Task<T>或者 void匕坯。一旦實現(xiàn)束昵,通用異步返回結(jié)果將會擴(kuò)展到任何 tasklike 方法上去。一些人認(rèn)為 tasklike 需要有一個 AsyncBuilder 屬性葛峻。這表明輔助類被用于創(chuàng)建 tasklike 對象锹雏。
在這個設(shè)計的注意事項中,微軟估計大概有五種人實際上會創(chuàng)建 tasklike 類泞歉,從而被普遍接受逼侦。其他人都很可能也像這五分之一。這是我們上面使用新語法的例子:
public async ValueTask<Customer> ReadFromCacheAsync(string key)
{
Customer result;
if (_Cache.TryGetValue(key, out result))
{
return result; //no allocation
}
else
{
result = await ReadFromDBAsync(key);
_Cache[key] = result;
return result;
}
}
如您所見腰耙,我們已經(jīng)去除了輔助方法榛丢,除了返回類型,它看起來像任何其他異步方法一樣挺庞。
何時使用 ValueTask<T>
所以應(yīng)該使用 ValueTask<T> 代替 Task<T>? 完全不必要晰赞,這可能有點難以理解,所以我們將引用相關(guān)文檔:
方法可能會返回一個該值類型的實例选侨,當(dāng)它們的操作可以同時執(zhí)行掖鱼,同時被頻繁喚起(invoked)。這時援制,對于Task<TResult>戏挡,每一次調(diào)用都是昂貴的成本,應(yīng)該被禁止晨仑。
使用 ValueTask<TResult> 代替 Task<TResult> 需要權(quán)衡利弊褐墅。例如拆檬,雖然 ValueTask<TResult> 可以避免分配,并且成功返回結(jié)果是可以同步返回的妥凳。然而它需要兩個字段竟贯,而 Task<TResult> 作為引用類型只是一個字段。這意味著調(diào)用方法最終返回的是兩個數(shù)據(jù)而不是一個數(shù)據(jù)逝钥,這就會有更多的數(shù)據(jù)被復(fù)制屑那。同時意味著如果在異步方法中需要等待時,只返回其中一個艘款,這會導(dǎo)致該異步方法的狀態(tài)機(jī)變得更大持际。因為要存儲兩個字段的結(jié)構(gòu)而不是一個引用。
再進(jìn)一步磷箕,使用者通過 await 來獲取異步操作的結(jié)果选酗,ValueTask<TResult> 可能會導(dǎo)致更復(fù)雜的模型阵难,實際上就會導(dǎo)致分配更多的內(nèi)存岳枷。例如,考慮到一個方法可能返回一個普通的已緩存 task 的結(jié)果Task<TResult>呜叫,或者是一個 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)該是返回一個 Task 或Task<TResult>。除非性能分析證明使用 ValueTask<TResult> 優(yōu)于Task<TResult>箱硕。Task.CompletedTask 屬性可能被單獨用于傳遞任務(wù)成功執(zhí)行的狀態(tài)拴竹, ValueTask<TResult> 并不提供泛型版本。
這是一段相當(dāng)長的段落剧罩,所以我們在下面的指南中總結(jié)了這一點栓拜。
ValueTask <T>指南
? 當(dāng)結(jié)果經(jīng)常被同步返回時,請考慮在性能敏感代碼中使用 ValueTask<T>惠昔。
? 當(dāng)內(nèi)存壓力是個問題幕与,且 Tasks 不能被緩存時,考慮使用 ValueTask<T>镇防。
? 避免在公共API中暴露 ValueTask<T>啦鸣,除非有顯著的性能影響。
? 不要在調(diào)用 Task.WhenAll 或 WhenAny 中調(diào)用 ValueTask<T>来氧。
表達(dá)式體成員
表達(dá)式體成員允許消除簡單函數(shù)的括號诫给。這通常是將一個四行函數(shù)減少到一行饼齿。例如:
public override string ToString()
{
return FirstName + " " + LastName;
}
public override string ToString() => FirstName + " " + LastName;
必須注意不要過分。例如蝙搔,假設(shè)當(dāng) FirstName 為空時缕溉,您需要避免產(chǎn)生空格。你可能會這么寫:
public override string ToString() => !string.IsNullOrEmpty(FirstName) ? FirstName + " " + LastName : LastName;
但是吃型,你可能會遇到 last name 同時為空证鸥。
public override string ToString() => !string.IsNullOrEmpty(FirstName) ? FirstName + " " + LastName : (!string.IsNullOrEmpty(LastName) ? LastName : "No Name");
如您所見,很容易得意忘形地使用這個功能勤晚。所以當(dāng)你遇到有多分支條件或者 null合并操作時枉层,請克制使用。
表達(dá)式體屬性
表達(dá)式體屬性是 C# 6 的新特性赐写。在使用 Get/Set 方法處理 MVVM風(fēng)格的模型之類時鸟蜡,非常有用。
這是C#6代碼:
public string FirstName
{
get { return Get<string>(); }
set { Set(value); }
}
還有 C# 7的替代方案:
public string FirstName
{
get => Get<string>();
set => Set(value);
}
雖然沒有減少代碼行數(shù)挺邀,但大部分 line-noise 代碼已經(jīng)消失了揉忘。而且每個屬性都能這么做,積少成多端铛。
有關(guān) Get/Set 在這些示例中的工作原理的更多信息泣矛,請參閱 C#, VB.NET To Get Windows Runtime Support, Asynchronous Methods。
表達(dá)式體構(gòu)造函數(shù)
表達(dá)式體構(gòu)造函數(shù)是C# 7 的新特性禾蚕。下面有一個例子:
class Person
{
public Person(string name) => Name = name;
public string Name { get; }
}
這里的用法非常有限您朽。它只有在零個或者一個參數(shù)的情況下才有效。一旦需要將其他參數(shù)分配給字段/屬性時换淆,則必須用回傳統(tǒng)的構(gòu)造函數(shù)哗总。同時也無法初始化其他字段,解析事件處理程序等(參數(shù)驗證是可能的倍试,請參見下面的“拋出表達(dá)式”讯屈。)
所以我們的建議是簡單地忽略這個功能。它只是將單參數(shù)構(gòu)造函數(shù)看起來與一般的構(gòu)造函數(shù)不同而已易猫,同時讓代碼大小減少而已耻煤。
析構(gòu)表達(dá)式
為了使 C# 更加一致,析構(gòu)被允許寫成和表達(dá)式的成員一樣准颓,就像用在方法和構(gòu)造函數(shù)一樣哈蝇。
對于那些忘記析構(gòu)的人來說,C# 中的析構(gòu)是在 Finalize 方法上重寫System.Object攘已。雖然 C# 不這樣表達(dá):
~UnmanagedResource()
{
ReleaseResources();
}
這種語法的一個問題是它看起來很像一個構(gòu)造函數(shù)炮赦,因此可以很容易地被忽略。另一個問題是它模仿 C ++中的析構(gòu)語法样勃,卻是完全不同的語義吠勘。但是已經(jīng)被使用了這么久性芬,所以我們只好轉(zhuǎn)向新的語法:
~UnmanagedResource() => ReleaseResources();
現(xiàn)在我們有一行孤立的、容易忽略的代碼剧防,用于終結(jié)對象生命周期植锉。這不是一個簡單的 屬性 或 ToString 方法,而是很重大的操作峭拘,需要顯眼一些俊庇。所以我建議不要使用它。
表達(dá)式體成員指南
? 為簡單的屬性使用表達(dá)式體成員鸡挠。
? 為方法重載使用表達(dá)式體成員辉饱。
? 簡單的方法考慮使用表達(dá)式體成員。
? 不要在表達(dá)式體成員使用多分支條件(a拣展?b:c)或 null 合并運算符(x ?? y)彭沼。
? 不要為 構(gòu)造函數(shù) 和 析構(gòu)函數(shù) 中使用表達(dá)式成員。
拋出表達(dá)式
表面上备埃,編程語言一般可以分為兩種:
- 一切都是表達(dá)式
- 語句姓惑、聲明和表達(dá)式都是獨立的概念
Ruby是前者的一個實例,甚至其聲明也是表達(dá)式瓜喇。相比之下挺益,Visual Basic代表后者,語句和表達(dá)式之間有很強(qiáng)的區(qū)別乘寒。例如,對于 "if" 而言匪补,當(dāng)它獨立存在時伞辛,以及作為表達(dá)式中的一部分時,是完全不同的語法夯缺。
C#主要是第二陣營蚤氏,但存在著 C語言的遺產(chǎn),允許你處理語句踊兜,當(dāng)成表達(dá)式一樣竿滨。可以編寫如下代碼:
while ((current = stream.ReadByte()) != -1)
{
//do work;
}
首先捏境,C#7 允許使用非賦值語句作為表達(dá)式∮谟危現(xiàn)在可以在表達(dá)式的任何地方放置 “throw” 語句,不用對語法做任何更改垫言。以下是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();
}
在這些例子中贰剥,很容易看出會發(fā)生什么情況。但是如果我們移動拋出表達(dá)式的位置呢筷频?
return (parts.Length == 0) ? throw new InvalidOperationException("No name!") : parts[0];
這樣看來就不夠易讀了蚌成。而左右的語句是相關(guān)的前痘,中間的語句與他們無關(guān)。從第一個版本看担忧,左邊是預(yù)期分支芹缔,右邊是錯誤分支。第二個版本的錯誤分支將預(yù)期分支分成兩半瓶盛,打破整條流程乖菱。
我們來看另一個例子。這里我們摻入一個函數(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)記錯誤分支。
這樣可以看到隨著參數(shù)改變位置实辑,上下文如何變化捺氢。
拋出表達(dá)式指南
? 在分支/返回語句中,考慮將拋出表達(dá)式放在條件(a剪撬?b:c)和 null 合并運算符(x ?? y)的右側(cè)摄乒。
? 避免將拋出表達(dá)式放到條件運算的中間位置。
? 不要將拋出表達(dá)式放在方法的參數(shù)列表中残黑。
有關(guān)異常如何影響 API設(shè)計的更多信息馍佑,請參閱 Designing with Exceptions in .NET。
模式匹配 和 加強(qiáng) Switch 語句
模式匹配(加強(qiáng)了 Switch 語句)對API設(shè)計沒有任何影響梨水。所以雖然可以使異構(gòu)集合的處理變得更加容易拭荤,但最好的情況還是盡可能地使用共享接口和多態(tài)性。
也就是說疫诽,有些細(xì)節(jié)還是要注意的舅世。考慮這個八月份發(fā)布的例子:
switch(shape)
{
case Circle c:
WriteLine($"circle with radius {c.Radius}");
break;
case Rectangle s when (s.Width == s.Height):
WriteLine($"{s.Width} x {s.Height} square");
break;
case Rectangle r:
WriteLine($"{r.Width} x {r.Height} rectangle");
break;
default:
WriteLine("<unknown shape>");
break;
case null:
throw new ArgumentNullException(nameof(shape));
}
以前奇徒,case的順序并不重要雏亚。在 C# 7 中,像 Visual Basic一樣逼龟,switch語句幾乎嚴(yán)格按順序執(zhí)行评凝。對于 when 表達(dá)式同樣適用。
實際上腺律,您希望最常見的情況是 switch 語句中的第一種情況奕短,就像在一系列 if-else-if 語句塊中一樣宜肉。同樣,如果任何檢查特別昂貴翎碑,那么它應(yīng)該越靠近底部谬返,只在必要時才執(zhí)行。
順序規(guī)則的例外是默認(rèn)情況日杈。它總是被最后處理遣铝,不管它的實際順序是什么。這會使代碼更難理解莉擒,所以我建議將默認(rèn)情況放在最后酿炸。
模式匹配表達(dá)式
雖然 switch 語句可能是 C# 中最常用的模式匹配; 但并不是唯一的方式。在運行時求值的任何布爾表達(dá)式都可以包含模式匹配表達(dá)式涨冀。
下面有一個例子填硕,它判斷變量 'o' 是否是一個字符串,如果是這樣鹿鳖,則嘗試將其解析為一個整數(shù)扁眯。
if (o is string s && int.TryParse(s, out var i))
{
Console.WriteLine(i);
}
注意如何在模式匹配中創(chuàng)建一個名為'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)在還不知道新的模式匹配代碼是否比以前的方式更有效,但它可能會消除一些冗余的類型檢查踪蹬。
一起維護(hù)這個在線文檔
C# 7 的新特性仍然很新鮮搀暑,而且關(guān)于它們在現(xiàn)實世界中如何運行假褪,還需要多多了解狭莱。所以如果你看到一些你不同意的東西僵娃,或者這些指南中沒有的話,請讓我們知道腋妙。
關(guān)于作者
喬納森·艾倫(Jonathan Allen)在90年代末期開始從事衛(wèi)生診所的MIS項目,從 Access 和 Excel 到企業(yè)解決方案讯榕。在為金融部門編寫自動化交易系統(tǒng)五年之后骤素,他成為各種項目的顧問,包括機(jī)器人倉庫的UI愚屁,癌癥研究軟件的中間層以及房地產(chǎn)保險公司的大數(shù)據(jù)需求济竹。在空閑時間,他學(xué)習(xí)和書寫16世紀(jì)以來的武術(shù)知識霎槐。
本文采用 知識共享署名-非商業(yè)性使用-相同方式共享 3.0 中國大陸許可協(xié)議
轉(zhuǎn)載請注明:作者 張蘅水