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

原文地址: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)載請注明:作者 張蘅水

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末送浊,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子丘跌,更是在濱河造成了極大的恐慌袭景,老刑警劉巖唁桩,帶你破解...
    沈念sama閱讀 218,858評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異耸棒,居然都是意外死亡荒澡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,372評論 3 395
  • 文/潘曉璐 我一進(jìn)店門与殃,熙熙樓的掌柜王于貴愁眉苦臉地迎上來单山,“玉大人,你說我怎么就攤上這事幅疼∶准椋” “怎么了?”我有些...
    開封第一講書人閱讀 165,282評論 0 356
  • 文/不壞的土叔 我叫張陵爽篷,是天一觀的道長悴晰。 經(jīng)常有香客問我,道長狼忱,這世上最難降的妖魔是什么膨疏? 我笑而不...
    開封第一講書人閱讀 58,842評論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮钻弄,結(jié)果婚禮上佃却,老公的妹妹穿的比我還像新娘。我一直安慰自己窘俺,他們只是感情好饲帅,可當(dāng)我...
    茶點故事閱讀 67,857評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著瘤泪,像睡著了一般灶泵。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上对途,一...
    開封第一講書人閱讀 51,679評論 1 305
  • 那天赦邻,我揣著相機(jī)與錄音,去河邊找鬼实檀。 笑死惶洲,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的膳犹。 我是一名探鬼主播恬吕,決...
    沈念sama閱讀 40,406評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼须床!你這毒婦竟也來了铐料?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,311評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎钠惩,沒想到半個月后柒凉,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,767評論 1 315
  • 正文 獨居荒郊野嶺守林人離奇死亡妻柒,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,945評論 3 336
  • 正文 我和宋清朗相戀三年扛拨,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片举塔。...
    茶點故事閱讀 40,090評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡绑警,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出央渣,到底是詐尸還是另有隱情计盒,我是刑警寧澤,帶...
    沈念sama閱讀 35,785評論 5 346
  • 正文 年R本政府宣布芽丹,位于F島的核電站北启,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏拔第。R本人自食惡果不足惜咕村,卻給世界環(huán)境...
    茶點故事閱讀 41,420評論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望蚊俺。 院中可真熱鬧懈涛,春花似錦、人聲如沸泳猬。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,988評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽得封。三九已至埋心,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間忙上,已是汗流浹背拷呆。 一陣腳步聲響...
    開封第一講書人閱讀 33,101評論 1 271
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留疫粥,地道東北人洋腮。 一個月前我還...
    沈念sama閱讀 48,298評論 3 372
  • 正文 我出身青樓,卻偏偏與公主長得像手形,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子悯恍,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,033評論 2 355

推薦閱讀更多精彩內(nèi)容

  • 前言 人生苦多库糠,快來 Kotlin ,快速學(xué)習(xí)Kotlin! 什么是Kotlin瞬欧? Kotlin 是種靜態(tài)類型編程...
    任半生囂狂閱讀 26,211評論 9 118
  • Spring Cloud為開發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見模式的工具(例如配置管理贷屎,服務(wù)發(fā)現(xiàn),斷路器艘虎,智...
    卡卡羅2017閱讀 134,659評論 18 139
  • 第5章 引用類型(返回首頁) 本章內(nèi)容 使用對象 創(chuàng)建并操作數(shù)組 理解基本的JavaScript類型 使用基本類型...
    大學(xué)一百閱讀 3,237評論 0 4
  • 他是一個修理工唉侄,一個手藝超群的修理工。那天野建,他與一幫工友下班回家属划,走到一個前不著村,后不著店的地方候生,他們看見有一輛...
    王虎林閱讀 253評論 0 0
  • 姓名:張弛 公司:沈陽防銹包裝材料有限責(zé)任公司 【六項精進(jìn)打卡第130天】 【知~學(xué)習(xí)】 閱讀《六項精進(jìn)》大綱0遍...
    Leo_zhang閱讀 137評論 0 0