三年前趁著讀 C# In Depth 看完 C# 7,C# 8.0 是 2019 年推出的,所這是篇晚了六年的開箱文(嚴格來說是考古文),但既然靠 C# 吃飯,就像缺了必修學分,該補修就得補修,我打算一路補到 C# 13。時代不同了,現在多了 AI 伴讀,一些深澀少用的功能 LLM 也能解釋得明明白白,學習程式語言邁入了新紀元。(雖然很多人心中所謂新紀元是以後再也不用學程式語言,噗)

參考來源:微軟官方文件 C# 8.0 版

C# 8.0 跟過往最大的不同是它是第一個專門針對 .NET Core 的 C# 版本 (當時是 .NET Core 3.0),增加或強化重點如下:

  1. 唯讀成員
    在 C# 8 之前可以宣告 public readonly struct StructName 將整個結構宣告為唯讀,C# 8.0 可只將特定成員方法/屬性/索引子/覆寫 System.Object 方法(例如:ToString())宣告為唯讀。 在編譯階段阻止對結構內容的不當修改,例如以下的例子,Display() 可以 X++,在 ReadonlyDisplay() X++ 會產生編譯錯誤:

    但要注意,在唯讀成員呼叫非唯讀方法時會產生防護性副本,以避免修改到原始結構執行個體,這可能衍生效率問題。
    public struct Accumulator
    {
        private int x;
        public Accumulator(int x) => this.x = x;
    
        // 非 readonly:可能修改狀態
        public void Increment()
        {
            x++; // 修改欄位
        }
    
        // readonly:承諾不修改原實例
        public readonly int Total()
        {
            // 這裡呼叫非 readonly 方法,編譯器會對 this 產生防護性副本,
            // 並在「副本」上呼叫 Increment(),原本的 this 不會被修改
            // 編譯器 warning CS8656: 從 'readonly' 成員呼叫非 readonly 成員
            // 'Accumulator.Increment()' 會 產生 'this' 的隱含複本。
            Increment();
            return x; // 注意:這裡取的是原始 this 的 x 值
        }
    }
    
  2. 預設介面方法
    介面也可以像抽象類別一樣加入實作邏輯,例如:
     // 定義介面,並給予部分方法預設實作
     public interface IMyInterface
     {
         void SayHello(); // 純宣告
         // 預設實作的方法
         void SayWelcome()
         {
             Console.WriteLine("Welcome from interface!");
         }
     }
    
     // 實作介面的類別
     public class MyClass : IMyInterface
     {
         // 必須實作沒有預設實作的方法
         public void SayHello()
         {
             Console.WriteLine("Hello from class!");
         }
     }
    
     class Program
     {
         static void Main()
         {
             MyClass obj = new MyClass();
             obj.SayHello(); // 只能直接呼叫類別實作的方法
    
             // 轉為介面型別,呼叫預設介面方法
             IMyInterface iobj = obj;
             iobj.SayWelcome();
         }
     }   
    
  3. 模式比對增強功能:
    • switch 運算式
    • 屬性模式
    • Tuple 模式
    • 位置模式 老朋友 switch ... case ... 多了許多新奇玩法:
    using System;
    
    public class Program
    {
        public static void Main()
        {
            // 1. switch 運算式
            var shape = new Circle { Radius = 5 };
            Console.WriteLine(GetShapeDescription(shape)); // 輸出: Circle with radius 5
    
            // 2. 屬性模式
            var person = new Person { Name = "Alice", Age = 20 };
            Console.WriteLine(CategorizePerson(person)); // 輸出: Adult
    
            // 3. Tuple 模式
            var weather = ("Rainy", 12);
            Console.WriteLine(WeatherAdvice(weather)); // 輸出: Bring an umbrella and coat
    
            // 4. 位置模式
            Console.WriteLine(DescribePoint(new Point(0, 0))); // 輸出: Origin
            Console.WriteLine(DescribePoint(new Point(3, 0))); // 輸出: On X axis at 3
            Console.WriteLine(DescribePoint(new Point(0, 4))); // 輸出: On Y axis at 4
            Console.WriteLine(DescribePoint(new Point(2, 3))); // 輸出: At (2, 3)
        }
    
        // 1. switch 運算式
        static string GetShapeDescription(Shape shape) =>
            // switch 可依據 shape 的類型執行不同邏輯
            shape switch
            {
                Circle c         => $"Circle with radius {c.Radius}",
                Rectangle r      => $"Rectangle {r.Width} x {r.Height}",
                _                => "Unknown shape"
            };
    
        // 2. 屬性模式
        static string CategorizePerson(Person p) =>
            p switch
            {
                // 依據 Person 的屬性執行不同邏輯
                { Name: null }         => "Anonymous",
                { Age: < 13 }          => "Child",
                { Age: >= 13 and < 18 }=> "Teenager",
                { Age: >= 18 }         => "Adult",
                _                      => "Unknown"
            };
    
        // 3. Tuple 模式
        static string WeatherAdvice((string condition, int temperature) weather) =>
            // 以 Tuple 傳入多個值,分別利用判斷
            weather switch
            {
                ("Sunny", _)              => "Wear sunglasses",
                ("Rainy", int t) when t < 15 => "Bring an umbrella and coat",
                ("Rainy", _)              => "Bring an umbrella",
                _                         => "Have a nice day"
            };
    
        // 4. 位置模式
        static string DescribePoint(Point p) =>
            p switch
            {
                (0, 0)      => "Origin",
                (0, var y)  => $"On Y axis at {y}",
                (var x, 0)  => $"On X axis at {x}",
                _           => $"At ({p.X}, {p.Y})"
            };
    }
    
    // 幾何圖形 base class & derived
    abstract class Shape {}
    class Circle : Shape { public int Radius { get; set; } }
    class Rectangle : Shape { public int Width { get; set; } public int Height { get; set; } }
    
    // Person 類,呈現屬性模式
    class Person { public string? Name { get; set; } public int Age { get; set; } }
    
    // Point 結構 (適合展示位置模式)
    public readonly struct Point
    {
        public int X { get; }
        public int Y { get; }
    
        public Point(int x, int y) => (X, Y) = (x, y);
        // 位置模式需要實作 Deconstruct 方法
        public void Deconstruct(out int x, out int y) => (x, y) = (X, Y);
    }
    // 若為 C# 9.0 可使用 record struct,自動實作 Deconstruct 方法
    // public record struct Point(int X, int Y);
    
  4. Using 宣告免加大括號
    可省去打字及縮排,讓程式更簡潔,下例中的 reader 會在區域變數範圍終點 Dispose(),也就是 LoadNumbers() 方法的結尾處。
    static IEnumerable<int> LoadNumbers(string filePath)
    {
        using StreamReader reader = File.OpenText(filePath);
    
        var numbers = new List<int>();
        string line;
        while ((line = reader.ReadLine()) is not null)
        {
            if (int.TryParse(line, out int number))
            {
                numbers.Add(number);
            }
        }
        return numbers;
    }    
    
  5. 靜態區域函式
    跟一般區域函式差在無法捕獲外部變數,不會產生閉包(Closure),有助於提升效能。
    public static void Main()
    {
        int factor = 3;
        int result = Add(5, 10);
        Console.WriteLine($"加總結果: {result}");
    
        // 靜態區域函式Bin
        static int Add(int a, int b)
        {
            // 無法存取外部區域變數 (如 factor)
            // 所有用到的資料都靠參數傳遞
            return a + b; 
        }
    }
    
  6. 可處置的(Disposable) ref struct
    在 C# 8.0 起,只要 ref struct 內部定義一個 public void Dispose() 方法,不需要實作 IDisposable 介面,也可以直接用在 using 語句或 using 宣告中,達到 Deterministic Cleanup (確定性清除資源)的效果。
  7. Nullable Reference Types
    原本 Reference Type 如 string、object 的預設值為 null,但在程式中要時時檢查其值是否為 null 實在太煩了,NullReferenceException 又是超常見的系統爆炸原因,故 C# 8.0 加入了 Nullable Reference Types 的概念,將 string 等型別預設成不允許 null,若要允許需寫成 string? s = null;。這個改變剛從 .NET 4.x 升到 .NET Core/.NET 6+ 時保證很有感,想不發現也難 😄。【延伸閱讀】C# 8 的 Nullable Reference Types by Huanlin 學習筆記
  8. 非同步資料流(Asynchronous Streams)
    把 async/await 的概念延伸到 IEnumerable,新增 IAsyncEnumerable 同步產生與消費資料,不需要等待所有資料都準備好才處理。這項特性可讓資料擷取 (如 I/O、API 分頁、多筆慢速來源) 更有效率並提升應用程式的即時性。
    public class Program
    {
        public static async Task Main(string[] args)
        {
            await foreach (var number in GenerateSequence())
            {
                Console.WriteLine(number);
            }
        }
    
        static async IAsyncEnumerable<int> GenerateSequence()
        {
            for (int i = 0; i < 10; i++)
            {
                await Task.Delay(1000); // 模擬非同步工作
                yield return i;
            }
        }
    }
    
  9. 範圍運算子 ..
    許多語言都支援在陣列索引值使用 .. 表示範圍,C# 8.0 也跟上了
    int[] numbers = [0, 10, 20, 30, 40, 50];
    int amountToDrop = numbers.Length / 2;
    
    // 從 amountToDrop 開始到結尾的所有元素
    int[] rightHalf = numbers[amountToDrop..];
    Display(rightHalf);  // output: 30 40 50
    
    // 到 amountToDrop 之前的所有元素
    int[] leftHalf = numbers[..^amountToDrop];
    Display(leftHalf);  // output: 0 10 20
    
    // 去掉頭一個及最後兩個元素
    int[] middle = numbers[1..^2];
    Display(middle);  // output: 10 20 30
    
    // 從開頭到結尾的所有元素
    int[] all = numbers[..];
    Display(all);  // output: 0 10 20 30 40 50
    
    void Display<T>(IEnumerable<T> xs) => Console.WriteLine(string.Join(" ", xs));
    
  10. Null 聯合指派 ??=
    ?? 指定變數為 null 的替代值是大家小學二年級就學過的 C# 技巧,??= 則可用於「當變數為 null 時就設成特定值」,簡單說就是把以下程式版段縮成一行。
    variable ??= expression;
    // 相當於
    if (variable is null)
    {
        variable = expression;
    }
    
  11. 非受控建構的類型
    C# 8 導入 Nullable Reference Types,因此泛型 where 約束條件也加入了指定參數是否允許為 null 的新語法:
    • where T : class? 表示 T 必須是 Nullable Reference Type
    • where T : class 表示 T 必須是 Non-Nullable Reference type
    • where T : struct 表示 T 必須是非 Nullable 的值型別
    • where T : notnull 表示 T 必須為不可為 null 的型別 (Reference 或 Value Type 都可)
  12. 巢狀運算式中的 stackalloc
    stackalloc 可在堆疊(Stack)上配置一塊記憶體區塊(如陣列、Buffer),所配置記憶體會隨方法回傳時自動釋放,不會造成 GC 壓力。使用 Span 或 ReadOnlySpan 可以安全地操作 stackalloc 所配置的記憶體,且不需 unsafe 區塊。
    在 C# 8 以前,stackalloc 只能出現在變數宣告時直接初始化及簡單的賦值運算,C# 8 起, stackalloc 可以出現在「巢狀化」的表達式中,例如:方法的傳入/回傳值、條件運算式或三元運算子、 其他需要 Span 或 ReadOnlySpan 的運算式等。
    // 直接巢狀 stackalloc 當方法參數
    Span<int> numbers = stackalloc[] { 1, 2, 3, 4, 5, 6 };
    var index = numbers.IndexOfAny(stackalloc[] { 4, 8, 12 });
    Console.WriteLine(index); // 輸出: 3
    
    // 三元運算子(條件運算式)結合
    int length = 1000;
    Span<byte> buffer = length <= 1024 
        ? stackalloc byte[length] 
        : new byte[length];
    
    // 字串修整(Trim)運用    
    string input = "This is a simple string \r\n";
    ReadOnlySpan<char> trimmedChar = input.AsSpan().Trim(stackalloc[] { ' ', '\r', '\n' });
    Console.WriteLine(trimmedChar.ToString());    
    
  13. 插補逐字字串的改善 講的是 $"..." Interpolated Strings 字串插值,當要結合 Verbatim String Literal 逐字字串常值 @"...",以前只能寫 $@"content",C# 8 起寫 @$"content" 也成。

A concise overview of C# 8.0’s key features, including pattern matching, nullable reference types, async streams, and more, with practical code examples.


Comments

# by yoyo

想看黑暗大介紹 Span<T> , Memory<T> and Pipelines

# by Jeffrey

to yoyo, 沒 PO FB 文章還是有人看耶,感動~~~ 衝著這點,已將 Span<T> 排入 TODO

# by Ike

我有訂閱 RSS,按時收看

# by Jeffrey

to Ike,都 2025 年了,此舉可獲頒鐵粉勳章。(RESPECT)

Post a comment