1. 程式人生 > 其它 >說說 C# 9 新特性的實際運用

說說 C# 9 新特性的實際運用

技術標籤:C#教程c#教程

你一定會好奇:“老周,你去哪開飛機了?這麼久沒寫部落格了。”

老周:“我買不起飛機,開了個鐵礦,挖了一年半的石頭。誰知鐵礦垮了,壓死了幾條蜈蚣,什麼也沒挖著。”

所以,這麼丟死人的事,還是不要提了,爺爺從小教導我做人要低調……

一轉眼,.NET 5 要來了,同時也帶來了 C# 9。遙想當年,老周剛接觸 .NET 1.1 的時候,才剛上大學;如今已經過去13年了。歲月是把水果刀,從來不饒人啊。

老周很少去寫諸如“XXX新特性”之類的文章,總覺得沒啥用處。不過,針對 C# 9,老周想說一點什麼。

好,在開始之前,老周再次強調一下:這些語言新特性的東西,你千萬不要特意去學習,千萬不要,不要,不要,重要的事情講四遍!這些玩意兒你只要看看官方給的說明,刷一遍就能掌握了(刷這個比刷抖音有意義多了),不用去學的。如果你連這些東東也要學習成本的話,我只想說句好唱不好聽的話——你的學習能力真的值得懷疑。

好了,下面開始表演。

第一齣:record 型別
record ,我還是用原詞吧,我知道有翻譯為“記錄型別”的說法。只是,只是,老周老覺得這不太好聽,可是老周也找不出更好的詞語,還是用回 record吧。

record 是引用型別,跟 class 很像(確實差不多)。那麼,用人民群眾都熟悉的 class 不香嗎,為何要新增個 record 呢?答:為了資料比較的便捷。

不明白?沒事,往下看。最近有一位熱心鄰居送了老週一只寵物:

   public class Cat
    {
        public string Nick { get; set; }
        public string
Name { get; set; } public int Age { get; set; } }

這隻新寵物可不簡單,一頂一的高階吃貨。魚肉、豬肉、雞腿、餅乾、豆腐、麵包、水果、麵條、小麥、飛蛾……反正,只要它能塞進嘴裡的,它都吃。

接下來,我們 new 兩個寵物例項。

  // 兩個例項描述的是同一只貓
    Cat pet1 = new Cat
    {
        Nick = "松子",
        Name = "Jack",
        Age = 1
    };
    Cat pet2 = new
Cat { Nick = "松子", Name = "Jack", Age = 1 }; // 居然不是同一只貓 Console.WriteLine("同一只?{0}", pet1 == pet2);

其實,兩個例項描述的都是我家的乖乖。可是,輸出的是:

同一只?False
這是因為,在相等比較時,人家關心的型別引用——引用的是否為同一個例項。但是,在資料處理方案中,我們更關注物件中的欄位/屬性是否相等,即內容比較。

現在,把 Cat 的宣告改為 record 型別。

public record Cat
{
    public string Nick { get; set; }
    public string Name { get; set; }
    public int Age { get; set; }
}

然後同樣用上面的 pet1 和 pet2 例項進行相等比較,得到預期的結果:

同一只?True

record 型別讓你省去了重寫相等比較(重寫 Equals、GetHashCode 等方法或過載運算子)的邏輯。

實際上,程式碼在編譯後 record 型別也是一個類,但自動實現了成員相等比較的邏輯。以前你要手動去折騰的事現在全交給編譯器去幹。

假如,有一個 User 型別,用於表示使用者資訊(包括使用者名稱、密碼),然後這個 User 型別在資料處理方案中可能會產生N多個例項。例如你根據條件從EF模型中篩選出一個 User 例項 A,根據使用者輸入的登入名和密碼產生了 User 例項 B。為了驗證使用者輸入的登入資訊是否正確,如果 User 是 class,你可能要這樣判斷:

if(A.UserName == B.UserName && A.Password == B.Password)
{
    ..................
}

但要是你把 User 定義為 record 型別,那麼,一句話的工夫:

A == B

第二齣:模式匹配(Pattern Matching)
"模式匹配"這個翻譯感覺怪怪滴,老周還沒想出什麼更好的詞語。模式匹配並不是什麼神奇的東西,它只是在對變數值進行檢測時的擴充套件行為。以前,老感覺C++/C# 的 switch 語句不夠強大,因為傳統的用法裡面,每個 case 子句只能比較單個常量值。比如

int 考試成績 = 85;

switch (考試成績)
{
    case 10:
        Console.WriteLine("才考這麼點破分啊");
        break;
    case 50:
        Console.WriteLine("還差一點,就合格了");
        break;
    case 85:
        Console.WriteLine("真是秀");
        break;
    case 90:
        Console.WriteLine("奇蹟發生");
        break;
}

我幻想著,要是能像下面這樣寫就好了:

switch (考試成績)
{
    case 0:
        Console.WriteLine("缺考?");
        break;
    case > 0 && <= 30:
        Console.WriteLine("太爛了");
        break;
    case > 30 && < 60:
        Console.WriteLine("還是不行");
        break;
    case >= 60 && < 80:
        Console.WriteLine("還得努力");
        break;
    case >= 80 && < 90:
        Console.WriteLine("秀兒,真優秀");
        break;
    case >= 90 && <= 100:
        Console.WriteLine("不錯,奇蹟");
        break;
}

等了很多年很多年(“千年等一回,等……”)以後,終於可以實現了。

switch (考試成績)
{
    case 0:
        Console.WriteLine("缺考?");
        break;
    case > 0 and <= 30:
        Console.WriteLine("太爛了");
        break;
    case > 30 and < 60:
        Console.WriteLine("還是不行");
        break;
    case >= 60 and < 80:
        Console.WriteLine("還得努力");
        break;
    case >= 80 and < 90:
        Console.WriteLine("秀兒,真優秀");
        break;
    case >= 90 and <= 100:
        Console.WriteLine("不錯,奇蹟");
        break;
}

喲西,真香。

有時候,不僅要檢測物件的值,還得深入到其成員。比如下面這個例子,Order類表示一條訂單資訊。

public class Order
{
    public int ID { get; set; }
    public string Company { get; set; }
    public string ContactName { get; set; }
    public float Qty { get; set; }
    public decimal UP { get; set; }
    public DateTime Date { get; set; }
}

前不久,公司接到一筆Order,做成了收益應該不錯。

 Order od = new Order
    {
        ID = 11,
        Company = "大嘴狗貿易有限公司",
        ContactName = "陳大爺",
        Qty = 425.12f,
        UP = 1000.55M,
        Date = new(2020, 10, 27)
    };

假如我要在變數 od 上做 switch,看看,就這樣:

switch (od)
{
    case { Qty: > 1000f }:
        Console.WriteLine("發財了,發財了");
        break;
    case { Qty: > 500f }:
        Console.WriteLine("好傢伙,年度大訂單");
        break;
    case { Qty: > 100f }:
        Console.WriteLine("訂單量不錯");
        break;
}

咦?這,這是什麼鬼?莫驚莫驚,這不是鬼。它的意思是判斷 Qty 屬性的值,如果訂單貨量大於 100 就輸出“訂單量不錯”;要是訂單貨量大於 1000,那就輸出“發財了,發財了”。

但你會說,這對大括號怎麼來的呢?還記得這種 LINQ 的寫法嗎?

from x in ...
    where x.A ...
    select new {
        Prop1 = ...,
        Prop2 = ...,
        ................
    }            

new { … } 是匿名型別例項,那如果是非匿名型別呢,看看前面的 Cat 例項初始化。

 Cat {
     ..........
 }

這就對了,這對大括號就是構造某例項的成員值用的,所以,上面的 switch 語句其實是這樣寫的:

   switch (od)
    {
        case Order{ Qty: > 1000f }:
            Console.WriteLine("發財了,發財了");
            break;
        case Order{ Qty: > 500f }:
            Console.WriteLine("好傢伙,年度大訂單");
            break;
        case Order{ Qty: > 100f }:
            Console.WriteLine("訂單量不錯");
            break;
    }

Order{ … } 就是匹配一個 Order 物件例項,並且它的 Qty 屬性要符合 … 條件。由於變數 od 始終就是 Order 型別,所以,case 子句中的 Order 就省略了,變成

case { Qty: > 1000f }:
        Console.WriteLine("發財了,發財了");
        break;

如果出現多個屬性,則表示為多個屬性設定匹配條件,它們之間是“且”的關係。比如

 case { Qty: > 100f, Company: not null }:
        Console.WriteLine("訂單量不錯");
        break;

猜猜啥意思?這個是可以“望文生義”的,Qty 屬性的值要大於 100,並且 Company 屬性的值不能為 null。不為 null 的寫法是 not null,不要寫成 !null,因為這樣太難看了。

如果你的程式碼分支較少,你可以用 if 語句的,只是得配合 is 運算子。

if (od is { UP: < 3000M })
{
    Console.WriteLine("報價不理想");
}

但是,這個寫法目前有侷限性,它只能用常量值來做判斷,你要是這樣寫就會報錯。

if (od is { Date: < DateTime.Now })
{
    ................
}

DateTime.Now 不是常量值,上面程式碼無法通過編譯。

is 運算子以前是用來匹配型別的,上述的用法是它的語法擴充套件。

 object n = 5000000L;
    if(n is long)
    {
        Console.WriteLine("它是個長整型");
    }

進化之後的 is 運算子也可以這樣用:

 object n = 5000000L;
    if(n is long x)
    {
        Console.WriteLine("它是個長整型,存放的值是:{0}", x);
    }

如果你在 if 語句內要使用 n 的值,就可以順便轉為 long 型別並賦值給變數 x,這樣就一步到位,不必再去寫一句 long x = (long)n 。

如果 switch… 語句在判斷之後需要返回一個值,還可以把它變成表示式來用。咱們把前面的 Order 例子改一下。

 string message = od switch
    {
        { Qty: > 1000f }    => "發財了",
        { Qty: > 500f }     => "年度大訂單",
        { Qty: > 100f }     => "訂單量不錯",
        _                   => "未知"
    };

    Console.WriteLine(message);

這時候你得注意:

1)switch 現在是表示式,不是語句塊,所以最後大括號右邊的分號不能少;

2)因為 switch 成了表示式,就不能用 case 子句了,所以直接用具體的內容來匹配;

3)最後返回“未知”的那個下劃線(_),也就是所謂的“棄嬰”,哦不,是“棄元”,就是雖然賦了值但不需要使用的變數,可以直接丟掉。這裡就相當於 switch 語句塊中的 default 子句,當前面所有條件都不能匹配時,就返回“未知”。

第三齣:屬性的 init 訪問器
要首先得知道,這個 init 只用於只讀屬性的初始化階段,對於可讀可寫的屬性,和以前一樣,直接 get; set; 即可。

有人說這個 init 不知幹啥用,那好,咱們先不說它,先來看看 C# 前些版本中新增的屬性初始化語句。

public class Dog
{
    public int No { get; } = 0;
    public string Name { get; } = "no name";
    public int Age { get; } = 1;
}

你看,這樣就可以給屬性分配初始值了,那還要 init 幹嗎呢?

好,我給你製造一個問題——我要是這樣初始化 Dog 類的屬性,你試試看。

 Dog x = new Dog
    {
        No = 100,
        Name = "吉吉",
        Age = 4
    };

試一下,編譯會出錯吧。

在這裡插入圖片描述

有些情況,你可以在屬性定義階段分配初始值,但有些時候,你必須要在程式碼中初始化。在過去,我們會c#教程通過定義帶引數的建構函式來解決。

   public class Dog
    {
        public int No { get; } = 0;
        public string Name { get; } = "no name";
        public int Age { get; } = 1;

        public Dog(int no, string name, int age)
        {
            No = no;
            Name = name;
            Age = age;
        }
    }

然後,這樣初始化。

   Dog x = new(1001, "吉吉", 4);

可是,這樣做的裝逼指數依然不夠高,你總不能每個類都來這一招吧,雖然不怎麼辛苦,但每個類都得去寫一個建構函式,不利落。

於是,init 訪問器用得上了,咱們把 Dog 類改改。

 public class Dog
    {
        public int No { get; init; }
        public string Name { get; init; }
        public int Age { get; init; }
    }

你不用再去寫帶引數的構造函數了,例項化時直接為屬性賦值。

   Dog x = new Dog
    {
        No = 100,
        Name = "吉吉",
        Age = 4
    };

這樣一來,這些只讀屬性都有預設的初始值了。

當然,這個賦值只在初始化過程中有效,初始化之後你再想改屬性的值,沒門!

x.Name = "鼕鼕";  //錯誤
x.Age = 10;       //錯誤
 

嗯,好了,以上就是老周對 C# 9 新特性用法的一些不成文的闡述。看完後你就別說難了。