Menu

Switch expression trong C#: cú pháp, pattern, discard và when

Switch expression của C# 8 biến một giá trị thành kết quả bằng các nhánh pattern thay vì case và break. Cú pháp, discard _, pattern thuộc tính, tuple và so sánh, and/or/not, điều kiện when, cảnh báo tính đầy đủ và SwitchExpressionException, kèm các cách viết tương đương chạy được trên C# 7.

Trang này có trình soạn thảo chạy trực tiếp. Sửa, chạy và xem đầu ra ngay lập tức.

Switch expression ánh xạ một giá trị đầu vào thành một kết quả. Nó có từ C# 8 và thay thế mẫu quen thuộc của câu lệnh switch mà mọi case chỉ để trả về hoặc gán một giá trị:

// C# 8 and later
string StatusText(int code) => code switch
{
    200 => "OK",
    301 or 302 => "Redirect",   // 'or' needs C# 9
    404 => "Not Found",
    >= 500 => "Server Error",   // relational patterns need C# 9
    _ => "Unknown"
};

Đọc từ trên xuống: giá trị đứng trước switch được kiểm tra lần lượt với từng nhánh, và nhánh đầu tiên có pattern khớp sẽ cung cấp kết quả. Không có từ khóa case, break hay default; _ (pattern discard) khớp với mọi thứ và đóng vai trò dự phòng.

Trong C# 7, cùng phép ánh xạ đó là một câu lệnh switch trong một phương thức nhỏ, với return ở mỗi case:

Đầu ra:

200 OK
302 Redirect
404 Not Found
503 Server Error
418 Unknown

Dạng biểu thức ngắn hơn chủ yếu vì mỗi nhánh chỉ một dòng và không còn thân phương thức. Nó cũng là một biểu thức, nên có thể đặt ở bất cứ đâu một giá trị được phép: đối số, chỗ trống trong nội suy chuỗi, return, giá trị khởi tạo trường.

Quy tắc cú pháp

var result = input switch
{
    pattern1 => value1,
    pattern2 when condition => value2,
    _ => fallback
};
  • Giá trị đầu vào đứng trước, sau đó là switch. Thứ tự này ngược với dạng câu lệnh.
  • Mỗi nhánh có dạng pattern => expression. Vế phải phải là một biểu thức duy nhất; cần nhiều logic hơn thì gọi một phương thức.
  • Các nhánh cách nhau bằng dấu phẩy. Được phép có dấu phẩy thừa sau nhánh cuối.
  • Toàn bộ là một biểu thức, nên câu lệnh dùng nó kết thúc bằng ; sau dấu ngoặc nhọn đóng.
  • Mọi nhánh phải tạo ra giá trị có chung một kiểu. var x = n switch { 0 => "zero", _ => 0 }; không biên dịch được (CS8506, không có kiểu phù hợp nhất). Từ C# 9, một kiểu đích được khai báo sẽ giải quyết: object x = n switch { 0 => "zero", _ => 0 }; biên dịch được.
  • Mỗi nhánh phải tạo ra một giá trị, nên nhánh không thể gọi phương thức void như Console.WriteLine, và switch expression không thể đứng riêng như một câu lệnh (CS0201). Để thực hiện hành động, hãy dùng câu lệnh switch.
  • Một nhánh có thể ném ngoại lệ thay vì tạo giá trị: _ => throw new ArgumentOutOfRangeException(nameof(input)).
  • Trình biên dịch từ chối nhánh không bao giờ được tới vì một nhánh trước đã bao phủ nó, với lỗi CS8510, The pattern is unreachable. It has already been handled by a previous arm of the switch expression or it is impossible to match. Hãy đặt nhánh cụ thể trước nhánh tổng quát.

Pattern so sánh và pattern logic (C# 9)

C# 9 bổ sung <, <=, >, >= làm pattern, cùng các toán tử kết hợp and, or và not. Nhờ vậy bảng khoảng giá trị đọc lên như một bản đặc tả:

// C# 9 and later
string Grade(int score) => score switch
{
    < 0 or > 100 => throw new ArgumentOutOfRangeException(nameof(score)),
    >= 90 => "A",
    >= 80 => "B",
    >= 70 => "C",
    >= 60 => "D",
    _ => "F"
};

string AgeGroup(int age) => age switch
{
    < 13 => "child",
    >= 13 and <= 19 => "teen",
    _ => "adult"
};

bool IsLetter(char c) => c is >= 'a' and <= 'z' or >= 'A' and <= 'Z';

Vì các nhánh được thử theo thứ tự, >= 80 chỉ gặp những điểm dưới 90. Pattern so sánh chỉ nhận hằng số ở vế phải; để so sánh với một biến, hãy dùng điều kiện when. Cách viết tương đương bảng khoảng giá trị trên C# 7 là một chuỗi if:

Đầu ra:

95 -> A
83 -> B
71 -> C
64 -> D
12 -> F

not thường được dùng nhất dưới dạng not null: x is not null, hoặc một nhánh not null => x.Name.

Pattern thuộc tính

Pattern thuộc tính (property pattern) khớp theo các thành viên của đối tượng: { Property: pattern, ... }. Kết hợp với switch expression, nó diễn đạt quy tắc nghiệp vụ mà không cần một chuỗi điều kiện &&:

// C# 8 and later
decimal ShippingCost(Order order) => order switch
{
    { Total: >= 100m } => 0m,                       // relational inside: C# 9
    { Country: "US", Express: true } => 15m,
    { Country: "US" } => 5m,
    { Express: true } => 30m,
    null => throw new ArgumentNullException(nameof(order)),
    _ => 12m
};

Mọi thuộc tính được liệt kê phải khớp thì nhánh mới khớp. Pattern thuộc tính rỗng { } khớp với mọi giá trị khác null. C# 10 bổ sung pattern thuộc tính mở rộng cho thành viên lồng nhau: { Customer.Address.Country: "US" } thay cho { Customer: { Address: { Country: "US" } } }.

Pattern tuple

Switch trên một tuple khớp nhiều giá trị cùng lúc, đây là cách gọn gàng để diễn đạt một bảng quyết định:

// C# 9 and later (the 'or' pattern)
string Winner(string a, string b) => (a, b) switch
{
    ("rock", "scissors") or ("scissors", "paper") or ("paper", "rock") => "player 1",
    var (x, y) when x == y => "draw",
    _ => "player 2"
};

var (x, y) tách tuple thành hai biến để điều kiện when so sánh. Trên C# 7, cách thay thế thường gặp là gộp các giá trị thành một khóa, hoặc dùng một dictionary chứa kết quả:

Đầu ra:

player 1
player 2
draw

Pattern kiểu và when

Một nhánh có thể kiểm tra kiểu lúc chạy và gán vào một biến, còn mệnh đề when thêm bất kỳ điều kiện nào mà cú pháp pattern không diễn đạt được:

// C# 8 and later
double Area(Shape shape) => shape switch
{
    Circle c => Math.PI * c.Radius * c.Radius,
    Rectangle r when r.Width == r.Height => r.Width * r.Width,
    Rectangle r => r.Width * r.Height,
    null => throw new ArgumentNullException(nameof(shape)),
    _ => throw new NotSupportedException(shape.GetType().Name)
};

Phiên bản C# 7 dùng type pattern is trong một chuỗi if, cũng gán biến theo cách tương tự:

Đầu ra:

Circle: 12.57
Rectangle: 13.50

Tính đầy đủ và SwitchExpressionException

Switch expression phải tạo ra giá trị cho mọi đầu vào. Khi trình biên dịch thấy có đầu vào không nhánh nào khớp, nó cảnh báo:

warning CS8509: The switch expression does not handle all possible values of its input type (it is not exhaustive). For example, the pattern '0' is not covered.

Đây là cảnh báo, không phải lỗi, nên code vẫn build được. Nếu một giá trị không khớp xuất hiện lúc chạy, biểu thức ném System.Runtime.CompilerServices.SwitchExpressionException, với thông báo kết thúc bằng giá trị không khớp (Unmatched value was 3.).

Enum là trường hợp đặc biệt. Ngay cả khi mọi thành viên có tên đều có nhánh, một biến enum vẫn có thể chứa bất kỳ số nguyên nào ((Size)3), nên trình biên dịch báo CS8524 cho "một giá trị enum không có tên":

// C# 8 and later
enum Size { Small, Medium, Large }

string Code(Size s) => s switch
{
    Size.Small => "S",
    Size.Medium => "M",
    Size.Large => "L",
    // warning CS8524 without the next arm
    _ => throw new ArgumentOutOfRangeException(nameof(s))
};

Hãy thêm một nhánh _ tường minh ném ra một ngoại lệ có ích. Cách này ghi rõ giả định, tắt cảnh báo, và cho lỗi rõ ràng hơn SwitchExpressionException nếu có lúc một giá trị sai xuất hiện.

Dictionary, một cách thay thế

Khi các nhánh chỉ là những cặp hằng số với giá trị và bảng có thể lớn dần hoặc đến từ cấu hình, một Dictionary làm được việc tương tự ở mọi phiên bản C# và có thể dựng lúc chạy:

Đầu ra:

19.00
11.50
0

Một quốc gia không có trong bảng nhận mức 0m. Tích của hai số decimal có số chữ số thập phân bằng tổng của hai toán hạng, vì vậy 100m * 0.19m in ra 19.00 còn mức bằng 0 in ra 0.

Switch expression và câu lệnh switch

câu lệnh switchswitch expression
Có từC# 1C# 8
Tạo ra giá trịkhôngcó
Cú pháp mỗi nhánhcase X: ... break;X => value,
Dự phòngdefault:_ =>
Nhiều câu lệnh mỗi nhánhcókhông, gọi một phương thức
Cảnh báo tính đầy đủkhôngcó (CS8509)
Không khớp lúc chạykhông có gì chạySwitchExpressionException

Cả hai dạng nhận cùng các pattern (kiểu, hằng số, thuộc tính, tuple, so sánh), nên học một dạng là biết dạng kia. Việc chọn dạng nào phụ thuộc vào hình dạng của bài toán: một giá trị thì đi qua biểu thức, một chuỗi hành động thì đi qua câu lệnh.

Câu hỏi thường gặp

Switch expression trong C# là gì?

Switch expression, có từ C# 8, là một biểu thức cho ra một giá trị: string text = code switch { 200 => "OK", 404 => "Not Found", _ => "Unknown" };. Giá trị đầu vào đứng trước từ khóa switch, mỗi nhánh có dạng pattern => result, các nhánh cách nhau bằng dấu phẩy, và không có từ khóa case, break hay default.

Trường hợp mặc định trong switch expression của C# là gì?

Pattern discard _ khớp với mọi thứ, nên _ => "Unknown" ở nhánh cuối đóng vai trò của default. Nếu không có nhánh bắt tất cả, trình biên dịch cảnh báo (CS8509) khi có đầu vào chưa được xử lý, và một giá trị không khớp lúc chạy sẽ ném SwitchExpressionException.

Làm sao khớp nhiều giá trị trong một nhánh của switch expression?

Dùng pattern or từ C# 9: "sat" or "sun" => "weekend". Với khoảng giá trị, kết hợp pattern so sánh bằng and: >= 13 and <= 19 => "teen". Trên C# 8 bạn phải liệt kê các giá trị thành những nhánh riêng có cùng kết quả.

Có thể dùng when trong switch expression của C# không?

Có. Một nhánh có thể thêm điều kiện sau pattern: Order o when o.Total > 100 => 0m. Nhánh chỉ khớp khi pattern khớp và điều kiện when đúng; nếu không, việc đánh giá tiếp tục với nhánh kế tiếp.

Nên dùng switch expression hay câu lệnh switch?

Dùng biểu thức khi mọi nhánh chỉ tạo ra một giá trị, ví dụ ánh xạ trạng thái sang nhãn hay tính giá. Dùng câu lệnh khi các nhánh chạy nhiều câu lệnh, có tác dụng phụ hoặc cần thoát khỏi vòng lặp. Biểu thức còn cho bạn cảnh báo về tính đầy đủ mà câu lệnh không có.

Hình minh họa các ngôn ngữ lập trình của Coddy

Học lập trình cùng Coddy

Bắt đầu