Search

C# Pattern Matching: Từ Cơ Bản Đến Nâng Cao (C# 12)

C# Pattern Matching: Từ Cơ Bản Đến Nâng Cao (C# 12)

Pattern matching là tính năng mình thích nhất trong C# hiện đại. Không phải vì nó fancy — mà vì nó biến những đoạn code if-else lồng nhau đau đầu thành expression đọc một lần hiểu ngay.

Vấn đề là pattern matching trong C# được bổ sung dần qua nhiều version: C# 7 thêm type pattern, C# 8 thêm switch expression và property pattern, C# 9 thêm relational và logical, C# 11 thêm list pattern. Mỗi version một ít, nên nhiều dev chỉ biết phần cơ bản mà bỏ qua những thứ mạnh hơn.

Bài viết này gom tất cả lại — từ nền tảng đến nâng cao, mỗi pattern kèm ví dụ thực tế và so sánh code trước/sau. Đọc xong bạn sẽ biết dùng pattern nào cho tình huống nào.

Type Pattern — nền tảng (C# 7)

Check kiểu và cast trong một bước:

// ❌ trước C# 7 — check rồi cast riêng
if (shape is Circle)
{
    var circle = (Circle)shape;
    return Math.PI * circle.Radius * circle.Radius;
}

// ✅ type pattern — check + cast cùng lúc
if (shape is Circle circle)
{
    return Math.PI * circle.Radius * circle.Radius;
}

Một dòng thay hai dòng, và không còn risk InvalidCastException nếu type sai — is check trước rồi mới assign.

Kết hợp với not:

// null check
if (order is not null)
{
    ProcessOrder(order);
}

// type + not
if (response is not OkObjectResult)
{
    return HandleError(response);
}

Switch Expression — thay thế switch statement (C# 8)

Switch expression là bước nhảy lớn nhất. Nó biến switch từ statement (làm gì đó) thành expression (trả về giá trị):

// ❌ switch statement — dài, nhiều break, dễ quên break
string GetStatusText(OrderStatus status)
{
    switch (status)
    {
        case OrderStatus.Pending:
            return "Đang chờ xử lý";
        case OrderStatus.Processing:
            return "Đang xử lý";
        case OrderStatus.Shipped:
            return "Đã giao cho vận chuyển";
        case OrderStatus.Delivered:
            return "Đã giao hàng";
        case OrderStatus.Cancelled:
            return "Đã hủy";
        default:
            throw new ArgumentOutOfRangeException(nameof(status));
    }
}

// ✅ switch expression — gọn, rõ, không quên case
string GetStatusText(OrderStatus status) => status switch
{
    OrderStatus.Pending    => "Đang chờ xử lý",
    OrderStatus.Processing => "Đang xử lý",
    OrderStatus.Shipped    => "Đã giao cho vận chuyển",
    OrderStatus.Delivered  => "Đã giao hàng",
    OrderStatus.Cancelled  => "Đã hủy",
    _ => throw new ArgumentOutOfRangeException(nameof(status))
};

_ là discard pattern — match mọi thứ, tương đương default. Compiler sẽ warn nếu bạn quên handle enum value — điều mà switch statement không làm.

Switch expression với type pattern

Xử lý kiểu polymorphic mà không cần virtual method:

double CalculateArea(Shape shape) => shape switch
{
    Circle c    => Math.PI * c.Radius * c.Radius,
    Rectangle r => r.Width * r.Height,
    Triangle t  => t.Base * t.Height / 2,
    _           => throw new NotSupportedException($"Unknown shape: {shape.GetType().Name}")
};

Cực kỳ hữu ích khi bạn không own class hierarchy (third-party types) hoặc muốn tránh thêm method vào domain model.

Property Pattern — match theo property (C# 8)

Check giá trị property của object mà không cần extract ra biến:

// ❌ if-else truyền thống
decimal CalculateDiscount(Customer customer)
{
    if (customer.Tier == "Gold" && customer.YearsActive > 5)
        return 0.20m;
    if (customer.Tier == "Gold")
        return 0.15m;
    if (customer.Tier == "Silver")
        return 0.10m;
    return 0;
}

// ✅ property pattern — đọc như bảng quy tắc
decimal CalculateDiscount(Customer customer) => customer switch
{
    { Tier: "Gold", YearsActive: > 5 }  => 0.20m,
    { Tier: "Gold" }                     => 0.15m,
    { Tier: "Silver", YearsActive: > 3 } => 0.12m,
    { Tier: "Silver" }                   => 0.10m,
    _                                    => 0m
};

Nhìn vào switch expression này, bạn thấy ngay business rule — không cần đọc if-else lồng nhau. Review PR mà thấy format này là approve nhanh hơn hẳn.

Nested property pattern

Check property lồng nhau:

// check Address.Country của Customer
string GetShippingZone(Order order) => order switch
{
    { Customer.Address.Country: "VN" }              => "Domestic",
    { Customer.Address.Country: "SG" or "TH" or "MY" } => "SEA",
    { Customer.Address.Country: "US" or "CA" }       => "Americas",
    { ShippingMethod: "Express" }                    => "Priority",
    _                                                => "International"
};

Customer.Address.Country — traverse qua navigation property. Không cần null check từng level vì property pattern tự handle null (nếu Customer null thì pattern không match, không throw NullReferenceException).

Relational Pattern — so sánh giá trị (C# 9)

Dùng <, >, <=, >= trực tiếp trong pattern:

// ❌ if-else chain
string GetTemperatureDescription(double temp)
{
    if (temp < 0) return "Đóng băng";
    if (temp < 15) return "Lạnh";
    if (temp < 25) return "Mát mẻ";
    if (temp < 35) return "Nóng";
    return "Cực nóng";
}

// ✅ relational pattern
string GetTemperatureDescription(double temp) => temp switch
{
    < 0   => "Đóng băng",
    < 15  => "Lạnh",
    < 25  => "Mát mẻ",
    < 35  => "Nóng",
    >= 35 => "Cực nóng"
};

Switch expression evaluate từ trên xuống dưới — order quan trọng. < 0 check trước, nên < 15 ngầm hiểu là >= 0 và < 15.

Kết hợp với and, or

// HTTP status code handler
string CategorizeResponse(int statusCode) => statusCode switch
{
    >= 200 and < 300 => "Success",
    >= 300 and < 400 => "Redirect",
    >= 400 and < 500 => "Client Error",
    >= 500           => "Server Error",
    _                => "Unknown"
};

// grading system
char GetGrade(double score) => score switch
{
    >= 90 and <= 100 => 'A',
    >= 80 and < 90   => 'B',
    >= 70 and < 80   => 'C',
    >= 60 and < 70   => 'D',
    >= 0 and < 60    => 'F',
    _                => throw new ArgumentOutOfRangeException(nameof(score))
};

Logical Pattern — not, and, or (C# 9)

Negate, combine patterns:

// null + empty check
if (name is not (null or ""))
{
    // name có giá trị
}

// complex validation
bool IsValidAge(int? age) => age is not null and >= 0 and <= 150;

// exclude specific values
string Categorize(string input) => input switch
{
    null or ""           => "Empty",
    not null and { Length: < 3 } => "Too short",
    _                    => "Valid"
};

not null and { Length: < 3 } — kết hợp logical pattern với property pattern. Đọc tự nhiên: "not null VÀ có Length nhỏ hơn 3".

Tuple Pattern — match nhiều giá trị cùng lúc (C# 8)

Khi cần switch trên nhiều biến:

// ❌ if-else nested hell
string GetAction(Role role, bool isOwner, bool isPublished)
{
    if (role == Role.Admin)
        return "Full access";
    if (role == Role.Editor && isOwner)
        return "Edit + Delete";
    if (role == Role.Editor && !isOwner && isPublished)
        return "Edit only";
    // ... tiếp tục 10 case nữa
}

// ✅ tuple pattern — truth table dễ đọc
string GetAction(Role role, bool isOwner, bool isPublished) =>
    (role, isOwner, isPublished) switch
    {
        (Role.Admin, _, _)          => "Full access",
        (Role.Editor, true, _)      => "Edit + Delete",
        (Role.Editor, false, true)  => "Edit only",
        (Role.Editor, false, false) => "Request review",
        (Role.Viewer, _, true)      => "Read only",
        (Role.Viewer, _, false)     => "No access",
        _                           => "Unknown"
    };

_ trong tuple là wildcard — match bất kỳ giá trị nào. (Role.Admin, _, _) nghĩa là: Admin thì full access, bất kể isOwner hay isPublished.

Pattern này tỏa sáng cho permission matrix, state machine transitions, hay bất kỳ logic nào phụ thuộc nhiều biến.

State machine

OrderStatus GetNextStatus(OrderStatus current, OrderEvent trigger) =>
    (current, trigger) switch
    {
        (OrderStatus.Pending, OrderEvent.Pay)       => OrderStatus.Paid,
        (OrderStatus.Pending, OrderEvent.Cancel)     => OrderStatus.Cancelled,
        (OrderStatus.Paid, OrderEvent.Ship)          => OrderStatus.Shipped,
        (OrderStatus.Paid, OrderEvent.Refund)        => OrderStatus.Refunded,
        (OrderStatus.Shipped, OrderEvent.Deliver)    => OrderStatus.Delivered,
        (OrderStatus.Shipped, OrderEvent.Return)     => OrderStatus.Returned,
        _ => throw new InvalidOperationException(
            $"Cannot {trigger} when order is {current}")
    };

Nhìn vào đây bạn thấy ngay toàn bộ state transitions. Thêm state mới? Thêm dòng. Compiler warn nếu quên case (khi dùng enum). Khó break hơn if-else chain rất nhiều.

List Pattern — match collection (C# 11)

Đây là pattern mới nhất và cũng gây wow nhất:

// check cấu trúc collection
string DescribeList(int[] numbers) => numbers switch
{
    []              => "Empty",
    [var single]    => $"Single: {single}",
    [var first, var second] => $"Pair: {first}, {second}",
    [var first, .., var last] => $"From {first} to {last} ({numbers.Length} items)",
};

[] match empty. [var single] match chính xác 1 phần tử. [var first, .., var last] match 2+ phần tử — .. là slice pattern, match "phần còn lại".

Use case thực tế: parse command

string HandleCommand(string[] args) => args switch
{
    ["help"]                     => ShowHelp(),
    ["version"]                  => ShowVersion(),
    ["add", var name]            => AddItem(name),
    ["add", var name, var qty]   => AddItem(name, int.Parse(qty)),
    ["remove", var id]           => RemoveItem(id),
    ["list", "--all"]            => ListAll(),
    ["list", "--filter", var f]  => ListFiltered(f),
    [var unknown, ..]            => $"Unknown command: {unknown}",
    []                           => ShowHelp()
};

Thay vì if (args.Length >= 1 && args[0] == "add" && ...) — list pattern diễn đạt ý đồ rõ ràng hơn rất nhiều.

Validate data shape

bool IsValidCsvRow(string[] columns) => columns switch
{
    // phải có ít nhất 3 cột, cột đầu không rỗng
    [{ Length: > 0 }, _, _, ..]  => true,
    _                            => false
};

// destructure + validate
(string Name, decimal Price)? ParseProduct(string[] parts) => parts switch
{
    [var name, var price] when decimal.TryParse(price, out var p) && p > 0
        => (name, p),
    _   => null
};

{ Length: > 0 } bên trong list pattern — kết hợp property pattern với list pattern. Nested patterns là chỗ C# pattern matching thực sự mạnh.

When Guard — thêm điều kiện cho pattern

Khi pattern alone không đủ express điều kiện:

string EvaluateLoan(LoanApplication app) => app switch
{
    { CreditScore: >= 750, Income: >= 50000 }
        => "Auto approved",

    { CreditScore: >= 650, Income: var income }
        when income >= app.LoanAmount * 0.3m
        => "Conditionally approved",

    { CreditScore: >= 650 }
        => "Requires review",

    { CreditScore: < 650 } when app.HasGuarantor
        => "Requires guarantor review",

    _ => "Declined"
};

when cho phép thêm runtime condition — ở đây check income >= loanAmount * 0.3. Pattern check structure, when check logic.

Var Pattern — capture mà không check

var match bất kỳ giá trị nào và capture nó vào biến:

// capture intermediate value
string FormatOrder(Order order) => order switch
{
    { Items: var items } when items.Count > 10
        => $"Bulk order: {items.Count} items",

    { Total: var total } when total > 1000
        => $"Large order: {total:C}",

    { Customer.Tier: "VIP", Total: var t }
        => $"VIP order: {t:C}",

    _ => "Standard order"
};

var total capture order.Total vào biến total để dùng trong kết quả. Không check type hay value — chỉ capture.

Kết hợp patterns: ví dụ thực chiến

API response handling

async Task<IActionResult> HandleExternalApiResponse(
    HttpResponseMessage response,
    string endpoint) => response switch
{
    { IsSuccessStatusCode: true, Content: var content }
        => Ok(await content.ReadFromJsonAsync<ApiData>()),

    { StatusCode: HttpStatusCode.NotFound }
        => NotFound(new { message = $"{endpoint} not found" }),

    { StatusCode: HttpStatusCode.Unauthorized }
        => Unauthorized(),

    { StatusCode: HttpStatusCode.TooManyRequests,
      Headers.RetryAfter.Delta: var delay } when delay is not null
        => StatusCode(429, new { retryAfter = delay.Value.TotalSeconds }),

    { StatusCode: var code } when (int)code >= 500
        => StatusCode(502, new { message = "Upstream server error" }),

    _ => StatusCode(500, new { message = "Unexpected response" })
};

Một method handle mọi response case. Đọc từ trên xuống dưới hiểu ngay flow xử lý. Thêm case mới? Thêm dòng.

Validation pipeline

string? ValidateProduct(CreateProductRequest req) => req switch
{
    { Name: null or "" }         => "Name is required",
    { Name.Length: > 200 }       => "Name too long (max 200)",
    { Price: <= 0 }              => "Price must be positive",
    { Price: > 999_999 }         => "Price exceeds maximum",
    { Category: null or "" }     => "Category is required",
    { Sku: { Length: not 8 } }   => "SKU must be exactly 8 characters",
    _                            => null  // valid
};

// sử dụng
var error = ValidateProduct(request);
if (error is not null) return BadRequest(error);

Validation rules đọc như checklist — dễ thêm, dễ sửa, dễ review. Trả về null nghĩa là valid. Pattern matching biến validation từ spaghetti if-else thành bảng quy tắc rõ ràng.

Exception handler

// trong global exception middleware
(int statusCode, string message) = exception switch
{
    ArgumentException { ParamName: var param }
        => (400, $"Invalid parameter: {param}"),

    KeyNotFoundException
        => (404, "Resource not found"),

    UnauthorizedAccessException
        => (401, "Authentication required"),

    InvalidOperationException { Message: var msg }
        when msg.Contains("concurrency")
        => (409, "Data was modified by another user"),

    OperationCanceledException
        => (499, "Request cancelled"),

    _ => (500, "Internal server error")
};

Deconstruct tuple ngay khi assign — (statusCode, message) nhận hai giá trị cùng lúc. Gọn hơn switch statement + assignment riêng lẻ.

Performance: có nên lo?

Câu hỏi mình hay gặp: switch expression có chậm hơn if-else không?

Câu trả lời ngắn: không đáng kể. Compiler optimize switch expression rất tốt — phần lớn trường hợp nó generate code tương đương hoặc tốt hơn if-else chain (nhờ jump table cho constant patterns).

Trường hợp duy nhất cần chú ý: property pattern trên object phức tạp với nhiều nested property access. Mỗi . là một dereference. Nhưng nếu bạn đang worry về cost của property access thì vấn đề không nằm ở pattern matching — mà ở architecture.

Benchmark nhanh trên 1 triệu iterations:

if-else chain (5 cases):      ~3.2ms
switch expression (5 cases):   ~3.1ms
property pattern (3 props):    ~3.4ms

Sai biệt nằm trong noise — không đáng optimize trừ hot path cực kỳ nhạy.

Những lỗi hay gặp

Quên discard _ ở cuối. Switch expression phải exhaustive — phải cover mọi case. Compiler báo warning nếu thiếu _. Đừng ignore warning này, nó bảo vệ bạn khỏi runtime exception.

Order pattern sai. Patterns evaluate từ trên xuống — specific pattern phải đặt TRƯỚC general pattern:

// ❌ sai — pattern đầu match hết, hai cái sau unreachable
animal switch
{
    Animal a     => "Some animal",    // match mọi Animal
    Dog d        => "Dog",            // unreachable!
    Cat c        => "Cat",            // unreachable!
};

// ✅ đúng — specific trước
animal switch
{
    Dog d        => "Dog",
    Cat c        => "Cat",
    Animal a     => "Some animal",
};

Dùng pattern matching khi if-else đơn giản hơn. Một cái null check không cần switch expression. if (x is null) return; đủ rồi. Pattern matching mạnh khi có nhiều case hoặc cần destructuring — đừng lạm dụng cho mọi thứ.

Quên null case. Property pattern KHÔNG match null:

// { Tier: "Gold" } KHÔNG match nếu customer là null
customer switch
{
    { Tier: "Gold" } => 0.15m,
    null             => 0m,     // phải handle null riêng
    _                => 0.05m
};

Tổng kết

Pattern matching đã biến C# từ một ngôn ngữ verbose thành một ngôn ngữ cực kỳ expressive. Không phải mọi nơi đều cần pattern matching — nhưng biết dùng đúng chỗ thì code gọn hơn, ý đồ rõ hơn, và ít bug hơn.

Cheat sheet nhanh để chọn pattern:

  • Check type + cast → type pattern is Type name
  • Map value → value → switch expression
  • Check property values → property pattern { Prop: value }
  • So sánh số → relational pattern > 0 and < 100
  • Exclude giá trị → logical pattern not null
  • Match nhiều biến → tuple pattern (a, b) switch
  • Match collection → list pattern [first, .., last]
  • Điều kiện runtimewhen guard

Mình khuyên bắt đầu bằng switch expression — nó là pattern matching feature bạn sẽ dùng nhiều nhất, và cũng dễ học nhất. Từ đó mở rộng sang property pattern và relational pattern khi cần. List pattern dùng khi xử lý collection. Tuple pattern cho state machine và permission matrix.

Viết code mà người khác đọc một lần hiểu ngay — đó là giá trị lớn nhất pattern matching mang lại.

Culi Dev

Culi Dev

Enjoy coding, enjoy life!

Leave a comment

Your email address will not be published. Required fields are marked *

Your experience on this site will be improved by allowing cookies Cookie Policy