參考資料:微軟官方文件 C# 9.0

C# 9 發行於 2020 年 11 月,跟 .NET 5 一起推出。以下是主要新功能及強化:

  1. Record
    記錄(Record)是 C# 9 新推出用來封裝不可變資料(Immutable Data)的型別,可以簡潔地寫成 ‵public record Student (int Id, string Name);`一次交代完包含的屬性。
    record 在編譯後會轉以 class 形式存在,為 Reference Type,C# 10 起支援 record struct,底層可以 struct 實現。由於 record 不可修改,如要修改需產生一個新副本再賦與新值。例如:
    public record Student (int Id, string Name);
    var s1= new Student("A1", "Jeffrey");
    var s2 = s1 with { };
    var s3 = s2 with { Name = "darkthread" };
    
    另外,record 內建 Deconstruct 方法,可將屬性分解對映成個別變數,例如:var (id, name) = student;。 如想深入了解推薦 Huanlin 老師這篇:C# 9:Record 詳解
  2. 僅供初始化的 setter
    可限定屬性只能在物件初始化的過程中給值,也就是只能在建構式、屬性初始設定式、物件初始設定式三處設定。public int ReadOnlyInt { get; } 一樣也有唯讀效果,但限定只能從建構式指定,無法寫成 new Foo() { ReadOnlyInt = 123 }
  3. 最上層陳述式
    現在 Program.cs 可以直接第一行就 Console.WriteLine(),不用寫 class Program、不需要 static void Main(string[] args),便是源自最上層陳述式帶來的創新。
  4. 模式比對增強功能:關聯式模式和邏輯模式
    C# 8 為 switch case 帶來許多花式玩法,C# 9 再支援兩種新寫法。   關聯式模式 (Relational Patterns),支援運算子 <、<=、>、>= 直接在模式中比較常數,依數值/可比較型別的區間決定:
    string WaterState(int tempF) => tempF switch
    {
        < 32  => "solid",
        32    => "solid/liquid transition",
        (> 32) and (< 212) => "liquid",
        212   => "liquid / gas transition",
        > 212 => "gas",
    };    
    
    邏輯模式 (Logical Patterns),可使用 is, and , or, not 組裝比對條件,比 when/if 好讀,像是 is not null 就很貼近自然語意。
    string Grade(int score) => score switch
    {
        < 0 or > 100 => "Invalid",
        >= 90        => "A",
        >= 80        => "B",
        >= 70        => "C",
        >= 60        => "D",
        _            => "F",
    };
    
    string Describe(object o) => o switch
    {
        // 型別模式 + 關聯式 + and
        Person { Age: >= 18 and < 65 } p => $"Adult: {p.Name}",
        Person { Age: < 18 } p            => $"Minor: {p.Name}",
        Person { Age: >= 65 } p           => $"Senior: {p.Name}",
    
        // or 列舉常值
        DayOfWeek d when d is DayOfWeek.Saturday or DayOfWeek.Sunday => "Weekend",
    
        // not null 簡潔空檢
        string s when s is not null and s.Length is > 0 => $"Text({s.Length})",
    
        _ => "Unknown"
    };
    
    另外,is 過去只能判斷型別,例如 s is string,C# 9 擴充了 is 的比對對象,可用來寫判斷函式,可讀性大增,例如:
    bool IsInRange(int x) => x is >= 10 and <= 20;
    bool IsSpecial(int x) => x is 0 or 42 or 100;
    bool HasContent(string? s) => s is not null and not "";
    
  5. PInvoke/Interop/Native API 相關
    原生大小的整數:引入與原生大小一致的新整數型別:nint 與 nuint,對映底層的 System.IntPtr 與 System.UIntPtr,主用於與平台相關的指標大小或 Native API 互通,並在 32 位與 64 位環境下自動對應為適當的位寬。
    函式指標:新增函式指標語法 delegate*,支援 managed 或 unmanaged 呼叫慣例,可於 unsafe 程式碼中直接以函式位址呼叫,避免 delegate 帶來的額外配置與間接成本。
    隱藏發出 localsinit 旗標:可使用屬性 [System.Runtime.CompilerServices.SkipLocalsInit] 抑制編譯器發出 .locals init 旗標,讓 JIT 不自動將區域變數歸零,藉此減少方法 prolog 初始化成本。
    模組初始設定式:以 [ModuleInitializer] 標註的方法,在組件載入時由編譯器生成的模組初始器呼叫,用於一次性的載入期初始化邏輯。
  6. partial 方法的新功能
    主要配合 Source Generator 或程式產生器,讓 partial 方法支援傳回值、out 參數、允許 public/protected/internal/private 可見範圍修飾(過往只限 private)。
    以下是個簡單範例:
    // 程式只宣告無實作    
    public partial class MyType
    {
        // 加顯式存取修飾詞 → 必須提供實作,並支援 out 與非 void 傳回
        internal partial bool TryParse(string s, out int value);
    }
    // 程式產生器生成 MyType.Generated.cs 提供實作內容
    public partial class MyType
    {
        internal partial bool TryParse(string s, out int value)
        {
            if (int.TryParse(s, out var v))
            {
                value = v;
                return true;
            }
            value = 0;
            return false;
        }
    }
    
  7. 目標型別(Target-Typed) new 運算式(註:這寫法我用好一陣子了,現在才知道是 C# 9 加的)
    在有明確型別上下文時,可以省略右邊的型別,只寫 new,讓程式更精簡:
    List<string> names = new();               // 型別由左側推斷
    Dictionary<int, string> map = new();      // 不必重複 Dictionary<int, string>
    
    Point p = new(3, 5);                      // 搭配具名型別與建構子參數
    MyClass obj = new();                      // 呼叫無參數建構子
    
    static List<int> Make() => new() { 1, 2, 3 }; // 回傳型別為 List<int>    
    
  8. static 匿名函式
    匿名方法或 Lambda 可標註為 static,避免捕捉外部變數 (Closure),讓產生的委派輕量化提升效率,亦可在編譯期抓出誤觸發 Closure。
    int factor = 10;
    
    // 一般 lambda 可以捕捉外部變數
    Func<int, int> normal = x => x * factor;
    
    // static lambda 不允許捕捉外部 (以下若使用 factor 會編譯錯誤)
    Func<int, int> timesTwo = static x => x * 2;
    
    // 多參數與區域函式內使用
    var data = new[] { 1, 2, 3 };
    var doubled = Array.ConvertAll(data, static x => x * 2);
    
    
  9. 目標型別條件運算式
    條件運算子 (?:) 可在不同分支型別不完全一致時,借助目標型別進行推斷。
    // 傳統 ? : 左右型別不一致的解法
    object obj = true ? 123 : "456";  // 提升至 object
    
    // C# 9 起:左右分支可不同,由目標推斷為 IEnumerable<int>
    IEnumerable<int> seq = (DateTime.Now.Minute % 2 == 0) ? new List<int>() : new int[] { };
    
    // 搭配 target-typed new
    List<int> result = condition ? new() : new() { 1, 2, 3 };
    
  10. Covariant 傳回型別 覆寫虛擬方法時,回傳型別可以更具體 (協變),使 API 更表達語意且減少轉型。
    abstract class Animal { }
    class Cat : Animal { }
    
    abstract class AnimalFactory
    {
        public abstract Animal Create();
    }
    
    class CatFactory : AnimalFactory
    {
        // C# 9 起允許:回傳更具體的 Cat
        public override Cat Create() => new Cat();
    }
    
    var factory = new CatFactory();
    Cat cat = factory.Create(); // 不需轉型
    
  11. GetEnumerator 迴圈的擴充 foreach 支援
    C# 9 起 foreach 能發現擴充方法型式的 GetEnumerator,過去只會找實例或靜態可見的 GetEnumerator,常需要修改原型別或另外包一層。
    public struct MyEnumerator<T>
    {
        private readonly IList<T> _list;
        private int _index;
        public MyEnumerator(IList<T> list) { _list = list; _index = -1; }
        public bool MoveNext() => ++_index < _list.Count;
        public T Current => _list[_index];
    }
    
    public static class MyEnumerableExtensions
    {
        // 擴充 GetEnumerator:讓任何 IList<T> 可用自訂列舉器
        public static MyEnumerator<T> GetEnumerator<T>(this IList<T> list) => new(list);
    }
    
    var list = new List<int> { 1, 2, 3 };
    foreach (var x in list) // 會使用擴充的 GetEnumerator
    {
        Console.WriteLine(x);
    }
    
  12. Lambda 捨棄參數
    Lambda 可使用下劃線 _ 作為捨棄參數,提升可讀性並避免未使用參數警告。
    // 多個參數但只需要第二個
    Func<int, int, int> pickSecond = (_, y) => y;
    
    // 事件處理常見模式:sender 不使用
    button.Click += (_, e) => Console.WriteLine(e.ToString());
    
    // Linq:只用到值,不用索引
    var arr = new[] { "a", "b", "c" };
    var withIndex = arr.Select((value, _) => value.ToUpper());
    
  13. 區域函式也可加 Attribute
    區域函式 (宣告在方法內的函式) 支援套用 Attribute:
    using System.Diagnostics.CodeAnalysis;
    using System.Runtime.CompilerServices;
    
    void Demo()
    {
        // 範例:指示不初始化區域變數 (可能提升效能,但需自行確保安全)
        [SkipLocalsInit]
        void HotPath() { /* ... */ }
    
        // 可空性/驗證示意
        void Print([NotNull] string? text, [CallerLineNumber] int line = 0)
        {
            Console.WriteLine($"Line={line}, Text={text}");
        }
    
        HotPath();
        Print("Hello");
    }
    
    Demo();
    

A concise overview of C# 9.0’s key features.


Comments

Be the first to post a comment

Post a comment