معرفی List Patterns Matching در C# 11
اندازه‌ی قلم متن
تخمین مدت زمان مطالعه‌ی مطلب: پنج دقیقه

در C# 11، افزونه‌ای به switch expressionها اضافه شده‌است که امکان بررسی توالی مقادیر آرایه‌ها و مجموعه‌ها را نیز می‌دهد که به آن list expressions هم می‌گویند. List Patterns امکان بررسی شکل یک لیست و یا آرایه را ممکن می‌کنند. برای مثال اگر نیاز است بررسی کنیم که آیا مجموعه‌ای با یک مقدار خاص، شروع می‌شود، پایان می‌یابد و یا حاوی آن است، List Patterns مفید واقع خواهند شد. در اینجا List Patterns، با [] مشخص می‌شوند و در بین []ها، توالی مقادیری را که قرار است با اعضای مجموعه‌ی مشخص شده، انطباق داده شوند، مشخص می‌کنیم. این افزونه به همراه ویژگی slice pattern نیز هست که امکان انطباق با صفر و یا چند المان یک مجموعه را میسر می‌کند. در این حالت از دو نقطه برای نمایش آن در بین []ها استفاده می‌شود. برای مثال الگوی زیر:
[1, 2, .., 10]
با تمام آرایه‌های زیر انطباق دارد:
int[] arr1 = { 1, 2, 10 };
int[] arr2 = { 1, 2, 5, 10 };
int[] arr3 = { 1, 2, 5, 6, 7, 8, 9, 10 };

بررسی چند مثال جهت آشنایی با مفهوم List Patterns

ابتدا مجموعه‌ی زیر را در نظر بگیرید:
int[] collection = { 1, 2, 3, 4 };

الف) روش انطباق با یک توالی مشخص
Console.WriteLine(collection is [1, 2, 3, 4]); // True
Console.WriteLine(collection is [1, 2, 4]); // False
توالی مشخص شده‌ی در الگوی اول، دقیقا با توالی عناصر آرایه انطباق دارد. اما در حالت دوم، چون توالی اعداد الگوی مشخص شده، با توالی اعداد آرایه یکی نیست، انطباقی رخ نداده‌است.

ب) امکان استفاده از discard و همچنین لیستی از عناصر
Console.WriteLine(collection is [_, 2, _, 4]); // True
Console.WriteLine(collection is [.., 3, _]); // True
- اگر نیاز به صرفنظر کردن از عناصر خاصی در یک توالی بود، می‌توان از discard و یا همان _ استفاده کرد؛ مانند الگوی اول. الگوی اول به معنای نیاز به انطباق با چهار عدد است که حتما باید دومین و چهارمین آن‌ها اعداد 2 و 4 باشند؛ اما مقدار اولین و سومین آن‌ها، مهم نیست.
- الگوی دوم به معنای تعریف یک توالی نامشخص، اما خاتمه یافته‌ای با عنصر 3 است و سپس صرفنظر کردن از آخرین عنصر آرایه.

در مثال زیر، الگوی انطباق با مجموعه‌ای که حداقل دو عضو دلخواهی را دارد، مشاهده می‌کنید:
if (new[] { 6, 7, 8 } is [_, _, ..])
{
   Console.WriteLine($"collection with at least two items");
}
و الگوی انطباق با مجموعه‌ای که اولین و آخرین عضو آن صفر هستند:
if (new[] { 0, 42, 42, 0 } is [0, .., 0])
{
   Console.WriteLine($"collection with first and last element equal to 0");
}


ج) امکان تعریف اعمال منطقی
Console.WriteLine(collection is [_, >= 2, _, _]); // True
بر اساس این الگو، هر مجموعه‌ی چهارتایی که عنصر دوم آن، بزرگتر و یا مساوی 2 باشد، معتبر شناخته می‌شود؛ صرفنظر از مقدار سایر عناصر آن.

در مثال زیر، الگوی انطباق با مجموعه‌ای را که اولین عضو آن یک عدد مثبت است، مشاهده می‌کنید:
if (new[] { 9, -1, -2 } is [> 0, ..])
{
   Console.WriteLine($"collection with positive first element");
}
و یا الگوی انطباق با مجموعه‌ای که دومین عضو آن، یکی از دو عدد 42 و منهای 42 می‌تواند باشد:
if (new[] { 1, 42, 0 } is [_, 42 or -42, ..])
{
   Console.WriteLine($"collection with second element equal to 42 or -42");
}


یک مثال دیگر: بررسی نحوه‌ی عملکرد List Patterns

namespace CS11Tests;

public static class ListPatternsMatching
{
    public static void Test()
    {
        Console.WriteLine(CheckSwitch(new[] { 1, 2, 10 }));          // prints 1
        Console.WriteLine(CheckSwitch(new[] { 1, 2, 7, 3, 3, 10 })); // prints 1
        Console.WriteLine(CheckSwitch(new[] { 1, 2 }));              // prints 2
        Console.WriteLine(CheckSwitch(new[] { 1, 3 }));              // prints 3
        Console.WriteLine(CheckSwitch(new[] { 1, 3, 5 }));           // prints 4
        Console.WriteLine(CheckSwitch(new[] { 2, 5, 6, 7 }));        // prints 50
    }

    public static int CheckSwitch(int[] values)
        => values switch
        {
            [1, 2, .., 10] => 1,
            [1, 2] => 2,
            [1, _] => 3,
            [1, ..] => 4,
            [..] => 50
        };
}
توضیحات:

- اولین الگوی تعریف شده‌ی در متد CheckSwitch، به معنای انطباق با هر توالی است که با 1 و 2 شروع می‌شود و سپس می‌تواند شامل هر نوع توالی دلخواهی باشد (صرفنظر از مقدار و یا ترتیب این مقادیر) و در نهایت با عدد 10 خاتمه پیدا می‌کند.
- دومین الگوی تعریف شده، تنها یک آرایه‌ی دو عضوی با مقادیر مشخص 1 و 2 را می‌پذیرد.
- توالی قابل انطباق با سومین الگوی تعریف شده، از دو عضو تشکیل می‌شود. اولین عضو آن حتما باید 1 باشد و مقدار دومین عضو آن مهم نیست.
- توالی قابل انطباق با چهارمین الگوی تعریف شده، از یک یا چند عضو دلخواه تشکیل می‌شود که اولین عضو آن حتما باید عدد 1 باشد.
- هر توالی تعریف شده‌ای با پنجمین الگوی تعریف شده، انطباق پیدا می‌کند.


امکان ترکیب list pattern matching و object pattern matching

در مثال‌های زیر، نمونه‌ای از ترکیب list pattern matching و object pattern matching را جهت ساخت شرط‌های پیچیده‌ای، مشاهده می‌کنید:
if (new[] { 1, 2, 3 } is [var first, _, _])
{
   Console.WriteLine($"three item collection with first item {first}");
}

if (new[] { 4, 5, 6 } is [_, var second, _])
{
   Console.WriteLine($"three item collection with second item {second}");
}
این الگو که var pattern هم نامیده می‌شود، به همراه ذکر var و نام یک متغیر است. در این حالت کار الگو، دریافت مقدار واقع شده‌ی در آن موقعیت خاص است.
نمونه مثالی از این قابلیت جهت جدا سازی اجزای یک URL:
var uri = new Uri("http://www.mysite.com/categories/category-a/sub-categories/sub-category-a.html");
var result = uri.Segments switch
{
    ["/"] => "Root",
    [_, var single] => single,
    [_, .. string[] entries, _] => string.Join(" > ", entries)
};


سایر نوع‌هایی که توسط List patterns قابل بررسی هستند

List patterns تنها با آرایه‌ها و لیست‌ها کار نمی‌کنند. بلکه می‌توان از آن‌ها با هر نوعی که به همراه تعریف indexer‌ها و یا خواص Length و Count است نیز استفاده کرد. اگر نیاز به استفاده از Slice patterns بود، این الگو با نوع‌هایی کار می‌کند که دارای indexer هایی با آرگومان‌هایی از نوع Range است و یا به همراه متد Slice دارای دو آرگومان Int است. برای مثال رشته‌ها نیز در اینجا قابل بررسی هستند.
  • #
    ‫۱ سال و ۹ ماه قبل، شنبه ۵ آذر ۱۴۰۱، ساعت ۱۳:۵۴
    بازنویسی متد ()SingleOrDefault توسط List Patterns Matching

    مثال متداول زیر را در مورد نحوه‌ی استفاده از متد ()SingleOrDefault را به همراه Patterns Matching، در نظر بگیرید:
    List<int> collection = new() { 1 };
    
    var formattedItemBefore = collection.SingleOrDefault() is { } item
        ? $"Formatted: {item}"
        : "No items found";
    این روش به همراه وابسته بودن به نحوه‌ی عملکرد متد ()SingleOrDefault است که اگر بیش از یک آیتم در مجموعه یافت شد، استثنایی را صادر می‌کند. اگر بخواهیم بر روی نحوه‌ی صدور این استثناء و همچنین متن آن کنترل داشته باشیم، اینبار با استفاده از List Patterns Matching می‌توان به صورت زیر عمل کرد:
    var formattedItemAfter = collection switch
    {
        [] => "No items found",
        [var singleItem] => $"Formatted: {singleItem}",
        _ => throw new InvalidOperationException($"Expected 0 or 1 items, but received {collection.Count} items.")
    };
    در اینجا بجای تکیه کردن بر پیاده سازی پیش‌فرض متد ()SingleOrDefault، می‌توان تمام حالات مدنظر را پوشش داد. همچنین امکان سفارشی سازی نوع و پیام استثنای صادره هم میسر شده‌است.
  • #
    ‫۱ سال و ۸ ماه قبل، یکشنبه ۶ آذر ۱۴۰۱، ساعت ۱۴:۲۷
    پشتیبانی از Pattern Match Span در C# 11

    در C# 11 می‌توان یک رشته‌ی ثابت را به صورت <ReadOnlySpan<char تعریف کرد و سپس بر روی آن عملیات patterns matching را انجام داد:
    ReadOnlySpan<char> strSpan = "Vahid";
    if (strSpan is "Vahid")
    {
       Console.WriteLine("Hey, Vahid");
    }

    در اینجا قابلیت‌های List Patterns Matching ذکر شده‌ی در بحث جاری هم قابل استفاده هستند. برای مثال می‌توان بررسی کرد که آیا رشته‌ی ثابت تعریف شده، با حرف خاصی شروع می‌شود (صرفنظر از مابقی حروف آن) و یا خیر:
    if (strSpan is ['V', ..])
    {
       Console.WriteLine("The name starts with V");
    }
  • #
    ‫۱ سال و ۸ ماه قبل، چهارشنبه ۱۶ آذر ۱۴۰۱، ساعت ۱۷:۲۴
    استفاده از List patterns matching در پیاده سازی الگوریتم‌های بازگشتی

    فرض کنید قصد داریم حاصل ضرب و یا حاصل جمع اعداد یک آرایه را به روش بازگشتی محاسبه کنیم:
    var values = Enumerable.Range(start: 1, count: 10).ToArray(); // 1, 2, 3, 4, 5, 6, 7, 8, 9, 10
    
    Console.WriteLine($"MultiplyAll: {MultiplyAll(values)}"); // MultiplyAll: 3628800
    Console.WriteLine($"AddAll: {AddAll(values)}"); // AddAll: 55
    
    static int MultiplyAll(params int[] values) =>
        values switch
        {
            [] => 1,
            [var first, .. var rest] => first * MultiplyAll(rest)
        };
    
    static int AddAll(params int[] elements) =>
        elements switch
        {
            [] => 0,
            [var first, .. var rest] => first + AddAll(rest)
        };
    می‌توان با استفاده از List patterns matching، به سادگی به اولین عنصر (first در اینجا) و سپس لیست مابقی عناصر (rest در اینجا)، جهت تکرار یک عملیات بازگشتی دست یافت.
  • #
    ‫۱ سال و ۸ ماه قبل، سه‌شنبه ۲۲ آذر ۱۴۰۱، ساعت ۱۴:۳۹
    تبدیل کدهایی که از اندیس‌های آرایه‌ها استفاده می‌کنند به List Patterns matching

    فرض کنید قصد داریم با اجزای آرایه‌ی زیر کار کنیم:
    var data = "item1|item2|item3";
    var collection = data.Split('|');
    برای مثال اگر آرایه‌ی collection دارای 2 عضو بود، از طریق collection[0], collection[1] با این دو عضو کار کنیم و یا اگر 3 عضوی بود، به همان صورت بر اساس ایندکس‌ها دسترسی صورت گیرد و اگر این آرایه 2 و یا 3 عضوی نبود، استثنایی را صادر کند. روش متداول انجام اینکار به صورت زیر است:
    var formattedDataBefore = collection.Length switch
    {
        2 => FormatData(collection[0], collection[1]),
        3 => FormatData(collection[0], collection[1], collection[2]),
        var length => throw new InvalidOperationException($"Expected 3 parts, but got {length} parts for formatted string: {data}."),
    };
    می‌توان این قطعه کد را با استفاده از List Patterns Matching به صورت زیر بازنویسی کرد:
    var formattedDataAfter = collection switch
    {
        [var part1, var part2] => FormatData(part1, part2),
        [var part1, var part2, var part3] => FormatData(part1, part2, part3),
        var parts => throw new InvalidOperationException($"Expected 3 parts, but got {parts.Length} parts for formatted string: {data}."),
    };

    نمونه‌ی دیگر این دسترسی‌های بر اساس ایندکس‌ها، مثال زیر است. در اینجا ساختار شیء Song به صورت زیر تعریف شده‌است:
    public class Song
    {
        public string Name { get; set; }
        public List<string> Lyrics { get; set; }
    }
    و فرض کنید songs لیستی از آن است. در این حالت یک روش جستجوی ابتدایی در این لیست، به صورت زیر است که برای مثال اولین Lyrics آن song خاص، مساوی Hello، تعداد Lyrics آن 6 و آخرین عضو Lyrics آن مساوی ? باشد:
    for (var i = 0; i < songs.Count; i++)
    {
        if (songs[i].Lyrics[0] == "Hello" && songs[i].Lyrics.Count == 6 &&
            songs[i].Lyrics[songs[i].Lyrics.Count - 1] == "?")
        {
            Console.WriteLine($"{i}");
        }
    }
    می‌توان این قطعه کد را در C# 11 به صورت زیر بازنویسی کرد که بسیار خواناتر است:
    for (var i = 0; i < songs.Count; i++)
    {
        if (songs[i].Lyrics is ["Hello", _, _, _, _, "?"])
        {
           Console.WriteLine($"{i}");
        }
    }
    و یا اگر از تعداد Lyrics مساوی 6، صرفنظر کنیم و تعداد اعضای آن مهم نباشد، می‌توان به صورت زیر عمل کرد:
    foreach (Song song in songs)
    {
        if (song.Lyrics is ["Hello", .., "?"])
        {
            Console.WriteLine(song.Name);
        }
    }

    به علاوه اگر در جستجویی دیگر، نیاز به کار با اعضای آرایه‌ی 5 عضوی یافت شده وجود داشت، می‌توان بدون نیاز به مراجعه‌ی متداول به ایندکس‌های آرایه، به صورت زیر عمل کرد:
    foreach (Song song in songs)
    {
      if (song.Lyrics is ["Hello", "from" or "is", var third, var forth, var fifth])
        {
          Console.WriteLine(song.Name);
          Console.WriteLine($"The third word is : {third}");
          Console.WriteLine($"The forth word is : {forth}");
          Console.WriteLine($"The fifth word is : {fifth}");
        }
    }
  • #
    ‫۱ سال و ۷ ماه قبل، پنجشنبه ۱۵ دی ۱۴۰۱، ساعت ۱۴:۲۷
    یک نکته‌ی تکمیلی: ایجاد نوع‌های سازگار با List Patterns Matching

    در انتهای این مطلب در مورد «سایر نوع‌هایی که توسط List patterns قابل بررسی هستند» توضیحات مختصری عنوان شد. کامپایلر #C در جهت یافتن نوع‌های سازگار با List Patterns Matching، به دنبال اینترفیس خاصی نمی‌گردد؛ بلکه به دنبال وجود یک سری اعضای خاص، در کلاس مدنظر است و این اعضاء به شرح زیر هستند:
    الف) نوع مدنظر باید به همراه یکی از خواص Length و یا Count باشد تا تعداد اعضای مجموعه را مشخص کند. اگر هر دو خاصیت با هم حضور داشته باشند، کامپایلر خاصیت Length را انتخاب می‌کند:
    public int Length { get; }
    public int Count { get; }

    ب) نوع مجموعه‌ای باید به همراه یک ایندکسر باشد که نوع خروجی آن مهم نیست. اگر در نوع تعریف شده، هر دو امضای زیر وجود داشته باشند، کامپایلر از نمونه‌ی this[Index index] استفاده می‌کند:
    public object this[int index] => throw null;
    public object this[System.Index index] => throw null;

    ج) نوع مجموعه‌ای باید از slice pattern، توسط یکی از امضاهای زیر که نوع خروجی آن مهم نیست، پشتیبانی کند. اگر هر دو با هم حضور داشته باشند، کامپایلر از this[System.Range index] استفاده می‌کند:
    public object this[System.Range index] => throw null;
    public object Slice(int start, int length) => throw null;

    برای مثال با توجه به نکات فوق، نوع جدید زیر، با List Patterns Matching سازگاری دارد:
    public class MyListPatternsCompatibleCollection
    {
        private readonly List<int> _items = new();
    
        public int Length => _items.Count;
    
        public int this[Index index] => _items[index];
    
        public ReadOnlySpan<int> this[Range range]
            => CollectionsMarshal.AsSpan(_items)[range];
    
        public void Add(int item) => _items.Add(item);
    }
    و نمونه‌ای از نحوه‌ی استفاده‌ی از آن به صورت زیر است:
     var collection = new MyListPatternsCompatibleCollection();
    collection.Add(1);
    collection.Add(2);
    collection.Add(3);
    
    _ = collection is [var head, .. var tail];