So khớp mẫu (Patern Matching) là thao tác phổ biến trong lập trình ứng dụng. Các mẫu được tạo ra dùng để kiểm tra một giá trị có thỏa mãn với một điều kiện (hay định dạng) cho trước hay không, ví dụ số điện thoại, địa chỉ email, v.v.

Một kỹ thuật phổ biến là dùng các biểu thức thường quy (regular expression), ngoài ra có thể sử dụng các cấu trúc điều khiển như if hay switch. Bài viết này sẽ giới thiệu một vài cách thức cơ bản và các cú pháp mở rộng mới từ C# 7.0 trở về sau dùng trong việc so khớp mẫu một cách hiệu quả.

Tạo các lớp minh họa

Để dễ hình dung, chúng ta sẽ tạo các lớp hình học minh họa đơn giản trong một chương trình Console C# như sau:


public class Square

{

   public double Side { get; }
   public Square(double side)

   {

     Side = side;

   }

}

public class Circle

{

   public double Radius { get; }

   public Circle(double radius)

   {

     Radius = radius;

   }

}

public struct Rectangle

{

  public double Length { get; }

  public double Height { get; }

  public Rectangle(double length, double height)

  {

    Length = length;

    Height = height;

  }

}

public class Triangle

{

  public double Base { get; }

  public double Height { get; }

  public Triangle(double @base, double height)

  {

    Base = @base;//dấu @ được dùng vì tên biến base trùng với từ khóa

    Height = height;

  }

}

Toán tử is

Giả sử chúng ta muốn xây dựng một phương thức tính diện tích một đối tượng hình học. Mỗi đối tượng hình học sẽ có những công thức riêng để tính diện tích nên cần xác định rõ đối tượng hình học là đối tượng gì? Chúng ta có thể làm điều này với if is:


public static double ComputeArea(object shape)

{

  if (shape is Square)

  {

    var s = (Square)shape;

    return s.Side * s.Side;

  }

  else if (shape is Circle)

  {

      var c = (Circle)shape;

      return c.Radius * c.Radius * Math.PI;

   }

   throw new ArgumentException(

      message: "shape is not a recognized shape",

      paramName: nameof(shape));

}

Toán tử is sẽ kiểm tra đối tượng shape có là một hình vuông (Square) hay một hình tròn (Circle). Nếu là một trong hai đối tượng này thì sẽ trả về những công thức tính phù hợp; nếu không phải là một trong hai đối tượng trên thì sẽ phát sinh một ngoại lệ:


Circle c = new Circle(3);

Square s = new Square(3);

Rectangle r = new Rectangle(3, 2);

Console.WriteLine(ComputeArea(c));

Console.WriteLine(ComputeArea(s));

Console.WriteLine(ComputeArea(r));// phát sinh ngoại lệ

Kể từ C# 7, toán tử is được cải tiến để sử dụng một cách đơn giản hơn. Phương thức ComputerArea ở trên có thể viết lại một phiên bản khác như sau:


public static double ComputeAreaModernIs(object shape)

{

  if (shape is Square s)

     return s.Side * s.Side;

  else if (shape is Circle c)

     return c.Radius * c.Radius * Math.PI;

  else if (shape is Rectangle r)

    return r.Height * r.Length;

  throw new ArgumentException(

     message: "shape is not a recognized shape",

     paramName: nameof(shape));

}

Toán tử is lúc này vừa kiểm tra biến (shape) vừa gán biến này đến một biến mới (như c hay r) có kiểu phù hợp.

Cấu trúc switch và case

Trong phương thức ComputerArea chúng ta chỉ kiểm tra một số ít các kiểu hình học thì việc dùng is if là hợp lý. Tuy nhiên, nếu việc kiểm tra các kiểu là quá nhiều thì cấu trúc switch có thể được dùng:


public static double ComputeAreaModernSwitch(object shape)

{

  switch (shape)

  {

   case Square s:

      return s.Side * s.Side;

   case Circle c:

      return c.Radius * c.Radius * Math.PI;

   case Rectangle r:

     return r.Height * r.Length;

   default:

      throw new ArgumentException(

         message: "shape is not a recognized shape",

         paramName: nameof(shape));

  }

}

Lệnh case phải được kết thúc bởi break, return hay goto. Một cải tiến thú vị ở đây là biến của switch không bị hạn chế đến một kiểu dữ liệu nào đó, bất kỳ kiểu nào cũng có thể được sử dụng. Biến shape có thể kiểu Square, kiểu Circle hay Rectangle. Đặc biệt hơn, lệnh case cũng có thể kiểm tra với kiểu null:


switch (shape)

{

  case Square s:

    return s.Side * s.Side;

  case Circle c:

    return c.Radius * c.Radius * Math.PI;

  case Rectangle r:

    return r.Length * r.Height;

  case null:

    throw new ArgumentNullException(paramName: nameof(shape), 
              
             message: "Shape must not be null");

  default:

     throw new ArgumentException(

         message: "shape is not a recognized shape",

         paramName: nameof(shape));

}

Lệnh của nhãn default sẽ thực thi khi không có case nào được thỏa mãn.

Mệnh đề when trong biểu thức case

Giả sử chúng ta muốn tạo các trường hợp đặc biệt như hình vuông có cạnh là 0 hay hình tròn có bán kính là 0 thì sẽ có diện tích là 0. Có thể viết lại phiên khác cho ComputerArea như sau:


public static double ComputeArea_Version3(object shape)

{

  switch (shape)

  {

    case Square s when s.Side == 0:

    case Circle c when c.Radius == 0:

       return 0;

    case Square s:

      return s.Side * s.Side;

    case Circle c:

      return c.Radius * c.Radius * Math.PI;

    default:

      throw new ArgumentException(

          message: "shape is not a recognized shape",

          paramName: nameof(shape));

  }

}

Ở đây chúng ta dùng mệnh đề when trong các case đầu tiên để kiểm tra cạnh (side) và bán kính (radius) của các đối tượng hình vuông và hình tròn có bằng 0 không, nếu bằng 0 sẽ trả về diện tích bằng 0.

Chúng ta có thể kiểm tra đồng thời nhiều điều kiện trong when, xét phiên bản khác của ComputerArea:


public static double ComputeArea_Version4(object shape)

{

  switch (shape)

  {

    case Square s when s.Side == 0:

    case Circle c when c.Radius == 0:

    case Triangle t when t.Base == 0 || t.Height == 0:

    case Rectangle r when r.Length == 0 || r.Height == 0:

       return 0;

    case Square s:

       return s.Side * s.Side;

    case Circle c:

       return c.Radius * c.Radius * Math.PI;

    case Triangle t:

       return t.Base * t.Height / 2;

    case Rectangle r:

       return r.Length * r.Height;

    default:

       throw new ArgumentException(

            message: "shape is not a recognized shape",

            paramName: nameof(shape));

  }

}

Sử dụng var trong case

Cho đến thời điểm này switchcase đã mang lại cho chúng ta rất nhiều cải tiến thú vị. Một cải tiến khác là cho phép sử dụng var trong case như sau:


switch (shapeDescription)

{

  case "circle":

      return new Circle(2);

  case "square":

      return new Square(4);

  case "large-circle":

     return new Circle(12);

  case var o when (o?.Trim().Length ?? 0) == 0:

     return null;

  default:

     return "invalid shape description";

}

Cho phép dùng var when trong case giúp cho việc kiểm tra các giá trị trở nên mềm dẻo, đơn giản và hiệu quả hơn.

Kết luận

So khớp mẫu rất hữu ích trong quá trình viết chương trình hướng đối tượng với hệ thống các lớp kế thừa. Một vài kỹ thuật đơn giản nhưng hiệu quả trong bài viết này sẽ giúp chúng ta có thể thoải mái hơn khi làm việc liên quan so khớp mẫu.