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

C# 12 隨 .NET 8 SDK 在 2023 釋出。

  1. Primary Constructors
    在 class/struct 宣告直接寫主建構式帶入參數,用來初始化欄位/屬性、轉傳給 base(...)、或在成員中引用,其他建構子需以 this(...) 轉呼叫主建構式。
    public readonly struct Distance(double dx, double dy)
    {
        public readonly double Magnitude { get; } = Math.Sqrt(dx * dx + dy * dy);
        public readonly double Direction { get; } = Math.Atan2(dy, dx);
    }
    // 背後會轉換成以下程式
    public readonly struct Distance
    {
        public readonly double Magnitude { get; }
    
        public readonly double Direction { get; }
    
        public Distance(double dx, double dy)
        {
            Magnitude = Math.Sqrt(dx * dx + dy * dy);
            Direction = Math.Atan2(dy, dx);
        }
    }
    // 另一個範例
    public class Employee(string firstName, string lastName, DateTime hireDate, decimal salary)
    {
        public string FirstName { get; } = firstName;
        public string LastName  { get; } = lastName;
        public DateTime HireDate { get; } = hireDate;
        public decimal Salary { get; } = salary;
    
        public override string ToString() => $"{FirstName} {LastName} hired on {HireDate:d} with {Salary:C}";
    }
    
  2. 集合表示式
    以中括號 [ <元素>,<元素>,... ] 直接建立可指派到多種集合型別的集合值,並支援展開運算子 .. 合併集合,可用於變數初始化、屬性、參數等位置,但不能用在常數或方法參數的預設值。
    // 指派到多種集合型別
    List<int> list = [1, 2, 3];
    int[] arr = [4, 5, 6];
    Span<string> days = ["Sun","Mon","Tue","Wed","Thu","Fri","Sat"];
    
    // 展開合併
    string[] vowels = ["a","e","i","o","u"];
    string[] consonants = ["b","c","d","f","g","h","j","k","l","m","n","p","q","r","s","t","v","w","x","z"];
    string[] alphabet = [..vowels, ..consonants, "y"];
    
  3. Inline 陣列
    [System.Runtime.CompilerServices.InlineArray(n)] 可在 struct 宣告固定長度的內嵌陣列,優點是可確保記憶體配置連續、降低堆積配置與 GC 壓力,適用高效能需求場景。
    using System.Runtime.CompilerServices;
    
    [InlineArray(8)]
    public struct ByteBuffer
    {
        private byte _element0; // 編譯器生成連續元素儲存
    }
    
    void Use()
    {
        ByteBuffer buf = default;
        for (int i = 0; i < 8; i++) buf[i] = (byte)i; // 可用索引器
    }
    
  4. Lambda 參數可省略、支援預設值
    不解釋,之前老覺得 Lambda 函式不能省略參數,無法給預設值很遜,C# 12 起封印解除。
    // 帶預設值的 lambda
    var greet = (string name = "World", string prefix = "Hello") => $"{prefix}, {name}!";
    
    Console.WriteLine(greet());                // Hello, World!
    Console.WriteLine(greet("C# 12"));         // Hello, C# 12!
    Console.WriteLine(greet(prefix: "Hi"));    // Hi, World!
    
  5. ref readonly 參數
    允許以參照傳遞參數但保證不修改其值,也不允許更換參照指向對象,能兼顧大型 struct 的效能與語意安全;對已使用 ref 但實際未修改的 API,可改成 ref readonly 而不破壞呼叫端相容性。
    public readonly struct BigStruct(int a, int b) { public int A => a; public int B => b; }
    
    static int Sum(ref readonly BigStruct s)
    {
        // s = new BigStruct(1,2); // 編譯錯誤:不可重新指派
        return s.A + s.B;
    }
    
    var bs = new BigStruct(10, 20);
    int r = Sum(ref bs);
    
  6. using 別名可應用於所有型別
    using 別名現在可對任何型別(含 tuple、陣列、指標、封閉泛型),用以縮短冗長型別、釐清語意、或消除命名衝突;限制是不支援 nullable 參考型別。
    using Pair = (int A, int B);
    using Map = System.Collections.Generic.Dictionary<string, object>;
    using Series = System.Collections.Generic.List<(DateTime T, double V)>;
    using S = string?; // 不允許:不支援 nullable 參考型別
    
    Pair p = (1, 2);
    Map m = new() { ["Name"] = "Alice", ["Age"] = 25 };
    Series s = [];
    
  7. Experimental Attribute
    [System.Diagnostics.CodeAnalysis.Experimental] 可標示實驗性 API,使用端會收到診斷(預設為警告,但比照錯誤將導致建置失敗),需明確抑制或設定才能使用,可用於套件/程式庫逐步釋出預覽功能。(註:與 Obsolete 相反:Obsolete 提醒淘汰,Experimental 提醒尚不穩定)
    using System.Diagnostics.CodeAnalysis;
    
    [Experimental("EXP001")]
    public class ExperimentalService
    {
        public void DoWork() { }
    }
    
    // 使用端會出現實驗性 API 診斷,需以 pragma 或編譯選項抑制
    #pragma warning disable EXP001
    new ExperimentalService().DoWork();
    #pragma warning restore EXP001
    
  8. Interceptors (預覽階段)
    Interceptors 仍為預覽功能,用於在編譯器/工具鏈層級重新導向呼叫以插入邏輯(如記錄/分析/改寫),尚未穩定,實務需留意版本相容與特性啟用條件。

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


Comments

# by ChrisTorng

看到 public readonly double Magnitude { get; } = Math.Sqrt(dx * dx + dy * dy); 想到最近踩坑經驗來分享。 此例的值是不變 (建構式給值) 因此適用。 若值有需要變動,可寫為: public double Magnitude => Math.Sqrt(dx * dx + dy * dy); 就是每次呼叫屬性皆執行 Sqrt()。 我踩的坑就是寫了 { get; } = ,沒想到它不會每次執行,而是建構式執行給值一次。

Post a comment