C# 8.0 - Pattern Matching
اندازه‌ی قلم متن
تخمین مدت زمان مطالعه‌ی مطلب: هشت دقیقه

در نگارش‌های پیشین #C، بهبودهایی در زمینه‌ی Pattern matching وجود داشتند. در نگارش 8 نیز این بهبودها ادامه پیدا کرده‌اند که نتیجه‌ی آن به‌وجود آمدن روش جدیدی برای نوشتن عبارات switch است.


معرفی روش جدید نوشتن عبارات switch در C#8.0

فرض کنید یک enum که معرف تعدادی رنگ است را تعریف کرده‌ایم:
    public enum Rainbow
    {
        Red,
        Orange,
        Yellow,
        Green,
        Blue,
        Indigo,
        Violet
    }
همچنین کلاسی را نیز جهت تشکیل اشیاء رنگ مبتنی بر RGB تدارک دیده‌ایم:
    class RGBColor
    {
        internal byte Red { get; }
        internal byte Green { get; }
        internal byte Blue { get; }

        internal RGBColor(byte red, byte green, byte blue)
        {
            Red = red;
            Green = green;
            Blue = blue;
        }

        public override string ToString() => $"rgb({Red}, {Green}, {Blue})";
    }
اکنون هدف ما این است که اگر یکی از اعضای این enum را انتخاب کردیم، بتوانیم معادل رنگ RGB آن‌را نیز داشته باشیم. برای این منظور می‌توان switch ساده‌ی زیر را تشکیل داد:
        internal static RGBColor FromRainbow(Rainbow rainbowBolor)
        {
            switch (rainbowBolor)
            {
                case Rainbow.Red:
                    return new RGBColor(0xFF, 0x00, 0x00);
                case Rainbow.Orange:
                    return new RGBColor(0xFF, 0x7F, 0x00);
                case Rainbow.Yellow:
                    return new RGBColor(0xFF, 0xFF, 0x00);
                case Rainbow.Green:
                    return new RGBColor(0x00, 0xFF, 0x00);
                case Rainbow.Blue:
                    return new RGBColor(0x00, 0x00, 0xFF);
                case Rainbow.Indigo:
                    return new RGBColor(0x4B, 0x00, 0x82);
                case Rainbow.Violet:
                    return new RGBColor(0x94, 0x00, 0xD3);
                default:
                    throw new ArgumentException(message: "invalid enum value", paramName: nameof(rainbowBolor));
            };
        }
این کاری است که تا پیش از C# 8.0 به صورت متداولی انجام می‌شود. اکنون در C# 8.0 می‌توان عبارت switch فوق را به صورت زیر خلاصه کرد:
        internal static RGBColor TasteTheRainbow(Rainbow rainbowColor) =>
            rainbowColor switch
        {
            Rainbow.Red => new RGBColor(0xFF, 0x00, 0x00),
            Rainbow.Orange => new RGBColor(0xFF, 0x7F, 0x00),
            Rainbow.Yellow => new RGBColor(0xFF, 0xFF, 0x00),
            Rainbow.Green => new RGBColor(0x00, 0xFF, 0x00),
            Rainbow.Blue => new RGBColor(0x00, 0x00, 0xFF),
            Rainbow.Indigo => new RGBColor(0x4B, 0x00, 0x82),
            Rainbow.Violet => new RGBColor(0x94, 0x00, 0xD3),
            _ => throw new ArgumentException(message: "invalid enum value", paramName: nameof(rainbowColor)),
        };
- در این روش جدید، بجای اینکه با ذکر switch و سپس، مقداری/نوعی شروع شود، ابتدا با نوع شروع می‌شود و سپس واژه‌ی کلیدی switch ذکر خواهد شد.
- در ادامه تمام caseها حذف می‌شوند و بجای آن‌ها صرفا مقادیر مدنظر باقی می‌ماند. در اینجا <= به صورت expressed as خوانده می‌شود.
- caseهای مختلف با کاما از هم جدا می‌شوند.
- همچنین در سطر آخر آن نیز از یک discard استفاده شده‌است که معادل همان حالت default یا حالتی است که هیچ تطابقی صورت نگرفته باشد.
- به علاوه اگر دقت کنید، نتیجه‌ی نهایی این switch جدید، به صورت یک مقدار، توسط متد TasteTheRainbow، بازگشت داده شده‌است. بنابراین نوشتن یک چنین عباراتی در C# 8.0، مجاز است:
var operation = "+";
int a = 1, b = 2;
var result = operation switch
{
   "+" => a + b,
   "-" => a - b,
   "/" => a / b,
     _ => throw new NotSupportedException()
};


معرفی Property Patterns در C# 8.0

کلاس زیر را درنظر بگیرید که از تعدادی خاصیت عمومی تشکیل شده‌است:
    class Address
    {
        public string AddressLine1 { get; set; }
        public string AddressLine2 { get; set; }
        public string City { get; set; }
        public string State { get; set; }
        public string PostalCode { get; set; }
        public string CountryRegion { get; set; }
    }
اکنون فرض کنید که می‌خواهیم مالیات فروش را بر اساس آدرس و محل آن، محاسبه کنیم. در C# 8.0 با معرفی قابلیت الگوهای خواص، می‌توان بر روی آدرس، یک switch را تشکیل داد و سپس تک تک خواص آن‌را ارزیابی کرد:
    static class PropertyPatterns
    {
        internal static decimal ComputeSalesTax(
            Address location,
            decimal salePrice) =>
            location switch
        {
            { State: "Fars" } => salePrice * 0.06m,
            { State: "Tehran", City: "Tehran" } => salePrice * 0.056m,

            // Other cases removed for brevity...
            _ => 0M
        };
    }
در اینجا، سمت چپ هر case، داخل یک {} قرار می‌گیرد و در آن می‌توان مقادیر چندین خاصیت شیء location دریافتی را بررسی کرد. برای نمونه در سطر دوم آن، روش ارزیابی بیش از یک خاصیت را نیز مشاهده می‌کنید که روش ذکر آن شبیه به تعریف شیء‌های JSON است. در آخر نیز توسط یک discard، حالت default ذکر شده‌است.


معرفی Tuple Patterns در C# 8.0

در switch‌های C# 8.0، می‌توان از tuples نیز برای تشکیل قسمت case و همچنین مقداری که قرار است switch بر روی آن صورت گیرد، استفاده کرد:
    static class TuplePatterns
    {
        internal static string RockPaperScissors(
            string first,
            string second)
            => (first, second) switch
        {
            ("rock", "paper") => "Rock is covered by Paper. Paper wins!",
            ("rock", "scissors") => "Rock breaks Scissors. Rock wins!",
            ("paper", "rock") => "Paper covers Rock. Paper wins!",
            ("paper", "scissors") => "Paper is cut by Scissors. Scissors wins!",
            ("scissors", "rock") => "Scissors is broken by Rock. Rock wins!",
            ("scissors", "paper") => "Scissors cuts Paper. Scissors wins!",
            (_, _) => "tie"
        };
    }
در اینجا بر روی tuple ای که به صورت (first, second) تعریف شده، یک switch تعریف می‌شود. سپس برای نمونه 6 حالت مختلف برای آن پیش‌بینی شده و یک حالت default که آن نیز توسط discards معرفی می‌شود.


بهبودهای Pattern Matching بر روی اشیاء در C# 8.0

فرض کنید شیء پایه‌ی Shape را تعریف و بر اساس آن دو شیء جدید دایره و مستطیل را ایجاد کرده‌ایم:
    class Shape
    {
        protected internal double Height { get; }
        protected internal double Length { get; }

        protected Shape(double height = 0, double length = 0)
        {
            Height = height;
            Length = length;
        }
    }

    class Circle : Shape
    {
        internal double Radius => Height / 2;
        internal double Diameter => Radius * 2;
        internal double Circumference => 2 * Math.PI * Radius;

        internal Circle(double height = 10, double length = 10)
            : base(height, length) { }
    }

    class Rectangle : Shape
    {
        internal bool IsSquare => Height == Length;

        internal Rectangle(double height = 10, double length = 10)
            : base(height, length) { }
    }
امکان Pattern Matching بر روی اشیاء، در C# 7x نیز وجود دارد؛ اما در C# 8.0 می‌توان از روش جدید بیان عبارت switch آن به صورت زیر نیز در این حالت استفاده کرد:
    static class ObjectPatterns
    {
        internal static string ShapeDetails(this Shape shape)
            => shape switch
        {
            Circle c => $"circle with (C): {c.Circumference}",
            Rectangle s when s.IsSquare => $"L:{s.Length} H:{s.Height}, square",
            Rectangle r => $"L:{r.Length} H:{r.Height}, rectangle",
            _ => "Unknown shape!" // Discard
        };
    }
در اینجا یک شیء، به متد ShapeDetails ارسال شده و سپس جزئیاتی از آن دریافت می‌شود. مطابق روش C# 8.0، در اینجا نیز کار با ذکر نوع و سپس عبارت switch، شروع می‌شود. در ادامه روش بررسی نوع‌ها را در caseهای این سوئیچ ملاحظه می‌کنید. اگر در قسمت case آن Circle c ذکر شد، یعنی نوع shape از نوع دایره بوده و همچنین در همینجا می‌توان متغیر c را بر این اساس تعریف کرد و از آن استفاده نمود و یا می‌توان به کمک واژه‌ی کلیدی when، بر روی این متغیری که جدید تعریف شده، شرطی را نیز بررسی کرد. حالت default آن هم توسط discards معرفی می‌شود.


معرفی Positional Patterns در C# 8.0

در اینجا یک Point را داریم که می‌خواهیم بر اساس آن یک Quadrant را استخراج کنیم:
    class Point
    {
        public int X { get; }

        public int Y { get; }

        public Point(int x, int y) => (X, Y) = (x, y);

        public void Deconstruct(out int x, out int y) => (x, y) = (X, Y);
    }

    enum Quadrant
    {
        Unknown,
        Origin,
        One,
        Two,
        Three,
        Four,
        OnBorder
    }
برای این منظور می‌توان از الگوهای موقعیتی C# 8.0 استفاده کرد:
    static class PositionalPatterns
    {
        internal static Quadrant AsQuadrant(Point point) => point switch
        {
            (0, 0) => Quadrant.Origin,
            var (x, y) when x > 0 && y > 0 => Quadrant.One,
            var (x, y) when x < 0 && y > 0 => Quadrant.Two,
            var (x, y) when x < 0 && y < 0 => Quadrant.Three,
            var (x, y) when x > 0 && y < 0 => Quadrant.Four,
            (_, _) => Quadrant.OnBorder, // Either are 0, but not both
            _ => Quadrant.Unknown
        };
    }
اگر به کلاس Point دقت کنید، یک قسمت Deconstruct هم دارد. به همین جهت در قسمت‌های case این switch، زمانیکه برای مثال (0,0) ذکر می‌شود (که یک tuple literal است)، به صورت خودکار یک شیء Point متناظر را با مقادیر X و Y آن، تشکیل می‌دهد. همچنین روش‌های مختلف مقایسه‌ی مقادیر x و y این tuple را نیز در caseهای مختلف آن مشاهده می‌کنید.
در اینجا اگر دقت کنید و case مخصوص discards معرفی شده‌است. اولی برای حالت‌هایی است که هیچکدام از شرایط پیش از آن را برآورده نمی‌کند، مانند حالت (1,0)، در غیراینصورت سطر بعد از آن بازگشت داده می‌شود.
  • #
    ‫۵ سال و ۳ ماه قبل، پنجشنبه ۱۶ خرداد ۱۳۹۸، ساعت ۱۵:۰۹
    چند مثال تکمیلی
    - بررسی StatusCode یک HttpException توسط روش جدید نوشتن عبارات switch و pattern matching:
            private string HandleHttpError(Exception ex, string text)
            {
                switch(ex)
                {
                    case HttpException httpEx:
                        return httpEx.StatusCode switch
                        {
                           404 => "[NOT FOUND]",
                           500 => "[INTERNAL SERVER ERROR]",
                           _ => text
                        };
                    default:
                        return text;
                }
            }
    - بررسی StatusCode یک HttpException توسط الگوهای خواص
            private string HandleHttpError(Exception ex, string text)
            {
                if (ex is HttpException { StatusCode: 404})
                {
                    return $"[NOT FOUND]";
                }
                else if (ex is HttpException { StatusCode: 500})
                {
                    return $"[INTERNAL SERVER ERROR]";
                }
                else
                {
                    return text;
                }
            }
  • #
    ‫۲ سال و ۱۰ ماه قبل، یکشنبه ۹ آبان ۱۴۰۰، ساعت ۲۳:۱۵
    بهبود «Property Patterns معرفی شده‌ی در C# 8.0» در C# 10.0

    در C# 8.0 برای بررسی خواص تو در تو باید از الگوی زیر استفاده کرد:
    ....{property: {subProperty: pattern}}....
    که این الگو در C# 10.0 به صورت زیر ساده شده‌است:
    ....{property.subProperty: pattern}....

    یک مثال:
    رکورد زیر را در نظر بگیرید:
    public record TestRec(string name, string surname);
    روش پیشین دسترسی به خاصیت طول name به صورت زیر است:
    string TakeFourSymbols(TestRec obj) => obj switch
    {
        TestRec { name: {Length: > 4} } rec => rec.name.Substring(0,4),
        // ...
    };
    که اکنون در C# 10.0 کمی متداول‌تر و ساده‌تر شده‌است:
    string TakeFourSymbols(TestRec obj) => obj switch
    {
        TestRec { name.Length: > 4 } rec => rec.name.Substring(0,4),
        // ...
    };