[周譯見] C# 7 中的模範和實踐

原文地址:https://www.infoq.com/articles/Patterns-Practices-CSharp-7git

關鍵點

  • 遵循 .NET Framework 設計指南,時至今日,仍像十年前首次出版同樣適用。
  • API 設計相當重要,設計不當的API大大增長錯誤,同時下降可重用性。
  • 始終保持"成功之道":只作正確的事,避免犯錯。
  • 去除 "line noise" 和 "boilerplate" 類型的代碼以保持關注業務邏輯
  • 在爲了性能犧牲而可讀性以前請保持清醒

C# 7 一個主要的更新是帶來了大量有趣的新特性。雖然已經有不少文章介紹了 C# 7 能夠作哪些事,但關於如何用好 C# 7 的文章仍是不多。遵循 .NET Framework設計指南中 的原則,咱們首先經過下面的策略,獲取這些新特性的最佳作法。github

元組返回結果

在 C# 以往的編程中,從一個函數中返回多個結果但是至關的乏味。Output 關鍵詞是一種方法,但若是對於異步方法不適用。Tuple<T>(元組) 儘管囉嗦,又要分配內存,同時對於其字段又不能有描述性名稱。自定義的結構優於元組,但在一次性代碼中濫用會產生垃圾代碼。最後,匿名類型和動態類型(dynamic) 的組合很是慢,又缺少靜態類型檢查。
全部的這一切問題,在新的元組返回語法中獲得瞭解決。下面是舊語法的例子:編程

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;

 

這個函數實際的返回類型是 ValueTuple<string, string>。顧名思義,這是相似 Tuple<T> 類的輕量級結構。這解決了類型膨脹的問題,但和 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>,但如今編譯器爲函數添加了TupleElementNames 屬性,容許代碼使用描述性名稱而不是 Item1/Item2。緩存

警告:TupleElementNames 屬性只能被編譯器使用。若是在返回類型上使用反射,則只能看到 ValueTuple<T> 結構。由於這些屬性在函數返回結果的時候纔會出現,相關的信息是不存在的。安全

編譯器盡所能地爲這些臨時的類型維持一種幻覺。例如,考慮下面這些聲明:數據結構

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 說明了這種設計帶來的破壞,致使失去類型安全。很容易不當心重命名字段,會將一個元組分配給一個剛好具備相同形狀的元組。重申一下,這是由於編譯器不會認爲 (string First, string Last) 和 (string make, string model) 是不一樣的類型。多線程

ValueTuple 是可變的

關於 ValueTuple 的一個有趣的見解:它是可變的。Mads Torgersen 解釋了緣由:閉包

下面的緣由解釋了可變結構爲什麼常常是壞的設計,請不要用於元組。
若是您以常規方式封裝可變結構體,使用私有、公共的訪問器,那麼您將遇到一些意外驚嚇。緣由是儘管這些結構體被保存在只讀變量中,訪問器將悄悄在結構體的副本中生效!併發

然而,元組只有公共的、可變的字段。因爲這種設計沒有訪問器,所以不會有上述現象帶來的風險。

再且由於它們是結構體,當它們被傳遞時會被複制。線程之間不直接共享,也不會有 「共享可變狀態」 的風險。這與 System.Tuple 系列的類型相反,爲了線程安全須要保證其不可變。

[譯者]:Mutator的翻譯參考https://en.wikipedia.org/wiki/Mutator_method#C.23_example爲 C# 中的訪問器

注意他說的是「字段」,而不是「屬性」。這可能會致使基於反射的庫會有問題,這將對返回元組結果的方法形成毀滅。

元組返回結果指南

✔ 當返回結果的列表字段很小且永不會改變時,考慮使用元組返回結果而不是 out 參數。
✔ 在元組返回結果中使用帕斯卡(PascalCase)來命名描述性字段。這使得元組字段看起來像普通類和結構體上的屬性。
✔ 在讀取元組返回值時不要使用var來解構(deconstructing) ,避免意外搞錯字段。
✘ 指望的返回值中用到反射的避免使用元組。
✘ 在公開的 APIs 中請不要使用元組返回結果,若是在未來的版本中須要返回其餘字段,將字段添加到元組返回結果具備破壞性。

(譯者:deconstructing 的翻譯參考 https://zhuanlan.zhihu.com/p/25844861 中對deconstructing的翻譯,下面的部分名詞也是如此)

解構多值返回結果

回到 LookupName 的示例, 建立一個名稱變量彷佛有點惱人,只能在被局部變量單獨替換以前當即使用它。C#7 也使用所謂的 「解構」 來解決這個問題。語法有幾種變形:

(string first, string last) = LookupName(0);
(var first, var last) = LookupName(0);
var (first, last) = LookupName(0);
(first, last) = LookupName(0);

在上面示例的最後一行,假定變量 first 和 last 已經事先被聲明瞭。

解構器

儘管名字很像 「析構(destructor)」,但解構器與對象銷燬無關。正如構造函數將獨立的值組合成一個對象同樣,解構器一樣是組合和分解對象。解構器容許任何類提供上述的解構語法。讓咱們來分析一下 Rectangle 類,它有這樣的構造函數:

public Rectangle(int x, int y, int width, int height)

當你在一個新的實例中調用 ToString 時,你會獲得"{X=0,Y=0,Width=0,Height=0}"。結合這兩個事實,咱們知道了在自定義的解構函數中對字段排序。

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 參數,而不是元組。一部分緣由是性能,這樣就減小了須要複製的數量。但最主要的緣由是微軟還爲重載打開了一道門。
繼續咱們的研究,注意到 Rectangle 還有第二個構造函數:

public Rectangle(Point location, Size size);

咱們一樣爲它匹配一個解構方法:

public void Deconstruct(out Point location, out Size size);
var (location, size) = myRectangle;

有多少個不一樣數量的構造參數就有多少個解構函數。即便你顯式地指出類型,編譯器也沒法肯定有哪些解構方法可使用。
在 API 設計中,結構一般能從解構中受益。類,特別是模型或者DTOs,如 Customer 和 Employee 可能不該該有解構方法,它們沒有方法解決諸如:"應該是 (firstName, lastName, phoneNumber, email)" 仍是 " (firstName, lastName, email, phoneNumber)" 的問題。某種程度來講,你們都應該開心。

解構器指南

✔ 考慮在讀取元組返回值時使用解構,但要注意避免搞錯標籤。
✔ 爲結構提供自定義的解構方法。
✔ 記得匹配類的構造函數中字段的順序,重寫 ToString 。
✔ 若是結構具備多個構造函數,考慮提供對應的解構方法。
✔ 考慮當即解構大值元組。大值元組的總大小超過16個字節,這可能帶來屢次複製的昂貴代價。請注意,引用類型的變量在32位操做系統中的大小老是4字節,而在64位操做系統是8字節。
✘ 當不知道在類中字段應以何種方式排序時,請不要使用解構方法。
✘ 不要聲明多個具備同等數量參數的解構方法。

Out 變量

C# 7 爲 帶有 "out" 變量的調用函數提供了兩種新的語法選擇。如今能夠在函數調用中這樣聲明變量。

if (int.TryParse(s, out var i))
{
    Console.WriteLine(i);
}

另外一種選擇是徹底使用"下劃線",忽略out 變量。

if (int.TryParse(s, out _))
{
    Console.WriteLine("success");
}

若是你使用過 C# 7 預覽版,可能會注意到一點:對被忽略的參數使用星號(*)已被更改成用下劃線。這樣作的部分緣由是在函數式編程中一般出於一樣的目的使用了下劃線。其餘相似的選擇包括諸如"void" 或者 "ignore" 的關鍵字。
使用下劃線很方便,同時意味着 API中的設計缺陷。在大多數狀況中,更好的方法是對忽視的 out 參數簡單地提供一個方法重載。

Out 變量指南

✔ 考慮用元組返回值替代 out參數。
✘ 儘可能避免使用 out 或者 ref 參數。[詳情見 框架設計指南 ]
✔ 考慮對忽視的 out 參數提供重載,這樣就不須要用下劃線了。

局部方法和迭代器

局部方法是一個有趣的概念。乍一看,就像是建立匿名方法的一種更易讀的語法。下面看看他們的不一樣。

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;
}

然而,一旦你開始深刻了解,一些有趣的內容將會浮現。

匿名方法 vs. 局部方法

當你建立一個普通的匿名方法時,老是會建立一個對應的隱藏類來存儲該匿名方法。該隱藏類的實例將被建立並存儲在該類的靜態字段中。所以,一旦建立,沒有額外的開銷。
反觀局部方法,不須要隱藏類。相反,局部方法表現爲其靜態父方法。

閉包

若是您的匿名方法或局部方法引用了外部變量,則產生"閉包"。下面是示例:

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;
}

對於匿名方法來講,隱藏類每次建立新實例時都要求外部父方法被調用。這確保每次調用時,會在父方法和匿名方法共享數據副本。
這種設計的缺點是每次調用匿名方法須要實例化一個新對象。這就帶來了昂貴的使用成本,同時加劇垃圾回收的壓力。
反觀局部方法,使用隱藏結構取代了隱藏類。這就容許繼續存儲上一次調用的數據,避免了每次都要實例化對象。與匿名方法同樣,局部方法實際存儲在隱藏結構中。

委託

建立匿名方法或局部方法時,一般會將其封裝到委託,以便在事件處理程序或者 LINQ 表達式中調用。
根據定義,匿名方法是匿名的。因此爲了使用它,每每須要當成委託存儲在一個變量或參數。
委託不能夠指向結構(除非他們被裝箱了,那就是奇怪的語義)。因此若是你建立了一個委託並指向一個局部方法,編譯器將會建立一個隱藏類代替隱藏結構。若是該局部方法是一個閉包,那麼每次調用父方法時都會建立一個隱藏類的新實例。

迭代器

在C#中,使用 yield 返回的 IEnumerable<T> 不能當即驗證其參數。相反,直到在匿名枚舉器中調用 MoveNext,才能夠對其參數進行驗證。
這在 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

在當前的 C# 版本中,GetSequence的迭代器須要徹底獨立的方法。而在 C# 7中,可使用局部方法實現。

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();
}

迭代器須要構建一個狀態機,因此它們的行爲就像在隱藏類中做爲委託返回閉包。

匿名方法和局部方法指南

✔ 當不須要委託時,使用局部方法代替匿名方法,尤爲是涉及到閉包。
✔ 當返回一個須要驗證參數的 IEnumerator 時,使用局部迭代器。
✔ 考慮將局部方法放到方法的開頭或結尾處,以便與父方法區分來。
✘ 避免在性能敏感的代碼中使用帶委託的閉包,這適用於匿名方法和局部方法。

引用返回、局部引用以及引用屬性

結構具備一些有趣的性能特性。因爲他們與其父數據結構一塊兒存儲,沒有普通類的頭開銷。這意味着你能夠很是密集地存儲在數組中,不多或不浪費空間。除了減小內存整體開銷外,還帶來了極大的優點,使 CPU 緩存更高效。這就是爲何構建高性能應用程序的人喜歡結構。
可是若是結構太大的話,須要避免沒必要要的複製。微軟的指南建議爲16個字節,足夠存儲2個 doubles 或者 4 個 integers。這不是不少,儘管有時可使用位域 (bit-fields)來擴展。

局部引用

這樣作的一個方法是使用智能指針,因此你永遠不須要複製。這裏有一些我仍然使用的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。爲了不復制,仍然使用舊式的 for 循環。即便如此,全部的讀和寫操做都是直接在 m_Entries 數組中操做。
使用 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;
    }
}

這是由於 "局部引用" 真的是一個安全的指針。咱們之因此說它 「安全」 ,是由於編譯器指向不容許任何臨時變量,諸如普通方法的結果。
若是你很想知道 " ref var entry = ref m_Entries[i];" 是否是有效的語法(是的),不管如何也不能這麼作,會形成混亂。 ref 既是用於聲明,又不會被用到。(譯者:這裏應該是指 entry 的 ref 修飾吧)

引用返回

引用返回豐富了本地方法,容許建立無副本的方法。
繼續以前的示例,咱們能夠將搜索結果輸出推到其靜態方法。

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");
}

在這個例子中,咱們返回了一個數組元素的引用。你也能夠返回對象中字段的引用,使用引用屬性(見下文)和引用參數。

ref int Echo(ref int input)
{
    return ref input;
}
ref int Echo2(ref Foo input)
{
    return ref Foo.Field;
}

引用返回的一個有趣的功能是調用者能夠選擇是否使用它。下面兩行代碼一樣有效:

Entry copy = FindColumn(m_Entries, "FirstName");
ref Entry reference = ref FindColumn(m_Entries, "FirstName");

引用返回和引用屬性

你能夠建立一個引用返回風格的屬性,但只能用於該屬性只讀的狀況下。例如:

public ref int Test { get { return ref m_Test; } }

對於不可變結構來講,這種模式彷佛絕不傷腦。調用者不須要花費額外的功夫,就能夠將其視爲引用值或普通值。
對於可變的結構,事情變得有趣起來。首先,這修復了一不當心就會經過修改屬性而改變結構返回值的老問題,只與值變化共進退。
考慮如下的類:

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中,將觸發一個編譯器錯誤。在 C# 7 中,咱們只是加了個 ref 修飾,卻能跑起來。

public ref Rectangle Size { get { return ref m_Size; } }

乍一看就像你一旦想覆蓋 size 的值就會被阻止。但事實證實,仍然能夠編寫以下代碼:

var rect = new Rectangle(0, 0, 10, 20);
s.Size = rect;

即便該屬性是「只讀」,也將如期執行。這個對象清楚本身不會返回一個 Rectangle對象,而是保留指向 Rectangle對象所在位置的指針。
如今有了新的問題,不可變結構再也不是永恆的。即便單個字段不能被更改,值卻被引用屬性替換了。C# 將經過拒絕執行該語法來警告你:

readonly int m_LineThickness;
public ref int LineThickness { get { return ref m_LineThickness; } }

引用返回和索引器

對於引用返回和局部引用最大的限制可能就是須要一個固定的指針。
考慮這行代碼:

ref int x = ref myList[0];

這樣的代碼無效,由於列表不像數組,在讀取其值時會建立一個副本結構。下面是對 List<T> 實現 引用的源碼

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> 接口的普通數組。可是,您能夠實現本身的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
    }

若是你這麼作,須要明確實現 IList<T> 和 IReadOnlyList<T> 接口。這是由於引用返回具備與普通返回值不一樣的簽名,所以不能知足接口的要求。
因爲索引器實際上只是專用屬性,它們與引用屬性具備相同的限制; 這意味着您沒法顯式定義 setter,而索引器倒是可寫的。

引用返回、局部引用和引用屬性指南

✔ 在使用數組的方法中,考慮使用引用返回而不是索引值
✔ 在擁有結構的自定義集合類中,對索引器考慮使用引用返回代替通常的返回結果。
✔ 將包含可變結構體的屬性暴露爲引用屬性。
✘ 不要將包含不可變結構的屬性暴露爲引用屬性。
✘ 不要在不可變或只讀類上暴露引用屬性。
✘ 不要在不可變或只讀集合類上暴露引用索引器。

ValueTask 和通用異步返回類型

Task類被建立時,它的主要角色是簡化多線程編程。它建立一種將長時間運行的操做推入線程池的通道,並在 UI線程上推遲讀取結果。而當你使用 fork-join 模式併發時,效果顯著。
隨着.NET 4.5中引入了 async/await ,一些缺陷也開始顯現。正如咱們在2011年的反饋(詳見 Task Parallel Library Improvements in .NET 4.5),建立一個 Task對象所花費的時間比可接受的時間長,所以必須重寫其內部,結果是建立Task<Int32> 所需的時間縮短了49%至55%,並在大小上減少了52%。
這是很好的一步,但 Task 仍然分配了內存。因此當你在緊湊循環中使用它,以下所示將產生大量的垃圾。

while (await stream.ReadAsync(buffer, offset, count) != 0)
{
    //process buffer
}

並且如前所述, C# 高性能代碼的關鍵在於減小內存分配和隨後的GC循環。微軟的Joe Duffy在 Asynchronous Everything 的文章中寫到:

首先,請記住,Midori 被整個操做系統用於內存垃圾回收。咱們必須學到了一些必要的經驗教訓,以便充分發揮做用。但我想說的主要是避免沒必要要的分配,分配越多麻煩越多,特別是短命對象。早期 .NET世界中流傳着一句口頭禪:Gen0 集合是無代價的。不幸的是,這造成了不少.NET的庫代碼濫用。Gen0 集合存在着中斷、弄髒緩存以及在高併發的系統中有高頻問題。

這裏的真正解決方案是建立一個基於結構的 task,而不是使用堆分配的版本。這其實是以System.Threading.Tasks.Extensions 中的 ValueTask<T>建立。而且由於 await 已經任何暴露的方法中工做了,因此你可使用它。

手動暴露ValueTask<T>

ValueTask<T>的基本用例是預期結果在大部分時間是同步的,而且想要消除沒必要要的內存分配。首先,假設你有一個傳統的基於 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));
}

並添加一個輔助方法來構建異步狀態機。

async Task<Customer> ReadFromCacheAsync_Inner(string key)
{
    var result = await ReadFromDBAsync(key);
    _Cache[key] = result;
    return result;
}

有了這一點,調用者可使用與 ReadFromDBAsync 徹底相同的語法來調用ReadFromCacheAsync;

async Task Test()
{
    var a = await ReadFromCacheAsync("aaa");
    var b = await ReadFromCacheAsync("bbb");
}

通用異步

雖然上述模式並不困難,但實施起來至關乏味。並且咱們知道,編寫代碼越繁瑣,出現簡單的錯誤就越有可能。因此目前 C# 7 的提議是提供通用異步返回結果。
根據目前的設計,你只能使用異步關鍵字,而且方法返回 Task、Task<T>或者 void。一旦實現,通用異步返回結果將會擴展到任何 tasklike 方法上去。一些人認爲 tasklike 須要有一個 AsyncBuilder 屬性。這代表輔助類被用於建立 tasklike 對象。
在這個設計的注意事項中,微軟估計大概有五種人實際上會建立 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;
    }
}

如您所見,咱們已經去除了輔助方法,除了返回類型,它看起來像任何其餘異步方法同樣。

什麼時候使用 ValueTask<T>

因此應該使用 ValueTask<T> 代替 Task<T>? 徹底沒必要要,這可能有點難以理解,因此咱們將引用相關文檔:

方法可能會返回一個該值類型的實例,當它們的操做能夠同時執行,同時被頻繁喚起(invoked)。這時,對於Task<TResult>,每一次調用都是昂貴的成本,應該被禁止。

使用 ValueTask<TResult> 代替 Task<TResult> 須要權衡利弊。例如,雖然 ValueTask<TResult> 能夠避免分配,而且成功返回結果是能夠同步返回的。然而它須要兩個字段,而 Task<TResult> 做爲引用類型只是一個字段。這意味着調用方法最終返回的是兩個數據而不是一個數據,這就會有更多的數據被複制。同時意味着若是在異步方法中須要等待時,只返回其中一個,這會致使該異步方法的狀態機變得更大。由於要存儲兩個字段的結構而不是一個引用。

再進一步,使用者經過 await 來獲取異步操做的結果,ValueTask<TResult> 可能會致使更復雜的模型,實際上就會致使分配更多的內存。例如,考慮到一個方法可能返回一個普通的已緩存 task 的結果Task<TResult>,或者是一個 ValueTask<TResult>。若是調用者的預期結果是 Task<TResult>,能夠被諸如 Task.WhenAll 和 Task.WhenAny 的方法調用,那麼 ValueTask<TResult> 首先須要使用 ValueTask<TResult>.AsTask 將其自身轉換爲 Task<TResult> ,若是 Task<TResult> 在第一次使用沒有被緩存了,將致使分配。

所以,Task的任何異步方法的默認選擇應該是返回一個 Task 或Task<TResult>。除非性能分析證實使用 ValueTask<TResult> 優於Task<TResult>。Task.CompletedTask 屬性可能被單獨用於傳遞任務成功執行的狀態, ValueTask<TResult> 並不提供泛型版本。

這是一段至關長的段落,因此咱們在下面的指南中總結了這一點。

ValueTask <T>指南

✔ 當結果常常被同步返回時,請考慮在性能敏感代碼中使用 ValueTask<T>。
✔ 當內存壓力是個問題,且 Tasks 不能被緩存時,考慮使用 ValueTask<T>。
✘ 避免在公共API中暴露 ValueTask<T>,除非有顯著的性能影響。
✘ 不要在調用 Task.WhenAll 或 WhenAny 中調用 ValueTask<T>。

表達式體成員

表達式體成員容許消除簡單函數的括號。這一般是將一個四行函數減小到一行。例如:

public override string ToString()
{
    return FirstName + " " + LastName;
}
public override string ToString() => FirstName + " " + LastName;

必須注意不要過度。例如,假設當 FirstName 爲空時,您須要避免產生空格。你可能會這麼寫:

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");

如您所見,很容易忘乎所以地使用這個功能。因此當你遇到有多分支條件或者 null合併操做時,請剋制使用。

表達式體屬性

表達式體屬性是 C# 6 的新特性。在使用 Get/Set 方法處理 MVVM風格的模型之類時,很是有用。
這是C#6代碼:

public string FirstName
{
    get { return Get<string>(); }
    set { Set(value); }
}

還有 C# 7的替代方案:

public string FirstName
{
    get => Get<string>();                      
    set => Set(value);              
}

雖然沒有減小代碼行數,但大部分 line-noise 代碼已經消失了。並且每一個屬性都能這麼作,聚沙成塔。
有關 Get/Set 在這些示例中的工做原理的更多信息,請參閱 C#, VB.NET To Get Windows Runtime Support, Asynchronous Methods

表達式體構造函數

表達式體構造函數是C# 7 的新特性。下面有一個例子:

class Person
{
    public Person(string name) => Name = name;
    public string Name { get; }
}

這裏的用法很是有限。它只有在零個或者一個參數的狀況下才有效。一旦須要將其餘參數分配給字段/屬性時,則必須用回傳統的構造函數。同時也沒法初始化其餘字段,解析事件處理程序等(參數驗證是可能的,請參見下面的「拋出表達式」。)
因此咱們的建議是簡單地忽略這個功能。它只是將單參數構造函數看起來與通常的構造函數不一樣而已,同時讓代碼大小減小而已。

析構表達式

爲了使 C# 更加一致,析構被容許寫成和表達式的成員同樣,就像用在方法和構造函數同樣。
對於那些忘記析構的人來講,C# 中的析構是在 Finalize 方法上重寫System.Object。雖然 C# 不這樣表達:

~UnmanagedResource()
{
    ReleaseResources();
}

這種語法的一個問題是它看起來很像一個構造函數,所以能夠很容易地被忽略。另外一個問題是它模仿 C ++中的析構語法,倒是徹底不一樣的語義。可是已經被使用了這麼久,因此咱們只好轉向新的語法:

~UnmanagedResource() => ReleaseResources();

如今咱們有一行孤立的、容易忽略的代碼,用於終結對象生命週期。這不是一個簡單的 屬性 或 ToString 方法,而是很重大的操做,須要顯眼一些。因此我建議不要使用它。

表達式體成員指南

✔ 爲簡單的屬性使用表達式體成員。
✔ 爲方法重載使用表達式體成員。
✔ 簡單的方法考慮使用表達式體成員。
✘ 不要在表達式體成員使用多分支條件(a?b:c)或 null 合併運算符(x ?? y)。
✘ 不要爲 構造函數 和 析構函數 中使用表達式成員。

拋出表達式

表面上,編程語言通常能夠分爲兩種:

  • 一切都是表達式
  • 語句、聲明和表達式都是獨立的概念

Ruby是前者的一個實例,甚至其聲明也是表達式。相比之下,Visual Basic表明後者,語句和表達式之間有很強的區別。例如,對於 "if" 而言,當它獨立存在時,以及做爲表達式中的一部分時,是徹底不一樣的語法。
C#主要是第二陣營,但存在着 C語言的遺產,容許你處理語句,當成表達式同樣。能夠編寫以下代碼:

while ((current = stream.ReadByte()) != -1)
{
    //do work;
}

首先,C#7 容許使用非賦值語句做爲表達式。如今能夠在表達式的任何地方放置 「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();
}

在這些例子中,很容易看出會發生什麼狀況。可是若是咱們移動拋出表達式的位置呢?

return (parts.Length == 0) ? throw new InvalidOperationException("No name!") : parts[0];

這樣看來就不夠易讀了。而左右的語句是相關的,中間的語句與他們無關。從第一個版本看,左邊是預期分支,右邊是錯誤分支。第二個版本的錯誤分支將預期分支分紅兩半,打破整條流程。

咱們來看另外一個例子。這裏咱們摻入一個函數調用。

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);
}

咱們已經能夠看到,寫到一塊是有問題的,儘管它的LINQ並不難看。可是爲了更好地閱讀代碼,咱們使用橙色標記條件,藍色標記函數調用,黃色標記函數參數,紅色標記錯誤分支。


這樣能夠看到隨着參數改變位置,上下文如何變化。

拋出表達式指南

✔ 在分支/返回語句中,考慮將拋出表達式放在條件(a?b:c)和 null 合併運算符(x ?? y)的右側。
✘ 避免將拋出表達式放到條件運算的中間位置。
✘ 不要將拋出表達式放在方法的參數列表中。
有關異常如何影響 API設計的更多信息,請參閱 Designing with Exceptions in .NET

模式匹配 和 增強 Switch 語句

模式匹配(增強了 Switch 語句)對API設計沒有任何影響。因此雖然可使異構集合的處理變得更加容易,但最好的狀況仍是儘量地使用共享接口和多態性。
也就是說,有些細節仍是要注意的。考慮這個八月份發佈的例子:

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語句幾乎嚴格按順序執行。對於 when 表達式一樣適用。
實際上,您但願最多見的狀況是 switch 語句中的第一種狀況,就像在一系列 if-else-if 語句塊中同樣。一樣,若是任何檢查特別昂貴,那麼它應該越靠近底部,只在必要時才執行。
順序規則的例外是默認狀況。它老是被最後處理,無論它的實際順序是什麼。這會使代碼更難理解,因此我建議將默認狀況放在最後。

模式匹配表達式

雖然 switch 語句多是 C# 中最經常使用的模式匹配; 但並非惟一的方式。在運行時求值的任何布爾表達式均可以包含模式匹配表達式。
下面有一個例子,它判斷變量 'o' 是不是一個字符串,若是是這樣,則嘗試將其解析爲一個整數。

if (o is string s && int.TryParse(s, out var i))
{
    Console.WriteLine(i);
}

注意如何在模式匹配中建立一個名爲's'的新變量,而後再用於TryParse。這種方法能夠鏈式組合,構建更復雜的表達式:

if ((o is int i) || (o is string s && int.TryParse(s, out i)))
{
    Console.WriteLine(i);
}

爲了方便比較, 將上述代碼重寫成 C# 6 風格:

if (o is int)
{
    Console.WriteLine((int)o);
}
else if (o is string && int.TryParse((string) o, out i))
{
    Console.WriteLine(i);
}

如今還不知道新的模式匹配代碼是否比之前的方式更有效,但它可能會消除一些冗餘的類型檢查。

一塊兒維護這個在線文檔

C# 7 的新特性仍然很新鮮,並且關於它們在現實世界中如何運行,還須要多多瞭解。因此若是你看到一些你不一樣意的東西,或者這些指南中沒有的話,請讓咱們知道。

關於做者

喬納森·艾倫(Jonathan Allen)在90年代末期開始從事衛生診所的MIS項目,從 Access 和 Excel 到企業解決方案。在爲金融部門編寫自動化交易系統五年以後,他成爲各類項目的顧問,包括機器人倉庫的UI,癌症研究軟件的中間層以及房地產保險公司的大數據需求。在空閒時間,他學習和書寫16世紀以來的武術知識。

相關文章
相關標籤/搜索