從 C# 1.0 到 C# 9.0,歷代 C# 語言特性一覽

C# 版本歷史記錄

從 C# 1.0 到 C# 9.0,歷代 C# 語言特性一覽.png

說明:因爲Markdown下維護這樣複雜的表格有一點麻煩,故,這裏以圖片形式展示出來,如後續內容有更新,請點擊 這裏 訪問原始筆記鏈接。爲知筆記 的表格渲染在移動端表現不佳,爲了獲得更好的閱讀體驗,請在電腦端訪問查看。

C# 版本特性說明

現在是 2021 年,相信 C# 7.0 以前的版本大家都應該沒有什麼問題,因爲像博主這樣的 90 後“中年”男人,接觸的都是這個版本的 C#。所以,在這裏我們主要講解大家C# 7.0、8.0 以及 9.0 的語法特性。考慮到文章篇幅有限,這裏選取的都是博主個人比較喜歡的語法特性,如果這裏沒有你喜歡的特性,請參考文章末尾的參考鏈接。如果這裏的特性你都不喜歡,請你馬上關掉這個網頁,願這個世界:Love & Peace。可能你會感覺到我說話變得小心翼翼起來,因爲這個世界上有種叫做“槓精”的生物,當它從我的隻言片語裏讀出那些挫敗感的時候,終於有了嘲笑我們這批步入30歲行列的90後的底氣,沒錯,我在最近的博客評論中被讀者“嘲諷”了,讓暴風雨來得更猛烈一些吧!

C# 7.0

在 C# 7.0 中,我個人比較喜歡的特性主要有以下幾個:元組和棄元更多的 expression-bodied 成員out 變量異步 Main 方法模式匹配引發表達式

元組和棄元

這個概念乍聽起來可能會有一點陌生,其實,按我的理解,這就是增強的元組語法,終於可以擺脫Item1Item2…啦:

 //示例1
(string Alpha, string Beta) namedLetters = ("a", "b");
Console.WriteLine($"{namedLetters.Alpha}, {namedLetters.Beta}");
 //示例2
var alphabetStart = (Alpha: "a", Beta: "b");
Console.WriteLine($"{alphabetStart.Alpha}, {alphabetStart.Beta}");
//示例3
int count = 5;
string label = "Colors used in the map";
var pair = (count, label);
Console.WriteLine(pair);

有一段時間,前端同事總和我吹噓 ES6 裏面的解構多麼多麼好用!對此,我想說,C# 一樣可以解構,假設我們現在有下面的一個方法:

static (string, double, double) GetLocation() 
 {
   
   
    var city = "西安市";
    var lat = 33.42d;
    var lon = 107.40d;
    return (city, lon, lat);
}

這就是簡化後的元組的用法,如果是以前,我們還需要返回一個Tuple<string, double, double>。此時,如果我們需要解析城市名稱及其經緯度,可以這樣做:

//示例4
(string city, double lon, double lat) = GetLocation();
Console.WriteLine($"{city},({lon},{lat})");

OK,那麼什麼又是棄元呢?繼續以上面的代碼爲例,如果我不關心經緯度,只需要城市名稱又該怎麼辦呢?人家的方法返回的是一個3元的結果,而我們只需要其中的1元,此時,就有了所謂棄元的概念:

(string city, _, _) = GetLocation();
Console.WriteLine($"{city}");

在 C# 中可以使用下劃線_來表示要捨棄的元,是爲棄元,怎麼樣?你學會了嗎?

更多的 expression-bodied 成員

這部分同樣是經過強化的 Lambda 表達式,之前我們可以在成員函數和 只讀屬性上使用 Lambda 表達式,而現在,我們可以將其運用在構造函數終結器以及 getset訪問器:

// Expression-bodied constructor
public ExpressionMembersExample(string label) => this.Label = label;

// Expression-bodied finalizer
~ExpressionMembersExample() => Console.Error.WriteLine("Finalized!");

private string label;

// Expression-bodied get / set accessors.
public string Label
{
   
   
    get => label;
    set => this.label = value ?? "Default label";
}

out變量

個人認爲,這是一個非常不錯的改進,終於不用再單獨聲明out變量啦:

if (int.TryParse(input, out int result))
    Console.WriteLine(result);
else
    Console.WriteLine("Could not parse input");

異步 Main 方法

顧名思義,Main 方法現在可以支持 async 關鍵字啦:

static async Task<int> Main()
{
    // This could also be replaced with the body
    // DoAsyncWork, including its await expressions:
    return await DoAsyncWork();
}

在沒有返回值的情況下,可以考慮返回Task:

static async Task Main()
{
    await SomeAsyncMethod();
}

模式匹配

主要是針對 isswitch 語句提供了增強的語法。在這裏,對於前者來說,我們可以將判斷和賦值兩個步驟合二爲一:

public static double ComputeAreaModernIs(object shape)
{
   
   
    if (shape is Square s)
        return s.Side * s.Side;
    else if (shape is Circle c)
        return c.Radius * c.Radius * Math.PI;
    else if (shape is Rectangle r)
        return r.Height * r.Length;
    // elided
    throw new ArgumentException(
        message: "shape is not a recognized shape",
        paramName: nameof(shape));
}

而對於後者來說,主要打破了傳統 switch 語句的常量模式:

public static double ComputeArea_Version3(object shape)
{
   
   
    switch (shape)
    {
   
   
        case Square s when s.Side == 0:
        case Circle c when c.Radius == 0:
            return 0;

        case Square s:
            return s.Side * s.Side;
        case Circle c:
            return c.Radius * c.Radius * Math.PI;
        default:
            throw new ArgumentException(
                message: "shape is not a recognized shape",
                paramName: nameof(shape));
    }
}

引發表達式

這個主要是針對 throw 關鍵字的增強,當我看到微軟的文檔的時候,我突然意識到,這個語法其實我用了很久啦!

//場景A:條件運算符
string arg = args.Length >= 1 ? args[0] :
    throw new ArgumentException("You must supply an argument");
//場景B:Null合併運算符
public string Name
{
   
   
    get => name;
    set => name = value ??
        throw new ArgumentNullException(
          paramName: nameof(value), 
          message: "Name cannot be null");
}
//場景C:Lambda表達式
DateTime ToDateTime(IFormatProvider provider) =>
    throw new InvalidCastException("Conversion to a DateTime is not supported.");

以上,就是 C# 7.0 中我個人比較喜歡的語法特性。需要了解所有 C# 7.0 語法特性的小夥伴們,則可以參考這裏:C# 7.0 - C# 7.3 中的新增功能

C# 8.0

在 C# 8.0 中,我個人比較喜歡的特性主要有以下幾個:默認接口方法異步流索引和範圍

默認接口方法

關於這個,我覺得有點多此一舉,如果一定要有一個默認行爲,那你用繼承來實現不就好啦,接口本來就是用來實現的啊摔!

public class ChineseSayHello : ISayHello
{
   
   
    public string Who {
   
    get; set; }
}

public interface ISayHello
{
   
   
    private const string DefaultPersopn = "Anumouse";
    string Who {
   
    get; set; }
    void SayHello()
    {
   
   
        Who = DefaultPersopn;
         Console.WriteLine($"Hello, {Who}");
    }
 }

在上面這個例子裏,ChineseSayHello沒有實現SayHello()方法不影響編譯,因爲ISayHello有默認實現,可正因爲如此,SayHello()方法屬於ISayHello,不屬於ChineseSayHello

//正確,可以編譯
var sayHello = new ChineseSayHello() as ISayHello;
sayHello.SayHello();
//錯誤,無法編譯
 var sayHello = new ChineseSayHello();
sayHello.SayHello();

異步流

該特性可以看作是IEnumerable<T>的一個延伸,即IAsyncEnumerable<T>,主要有下面三個屬性:

  • 它是用 async 修飾符聲明的。
  • 它將返回 IAsyncEnumerable。
  • 該方法包含用於在異步流中返回連續元素的 yield return 語句。

下面是一個來自微軟官方的基本示例:

//生成異步流
public static async System.Collections.Generic.IAsyncEnumerable<int> GenerateSequence()
{
   
   
    for (int i = 0; i < 20; i++)
    {
   
   
        await Task.Delay(100);
        yield return i;
    }
}
//枚舉異步流
await foreach (var number in GenerateSequence())
{
   
   
    Console.WriteLine(number);
}

和異步流相關的一個概念是:異步可釋放,即 System.IAsyncDisposable,這個可以參考:實現 DisposeAsync 方法

索引和範圍

關於這個,我們換一種說法,可能大家就能接受啦!是什麼呢?答案是:切片。切片語法博主經常在 Python 中使用,想不到有生之年居然可以在 C# 裏用到這個語法。不過,這個語法糖怎麼看都不甜啊,因爲沒那味兒!

var words = new string[]
{
   
   
                // index from start index from end
    "The", // 0 ^9
    "quick", // 1 ^8
    "brown", // 2 ^7
    "fox", // 3 ^6
    "jumped", // 4 ^5
    "over", // 5 ^4
    "the", // 6 ^3
    "lazy", // 7 ^2
    "dog" // 8 ^1
};  
//取最後一個元素
Console.WriteLine($"The last word is {words[^1]}");
//獲取第一個元素到第三個元素
var quickBrownFox = words[1..4];
//獲取倒數第一個元素到倒數第二個元素
var lazyDog = words[^2..^0];
//獲取全部元素
var all = words[..];
//獲取開始到第三個元素
var firstPhrase = words[..4];
//獲取結束到倒數第二個元素
var lastPhrase = words[6..];

看起來這些東西在 Python 裏都有啊,到底是哪裏除了問題呢?我覺得更多的是符號上的不同吧, ^ 這個符號除了表示指數的意思以外,還有按位進行異或運算的意思,所以,這個語法糖加進來以後就會顯得相當混亂,而 .. 這個符號顯然沒有 : 寫起來方便啊,所以,雖然 C# 從 C# 8.0 開始有了切片語法,可這不是我想要的切片語法啊!

以上,就是 C# 8.0 中我個人比較喜歡的語法特性。需要了解所有 C# 8.0 語法特性的小夥伴們,則可以參考這裏:C# 8.0 中的新增功能

C# 9.0

在 C# 9.0 中,我個人比較喜歡的特性主要有以下幾個:Record頂級語句模式匹配增強

Record

record 是 C# 9.0 中提供的一個新的關鍵字,地位上等同於 classstruct,中文翻譯爲:記錄類型。這是一種引用類型,它提供合成方法來提供值語義,從而實現相等性。 默認情況下,記錄是不可變的。簡而言之,record 是不可變的引用類型。你可能會說,我們爲什麼要搞這麼一個類型出來呢?難道 class 不香嗎?

我覺得如果要回答這個問題,可以借鑑 DDD 中的實體值對象這兩個概念。實體 通常都有一個唯一的標識並且在整個生命週期中具有連續性,這一類角色通過 class 來實現一直都工作得很好。例如,每一個 User 都會有一個唯一的UserId ,我們使用 UserId 來判斷其相等性。而 值對象 則是指那些沒有唯一的標識、不可變的、通過屬性來判斷相等性。例如,我們有一個地址 Address,它由省、市、區、縣和詳細地址組成,那麼,問題來了,如果兩個 Address 的省、市、區、縣和詳細地址都相同,這兩個 Address 是不是同一個地址呢?常識告訴我們:不會,因爲它們是不同的實例。

這就是 record 出現的原因,對於上面的這個問題,我們可以來解決:

record Address
{
   
   
    public string Province {
   
    get; set; }
    public string City {
   
    get; set; }
    public string District {
   
    get; set; }
    public string County {
   
    get; set; }
}

var addr1 = new Address() {
   
    Province = "陝西省", City = "西安市", District = "雁塔區" };
var addr2 = new Address() {
   
    Province = "陝西省", City = "西安市", District = "雁塔區" };
Console.WriteLine($"addr1 == addr2:{addr1 == addr2}");

想想以前我們是怎麼做的呢?是不是要寫類似下面這樣的代碼:

if (addr1.Province == addr2.Province && addr1.City == addr2.City) {
   
   

    //屬性太多啦,我就不一個一個地比較啦,懂得都懂
}

所以,這就是 record 存在的意義。除此之外呢,這個關鍵字更多的是語法層面上的,實際上從編譯出來的 IL 來看,它本質上依然是一個類,並且它是不可變的。定義記錄類型時,編譯器會合成其他幾種方法:

  • 基於值的相等性比較方法
  • 替代 GetHashCode()
  • 複製和克隆成員
  • PrintMembers 和 ToString()

那麼,你可能還會有疑問,假如我定義了兩個不同的記錄類型,它們都擁有相同的屬性成員,如果按值相等來判斷的話,豈不是這兩個不同的記錄類型變成相同的了?這麼重要的問題,微軟怎麼可能沒有想到呢?編譯器會合成一個 EqualityContract 屬性,該屬性返回與記錄類型匹配的 Type 對象。在這裏,微軟再一次發揮了元組的威力,對於上面定義的地址,我們可以繼續使用解構語法:

(province, city, district, county) = addr1;

當然,我相信哪怕到2090年,這個世界上依然會有“槓精”:你說這玩意兒不能變?我就想變怎麼辦?答案是使用with語法:

public record Person
{
   
   
    public string LastName {
   
    get; }
    public string FirstName {
   
    get; }

    public Person(string first, string last) => (FirstName, LastName) = (first, last);
}

var person = new Person("Bill", "Wagner");
Person brother = person with {
   
    FirstName = "Paul" }; // 修改FirstName的副本
Person clone = person with {
   
    }; // 空集副本

好了,關於記錄類型就先爲大家介紹到這裏,更詳細的說明可以參考這裏:使用記錄類型

頂級語句

頂級語句,這個又是一個聽起來非常模糊的概念對不對? 大家可以看一下這篇文章:26 種不同的編程語言的 “Hello World” 程序。怎麼樣,在衆多解釋型的語言中,C#、Java 甚至 C++ 的 “Hello World” 是不是都看起來有一點臃腫?

好了,現在可以夢想成真啦!

using System;

Console.WriteLine("Hello World!");

如果覺得這樣還顯得臃腫,可以省略 using 部分:

System.Console.WriteLine("Hello World!");

當然啦,一個項目裏顯然只能有一個文件可以使用頂級語句,你可以理解爲這些代碼運行在一個看不見的Main()方法中,而Main()方法顯然只能有一個,相比下來,Python 就自由多啦,不過if __name__ == '__main__'的老梗就不再這裏展開啦!

模式匹配增強

感覺微軟在模式匹配的道路上越走越遠啊,說好的語法糖呢?這簡直是毒藥,7.0 裏面眼花繚亂的switch都還沒學會呢!

public static bool IsLetter(this char c) =>
    c is >= 'a' and <= 'z' or >= 'A' and <= 'Z';

public static bool IsLetterOrSeparator(this char c) =>
    c is (>= 'a' and <= 'z') or (>= 'A' and <= 'Z') or '.' or ',';

if (e is not null)
{
   
   
    // ...
}

以上,就是 C# 9.0 中我個人比較喜歡的語法特性。需要了解所有 C# 9.0 語法特性的小夥伴們,則可以參考這裏:C# 9.0 中的新增功能

參考鏈接

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章