Menu

Pattern matching trong C#: is, switch, property pattern, list pattern

Cách pattern matching hoạt động trong C#: toán tử is với mẫu kiểu và mẫu hằng, câu lệnh switch với mẫu trong case và when, biểu thức switch, mẫu thuộc tính, mẫu tuple và mẫu vị trí, mẫu quan hệ và mẫu logic (and, or, not), mẫu danh sách, và phiên bản C# nào bổ sung từng loại.

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.

Pattern matching kiểm tra một giá trị có khớp với một hình dạng hay không: "nó có phải Circle không?", "nó có null không?", "Status của nó có phải "paid" và Total có trên 100 không?". Khi phép kiểm tra thành công, mẫu (pattern) còn có thể tách các phần của giá trị ra thành biến mới. Tính năng này bắt đầu khá nhỏ ở C# 7 với toán tử is và được mở rộng qua từng phiên bản kể từ đó, nên trang này ghi chú phiên bản cho từng dạng.

Mẫu kiểu với is (C# 7)

Trước C# 7, kiểm tra kiểu rồi dùng nó cần hai bước: as cộng một phép kiểm tra null, hoặc is cộng một phép ép kiểu. Mẫu kiểu (type pattern) làm cả hai cùng lúc và cho bạn một biến đúng kiểu:

Đầu ra:

card ending 4242: 49.90
transfer from PT50...: 1200
no payment
other payment: 5

p is CardPayment card chỉ đúng khi p là một CardPayment khác null (hoặc một lớp kế thừa từ nó), và khi đó card giữ giá trị đó với đúng kiểu tĩnh. Giá trị null không bao giờ khớp với mẫu kiểu, vì thế khoản thanh toán null rơi xuống tới phép kiểm tra null.

Phạm vi và gán chắc chắn

Biến của mẫu tồn tại trong khối lệnh bao quanh, nhưng trình biên dịch chỉ cho bạn đọc nó ở những chỗ chắc chắn phép khớp đã thành công. Nhờ vậy dạng "mệnh đề bảo vệ" (guard clause) hoạt động được: bạn thoát sớm và dùng biến ở phía sau:

Đầu ra:

19.99
42 is over 40
price must be text

boxed is int n && n > 40 cho thấy một dạng phổ biến khác: biến dùng được ở bên phải &&, vì vế đó chỉ chạy khi phép khớp đã thành công. Ở bên phải || thì biến chưa được gán, và trình biên dịch sẽ báo như vậy (CS0165).

Mẫu cũng mở hộp (unbox) giá trị: boxed is int n thành công với một int đã đóng hộp và cho bạn giá trị mà không cần một phép ép kiểu có thể ném ngoại lệ. Với kiểu giá trị nullable, maybe is int n thành công đúng khi maybe có giá trị.

Mẫu hằng và mẫu null

Một hằng số cũng là một mẫu. x is null là mẫu được dùng nhiều nhất, và C# 7 còn cho phép x is 0, status is "paid" hay một thành viên enum:

if (customer is null) return;          // null check that ignores any == overload
if (count is 0) Console.WriteLine("empty");
if (level is LogLevel.Error) Alert();

Nhiều nhóm ưu tiên is null hơn == null vì nó không thể bị chuyển hướng bởi một toán tử nạp chồng trên kiểu. C# 9 bổ sung dạng phủ định, is not null.

Mẫu trong câu lệnh switch (C# 7)

Các mẫu kiểu đó cũng có thể làm nhãn cho case, và mệnh đề when thêm một điều kiện. Các case được thử từ trên xuống dưới, nên hãy sắp xếp từ cụ thể đến tổng quát:

static string Describe(object o)
{
    switch (o)
    {
        case null:                           return "nothing";
        case int n when n < 0:               return "negative number";
        case int n:                          return $"number {n}";
        case string s when s.Length == 0:    return "empty text";
        case string s:                       return $"text of {s.Length} chars";
        case IEnumerable<int> list:          return "a sequence of numbers";
        default:                             return o.GetType().Name;
    }
}

Describe(-4);                 // negative number
Describe("");                 // empty text
Describe(new List<int>());    // a sequence of numbers
Describe(2.5);                // Double

Trình biên dịch kiểm tra thứ tự: một case không bao giờ khớp được vì một case phía trước đã bao trùm nó là lỗi CS8120. default luôn được xét cuối cùng, dù nó nằm ở đâu. Cùng logic đó viết bằng chuỗi if/is, như ví dụ thanh toán ở trên, chạy được ở mọi phiên bản C# từ 7 trở đi.

Biểu thức switch (C# 8)

Biểu thức switch là dạng gọn cho việc "tính một giá trị từ nhiều trường hợp". Giá trị đứng trước, rồi đến switch, rồi các nhánh mẫu => kết quả cách nhau bởi dấu phẩy, với _ để bắt mọi trường hợp còn lại:

public abstract record Shape;
public record Circle(double Radius) : Shape;
public record Rectangle(double Width, double Height) : Shape;

static double Area(Shape shape) => shape switch
{
    Circle c => Math.PI * c.Radius * c.Radius,
    Rectangle { Width: var w, Height: var h } => w * h,
    _ => throw new ArgumentException("Unknown shape", nameof(shape)),
};

Area(new Rectangle(3, 4));   // 12

Nếu lúc chạy không có nhánh nào khớp, biểu thức sẽ ném SwitchExpressionException, và trình biên dịch cảnh báo (CS8509) khi nó thấy có đầu vào chưa được xử lý. Trang về biểu thức switch trình bày riêng phần cú pháp; phần còn lại của trang này nói về các mẫu bạn có thể đặt trong các nhánh.

Mẫu thuộc tính (C# 8, mở rộng ở C# 10)

Mẫu thuộc tính (property pattern) so khớp các thuộc tính của một đối tượng với những mẫu lồng nhau, đặt trong cặp ngoặc nhọn. Đối tượng cũng phải khác null, nên chỉ riêng { } có nghĩa là "khác null":

public record Address(string City, string Country);
public record Order(decimal Total, string Status, Address ShipTo, int Items);

static string Shipping(Order order) => order switch
{
    { Status: "cancelled" }                   => "no shipment",
    { ShipTo.Country: "PT", Total: >= 50m }   => "free, domestic",     // C# 10 dotted form
    { ShipTo.Country: "PT" }                  => "4.90, domestic",
    { Total: > 200m }                         => "free, international",
    _                                         => "12.00, international",
};

Shipping(new Order(60m, "paid", new Address("Porto", "PT"), 2));    // free, domestic
Shipping(new Order(260m, "paid", new Address("Lyon", "FR"), 2));    // free, international

Ở C# 8 và 9 bắt buộc phải dùng dạng lồng nhau: { ShipTo: { Country: "PT" } }. C# 10 cho phép cách viết tắt có dấu chấm như ở trên. Mẫu thuộc tính cũng dùng được với is, giúp các phép kiểm tra nhiều điều kiện đọc như một đoạn mô tả: if (order is { Status: "paid", Items: > 0 }).

Mẫu quan hệ và mẫu logic (C# 9)

C# 9 bổ sung các phép so sánh (<, <=, >, >=) và các phép kết hợp and, or và not, biến các khoảng giá trị thành mẫu:

static string Grade(int score) => score switch
{
    < 0 or > 100      => "invalid",
    >= 90             => "A",
    >= 75 and < 90    => "B",
    >= 50             => "C",
    _                 => "F",
};

Grade(82);    // B
Grade(101);   // invalid

if (input is not null and not "") { ... }
if (c is >= 'a' and <= 'z' or >= 'A' and <= 'Z') { ... }   // and binds tighter than or

Cách tương đương trong C# 7 là một chuỗi if/else if hoặc một switch với các mệnh đề when (case int s when s >= 90:).

Mẫu tuple và mẫu vị trí (C# 8)

Để switch trên nhiều giá trị cùng lúc, bạn switch trên một tuple:

static string Quadrant(int x, int y) => (x, y) switch
{
    (0, 0)       => "origin",
    (> 0, > 0)   => "I",
    (< 0, > 0)   => "II",
    (< 0, < 0)   => "III",
    (> 0, < 0)   => "IV",
    _            => "on an axis",
};

Quadrant(-2, 5);   // II

Mẫu vị trí (positional pattern) làm điều tương tự với mọi kiểu có phương thức Deconstruct, bao gồm cả record: case Point(0, 0): hoặc p is Rectangle(var w, var h) && w == h.

Mẫu danh sách (C# 11)

Mẫu danh sách (list pattern) so khớp mảng và danh sách theo các phần tử. .. khớp với số phần tử bất kỳ (một "slice"), và mỗi vị trí phần tử có thể chứa bất kỳ mẫu nào khác:

static string Route(string[] args) => args switch
{
    []                         => "help",
    ["add", var item]          => $"add {item}",
    ["remove", var item, ..]   => $"remove {item}",
    [var cmd, ..]              => $"unknown command {cmd}",
};

Route(new[] { "add", "milk" });             // add milk
Route(new[] { "remove", "milk", "now" });   // remove milk

int[] nums = { 1, 2, 3, 4 };
if (nums is [1, .., var last]) Console.WriteLine(last);   // 4

Phiên bản nào bổ sung mẫu nào

MẫuVí dụPhiên bản
Mẫu kiểu kèm biếno is Circle cC# 7.0
Mẫu hằngx is null, n is 0C# 7.0
Mẫu varx is var vC# 7.0
Mẫu trong nhãn case, điều kiện whencase int n when n < 0:C# 7.0
Biểu thức switch, discard _x switch { ... }C# 8
Mẫu thuộc tính{ Status: "paid" }C# 8
Mẫu tuple(x, y) switch { (0, 0) => ... }C# 8
Mẫu vị tríPoint(0, 0)C# 8
Mẫu quan hệ> 100, <= 0C# 9
Mẫu logicnot null, >= 1 and <= 5, 'a' or 'b'C# 9
Mẫu kiểu không biếncase Circle:, Circle => ...C# 9
Mẫu thuộc tính mở rộng{ ShipTo.City: "Porto" }C# 10
Mẫu danh sách và slice[first, .., last]C# 11

Lỗi thường gặp

  • Sắp xếp case từ tổng quát đến cụ thể. case Payment p: đứng trước case CardPayment c: khiến case thứ hai không bao giờ chạm tới được (CS8120). Đặt case cụ thể lên trước.
  • Tưởng mẫu kiểu khớp với null. null is string s là false. Hãy xử lý null bằng một case riêng hoặc is null.
  • Đọc biến của mẫu ở chỗ phép khớp có thể đã thất bại. Sau a is int n || ..., n chưa được gán (CS0165).
  • Dùng switch theo kiểu dài dòng thay cho phương thức ảo. Nếu mỗi lớp con mới đều buộc bạn sửa cùng một câu switch ở năm chỗ, hành vi đó nên nằm trong cây kế thừa. Mẫu phát huy sức mạnh với dữ liệu đến từ bên ngoài (cấu trúc JSON, thông điệp, tuple đầu vào) và với các tập kiểu khép kín do bạn kiểm soát.
  • Quên nhánh bắt mọi trường hợp trong biểu thức switch. Một đầu vào không khớp sẽ ném ngoại lệ lúc chạy; hãy đọc cảnh báo CS8509.

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

Pattern matching trong C# là gì?

Pattern matching kiểm tra một giá trị có khớp với một hình dạng hay không và, khi khớp, có thể tách các phần của nó ra thành biến ngay trong cùng bước đó. if (shape is Circle c) kiểm tra kiểu và cho bạn một biến c đúng kiểu; order is { Status: "paid", Total: > 100 } kiểm tra các thuộc tính. Mẫu (pattern) xuất hiện trong biểu thức is, câu lệnh switch và biểu thức switch.

Làm sao dùng is với một biến trong C#?

if (obj is Customer c) { ... } kiểm tra obj là một Customer (khác null) và gán nó cho c, có kiểu Customer, bên trong if. Cách này thay cho cặp lệnh cũ var c = obj as Customer; if (c != null). Biến chỉ được coi là đã gán ở những chỗ phép kiểm tra chắc chắn đúng, nên if (!(obj is Customer c)) return; vẫn cho phép dùng c sau if.

is null và == null trong C# khác nhau thế nào?

x is null luôn kiểm tra chính tham chiếu đó. x == null gọi toán tử == của kiểu nếu kiểu đó nạp chồng nó, thường cho cùng kết quả nhưng không được đảm bảo. Với điều kiện ngược lại, hãy viết x != null, !(x is null) hoặc x is not null từ C# 9.

Pattern matching trong switch với when hoạt động thế nào trong C#?

Một nhãn case có thể chứa một mẫu kèm điều kiện bảo vệ when: case Order o when o.Total > 100:. Case chỉ khớp khi mẫu khớp và điều kiện đúng, và các case được thử từ trên xuống dưới, nên hãy đặt case cụ thể hơn lên trước. Trình biên dịch báo lỗi CS8120 khi một case không bao giờ được chạm tới vì một case phía trước đã bao trùm nó.

Phiên bản C# nào bổ sung những mẫu nào?

C# 7.0: mẫu kiểu, mẫu hằng và mẫu var trong is và case, cùng với when. C# 8: biểu thức switch, mẫu thuộc tính, mẫu tuple và mẫu vị trí. C# 9: mẫu quan hệ (> 5), mẫu logic (and, or, not) và mẫu kiểu không biến. C# 10: mẫu thuộc tính mở rộng ({ Address.City: "Porto" }). C# 11: mẫu danh sách ([1, .., var last]).

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