歷史回顧 - C# 9 新功能盤點
| | | 0 | |
參考資料:微軟官方文件 C# 9.0
C# 9 發行於 2020 年 11 月,跟 .NET 5 一起推出。以下是主要新功能及強化:
- Record
記錄(Record)是 C# 9 新推出用來封裝不可變資料(Immutable Data)的型別,可以簡潔地寫成 ‵public record Student (int Id, string Name);`一次交代完包含的屬性。
record 在編譯後會轉以 class 形式存在,為 Reference Type,C# 10 起支援 record struct,底層可以 struct 實現。由於 record 不可修改,如要修改需產生一個新副本再賦與新值。例如:
另外,record 內建 Deconstruct 方法,可將屬性分解對映成個別變數,例如:public record Student (int Id, string Name); var s1= new Student("A1", "Jeffrey"); var s2 = s1 with { }; var s3 = s2 with { Name = "darkthread" };var (id, name) = student;。 如想深入了解推薦 Huanlin 老師這篇:C# 9:Record 詳解 - 僅供初始化的 setter
可限定屬性只能在物件初始化的過程中給值,也就是只能在建構式、屬性初始設定式、物件初始設定式三處設定。public int ReadOnlyInt { get; }一樣也有唯讀效果,但限定只能從建構式指定,無法寫成new Foo() { ReadOnlyInt = 123 }。 - 最上層陳述式
現在 Program.cs 可以直接第一行就 Console.WriteLine(),不用寫 class Program、不需要 static void Main(string[] args),便是源自最上層陳述式帶來的創新。 - 模式比對增強功能:關聯式模式和邏輯模式
C# 8 為 switch case 帶來許多花式玩法,C# 9 再支援兩種新寫法。 關聯式模式 (Relational Patterns),支援運算子 <、<=、>、>= 直接在模式中比較常數,依數值/可比較型別的區間決定:
邏輯模式 (Logical Patterns),可使用 is, and , or, not 組裝比對條件,比 when/if 好讀,像是 is not null 就很貼近自然語意。string WaterState(int tempF) => tempF switch { < 32 => "solid", 32 => "solid/liquid transition", (> 32) and (< 212) => "liquid", 212 => "liquid / gas transition", > 212 => "gas", };
另外,is 過去只能判斷型別,例如 s is string,C# 9 擴充了 is 的比對對象,可用來寫判斷函式,可讀性大增,例如: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" };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 ""; - 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] 標註的方法,在組件載入時由編譯器生成的模組初始器呼叫,用於一次性的載入期初始化邏輯。 - 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; } } - 目標型別(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> - 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); - 目標型別條件運算式
條件運算子 (?:) 可在不同分支型別不完全一致時,借助目標型別進行推斷。// 傳統 ? : 左右型別不一致的解法 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 }; - 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(); // 不需轉型 - 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); } - 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()); - 區域函式也可加 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