C# 12.0 - Collection Expressions & Spread Operator
اندازه‌ی قلم متن
تخمین مدت زمان مطالعه‌ی مطلب: چهار دقیقه

C# 12 به همراه روش جدیدی برای آغاز مجموعه‌ها است که با آرایه‌ها، Spanها و هر نوعی که آغازگرهای مجموعه‌ها را بپذیرد، کار می‌کند. همچنین اپراتور جدیدی را هم به نام spread operator به صورت .. به زبان #C اضافه کرده‌است که امکان ساده‌تر ترکیب مجموعه‌ها را میسر می‌کند.


آغاز ساده‌تر مجموعه‌ها با کمک Collection Expressions

تا پیش از C# 12 برای آغاز یک آرایه می‌توان از روش زیر استفاده کرد که در آن نوع آرایه از طریق نوع اعضای آن حدس زده می‌شود:
var numbers1_CS11 = new[] { 1, 2, 3 };
که در حقیقت ساده شده‌ی تعریف اصلی زیر است:
var numbers1_CS_11 = new int[] { 1, 2, 3 };
در C# 12، می‌توان این تعاریف را به کمک collection expressions، خلاصه‌تر هم کرد:
int[] numbers1_CS12 = [ 1, 2, 3 ];
که در اینجا، {}‌ها به [] تبدیل شده‌اند و ذکر نوع آرایه، ضروری است (یعنی نمی‌توان از var جهت تعریف آن‌ها استفاده کرد)؛ در غیراینصورت با خطای زیر متوقف می‌شویم:
error CS9176: There is no target type for the collection expression.

یک collection expression و یا collection literals، به مجموعه‌ای از عناصر گفته می‌شود که بین دو براکت [] قرار می‌گیرند.

نمونه‌ی دیگر آن کار با Spanها است که نمونه کد C# 11 آن:
Span<string> span1_CS11 = new string[] { "AC", "AL" };
در C# 12 به صورت زیر خلاصه می‌شود:
Span<string> span1_CS12 = [ "AC", "AL" ];
و در اینجا امکان کار با ReadOnlySpan‌ها هم وجود دارد:
ReadOnlySpan<string> readOnlySpan_CS12 = [ "Africa",  "Asia", "Europa"];

مثال دیگر، نحوه‌ی آغاز آرایه‌های چندبعدی است:
int[][] array2D_CS11 =
  {
    new int[] { 2002, 2006, 2010},
    new int[] { 2014, 2018},
    new int[] { 2022, 2026, 2030}
  };
که در C# 12 به صورت خلاصه‌ی زیر قابل بیان است:
int[][] array2D_CS12 =
  [
     [2002, 2006, 2010],
     [2014, 2018],
     [2022, 2026, 2030]
  ];

و یا حتی این مورد را در مورد نحوه‌ی آغاز Listهای پیش از C#12
List<string> list_CS11 = new List<string> { "Item 1", "Item 2" };
نیز می‌توان بکار گرفت:
List<string> list_CS12 = [ "Item 1", "Item 2" ];

در کل همانطور که مشاهده می‌کنید، این تغییر، تغییر مثبتی است و حجم قابل ملاحظه‌ای از کدها را کاهش داده و خواندن آن‌ها را نیز ساده‌تر می‌کند.

یک نکته: روش ساده شده‌ی آغاز یک لیست با مجموعه‌ای خالی در C# 12 به صورت زیر است:
// Before C#12
List<User> users = new List<User>();
// or
var users = new List<User>();
// or
List<User> user = new();

// C#12
List<User> users = [];


اضافه شدن spread operator به زبان #C

اگر پیشتر با زبان JavaScript کار کرده باشید، با spread operator هم آشنایی دارید. کار آن ساده سازی یکی کردن مجموعه‌ها و یا افزودن ساده‌تر عناصری به آن‌ها است و .. بالاخره به زبان #C هم راه پیدا کرده‌است! برای مثال دو آرایه‌ی زیر را درنظر بگیرید:
int[] numbers1_CS12 = [ 1, 2, 3 ];
int[] numbers2_CS12 = [ 4, 5, 6 ];
در C# 12 برای یکی کردن آن‌ها می‌توان از spread operator به صورت زیر استفاده کرد:
int[] allItems = [ ..numbers1_CS12, ..numbers2_CS12 ];
Spread به معنای «پخش کردن»/«گسترده کردن»/«باز کردن» هست. برای مثال در اینجا، اعضای دو آرایه را داخل یک آرایه‌ی جدید، پخش کرده‌ایم!

اگر در نگارش‌های قبلی #C بخواهیم چنین کاری را انجام دهیم، یک روش آن به صورت زیر است:
int[] allItems_CS11 = numbers1_CS12.Concat(numbers2_CS12).ToArray();
که ... نگارش C# 12 آن کارآیی بیشتری دارد؛ چون تعداد بار اختصاص حافظه‌ی آن کمتر است. در C# 12، هنگام استفاده از spread operator، کار کپی کردن اطلاعات صورت نمی‌گیرد و همچنین طول نهایی مجموعه‌ی حاصل دقیقا مشخص می‌شود که این مورد از چندین بار تخصیص حافظه برای چسباندن آرایه‌های مختلف به هم جلوگیری می‌کند.

همچنین اپراتور پخش کردن، قابلیت قرارگرفتن در کنار سایر اعضای یک آرایه را هم به سادگی و با خوانایی بیشتری به همراه دارد:
int[] join = [..a, ..b, ..c, 6, 5];

به علاوه محدودیتی در مورد نوع مجموعه‌ی بکار گرفته شده نیز در اینجا وجود ندارد. برای نمونه در مثال زیر، یک آرایه، یک Span و یک لیست، با هم یکی شده‌اند:
int[] a =[1, 2, 3];
Span<int> b = [2, 4, 5, 4, 4];
List<int> c = [4, 6, 6, 5];

List<int> join = [..a, ..b, ..c, 6, 5];

و مثالی دیگر، نحوه‌ی ساده‌ی تعریف لیستی از tuples است:
List<(string, int)> otherScores = [("Dave", 90), ("Bob", 80)];
و سپس باز کردن آن داخل آرایه‌ای از tuples:
(string name, int score)[] scores = [("Alice", 90), ..otherScores, ("Charlie", 70)];
  • #
    ‫۹ ماه قبل، یکشنبه ۵ آذر ۱۴۰۲، ساعت ۲۱:۴۶
    نکته تکمیلی: Collection literalها اصطلاحاً target-typed هستند مثلاً در شرایطی که نیاز باشد از Ternary Operator درون [] استفاده شود نیازی به Cast کردن آن نیست و خود کامپایلر آن را تشخیص خواهد داد:

  • #
    ‫۹ ماه قبل، شنبه ۱۸ آذر ۱۴۰۲، ساعت ۱۲:۱۲
    اصلاحیه: کارآیی spread operator بیشتر نیست!

    در متن در مورد spread operator عنوان شده «که ... نگارش C# 12 آن کارآیی بیشتری دارد». این مورد بدون توجه به #Low-Level C تولیدی، نوشته شد و ... متاسفانه نادرست است!
    برای مثال فرض کنید، چنین متدی را دارید که با استفاده از spread operator، کار بازکردن یک آرایه را انجام می‌دهد:
    public int[] WithSpread()
    {
       int[] data = new int[10_000];
       int[] results = [..data];
       return results;
    }
    معادل #Low-Level C آن (کد نهایی که کامپایلر برای تبدیل آن به IL تولید می‌کند) به صورت زیر است ( #Low-Level C را در Rider، در منوی #Tools -> IL Viewer -> Select Low-Level C می‌توانید تولید کنید):
    public int[] WithSpread()
    {
      int[] numArray1 = new int[10000];
      int index1 = 0;
      int[] numArray2 = new int[numArray1.Length];
      int[] numArray3 = numArray1;
      for (int index2 = 0; index2 < numArray3.Length; ++index2)
      {
        int num = numArray3[index2];
        numArray2[index1] = num;
        ++index1;
      }
      return numArray2;
    }
    همانطور که مشاهده می‌کنید، این قطعه کد در C#12 و دات‌نت 8، به شدت ابتدایی تولید شده و به همراه هیچ نوع بهینه سازی نیست. کارآیی این قطعه کد، نسبت به زمانیکه از متد قدیمی CopyTo آرایه‌ها استفاده می‌شود، به مراتب کمتر است (تا 3 برابر!)؛ چون متد CopyTo به همراه بهینه سازی‌های سخت‌افزاری هم هست. به نظر قرار شده بهینه سازی کارآیی spread operator در نگارش بعدی دات‌نت انجام شود.

    برای آزمایش شخصی آن، از کلاس زیر استفاده کنید:
    using BenchmarkDotNet.Attributes;
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    using System.Threading.Tasks;
    
    namespace SpreadBenchmark
    {
        [MemoryDiagnoser]
        public class Tests
        {
            private readonly int[] _myData = new int[10_000];
    
            [Benchmark(Baseline = true)]
            public int[] WithToArray()
            {
                int[] results = _myData.ToArray();
                return results;
            }
    
            [Benchmark]
            public int[] WithCopyTo()
            {
                int[] results = new int[_myData.Length];
                _myData.CopyTo(results, 0);
                return results;
            }
    
            [Benchmark]
            public int[] WithSpread()
            {
                int[] results = [.._myData];
                return results;
            }
        }
    }
    که در فایل Program.cs به این صورت فراخوانی می‌شود:
    using BenchmarkDotNet.Running;
    using SpreadBenchmark;
    
    BenchmarkRunner.Run<Tests>();
    با این وابستگی:
    <Project Sdk="Microsoft.NET.Sdk">
      <PropertyGroup>
        <OutputType>Exe</OutputType>
        <TargetFramework>net8.0</TargetFramework>
        <ImplicitUsings>enable</ImplicitUsings>
        <Nullable>enable</Nullable>
      </PropertyGroup>
    
      <ItemGroup>
        <PackageReference Include="BenchmarkDotNet" Version="0.13.10" />
      </ItemGroup>
    </Project>