عنوان:

‫معرفی Collection Expression Arguments در C# 15.0


نویسنده: وحید نصیری
تاریخ: ۱۴۰۴/۱۱/۲۴ ۱۰:۵۰
آدرس: www.dntips.ir
هر نسخه‌ی جدید از سی‌شارپ، با وعده‌ی سادگی و کارایی بیشتر همراه است. یکی از جذاب‌ترین و البته بحث‌برانگیزترین ویژگی‌هایی که در پیش‌نمایش‌های C# 15 و .NET 11 معرفی شده، امکان ارسال آرگومان به «عبارات مجموعه‌ای» یا همان Collection Expression Arguments است. در این مقاله، به بررسی دقیق این ویژگی، نحوه عملکرد آن با سینتکس جدید with و بازخوردهای جامعه برنامه‌نویسان می‌پردازیم.

مقدمه: یادآوری Collection Expressions
پیش از بررسی ویژگی جدید، بد نیست نگاهی به «عبارات مجموعه‌ای» (Collection Expressions) داشته باشیم که در نسخه‌های اخیر معرفی شدند. این ویژگی به ما اجازه می‌دهد با استفاده از کروشه [] مجموعه‌ها را به شکلی بسیار خوانا و مختصر تعریف کنیم:
List<int> evenNumbers = [2, 4, 6, 8, 10];
List<int> oddNumbers = [1, 3, 5, 7, 9];

// ترکیب دو لیست و افزودن یک مقدار در ابتدا (Spread Operator)
List<int> allNumbers = [0, ..evenNumbers, ..oddNumbers];
مزیت این روش فراتر از زیبایی بصری است؛ کامپایلر در پشت صحنه می‌تواند کدهای بهینه‌تری تولید کند. اما یک نقص وجود داشت: اگر می‌خواستید ظرفیت (Capacity) لیست را از ابتدا تعیین کنید یا یک مقایسه‌گر (Comparer) خاص به HashSet بدهید، مجبور بودید به روش سنتی new List برگردید.

ورود Collection Expression Arguments
در C# 15، پیشنهادی که پیش‌تر با نام مستعار args() شناخته می‌شد، نهایتاً با کلمه کلیدی آشنای with به واقعیت پیوست. این ویژگی اجازه می‌دهد پارامترهای سازنده (Constructor) را مستقیماً درون کروشه ارسال کنید.

سینتکس نهایی در C# 15
طبق آخرین مستندات، برای تعیین ظرفیت اولیه لیست، سینتکس به شکل زیر خواهد بود:
// تعیین ظرفیت اولیه لیست برای جلوگیری از تخصیص مجدد حافظه (Resizing)
List<int> allNumbers = [with(capacity: 32), 0, ..evenNumbers, ..oddNumbers];
این کد توسط کامپایلر به چیزی شبیه به new List ترجمه می‌شود. همچنین این قابلیت برای مجموعه‌های پیچیده‌تر مانند HashSet که نیاز به تنظیمات خاص دارند، بسیار کاربردی است:
// تعریف یک HashSet با نادیده گرفتن کوچک و بزرگی حروف
HashSet<string> names = [with(comparer: StringComparer.OrdinalIgnoreCase), "Alice", "Bob"];

تحلیل فنی
چرا مایکروسافت به دنبال این تغییر است؟ پاسخ در دو کلمه خلاصه می‌شود: یکپارچگی و عملکرد.
  • بهینه‌سازی خودکار: وقتی شما از with(capacity: ...) استفاده می‌کنید، کامپایلر دقیقاً می‌داند که چه مقدار حافظه رزرو کند. در حالت‌های ترکیبی (مانند استفاده از .. همراه با مقادیر ثابت)، کامپایلر می‌تواند محاسبات پیچیده‌ای انجام دهد تا دقیق‌ترین ظرفیت را از همان ابتدا به سازنده پاس دهد.
  • پشتیبانی از انواع سفارشی: این ویژگی فقط برای List یا Dictionary نیست. هر نوع داده‌ای که الگوی Collection Expression را پیاده‌سازی کرده باشد، می‌تواند از طریق متد Create آرگومان‌های ارسالی را دریافت و مدیریت کند.

چالش‌ها و بازخوردهای جامعه برنامه‌نویسان
با وجود مزایای فنی، این ویژگی با موجی از انتقادات در انجمن‌هایی مانند Reddit و GitHub مواجه شده است. برخی از توسعه‌دهندگان معتقدند سی‌شارپ در حال تبدیل شدن به یک زبان بیش از حد پیچیده (Over-engineered) است.
نکات اصلی منتقدان:
  • خوانایی: بسیاری معتقدند عبارت [with(capacity: 10), 1, 2] خواناتر از new List نیست.
  • پیچیدگی غیرضروری: برخی برنامه‌نویسان نگرانند که اضافه شدن راه‌های متعدد برای انجام یک کار مشابه، یادگیری زبان را برای تازه‌کارها دشوار و کد بیس‌های قدیمی را شلوغ کند.
  • فشار آنالایزرها: انتقاد تندی به آنالایزرهای جدید وارد شده که توسعه‌دهنده را مجبور می‌کنند کدهای ساده‌ای مثل .ToList() را به Collection Expressions تبدیل کنند، حتی وقتی که نیازی به آن نیست.


بررسی فنی پشت صحنهwith
برای درک بهتر این موضوع، باید بدانیم که کامپایلر سی‌شارپ چگونه این عبارت‌های جدید را به کدهای قابل فهم برای دات‌نت تبدیل می‌کند. در واقع، جادوی اصلی در متدی به نام Create نهفته است. در ادامه، نحوه پیاده‌سازی این قابلیت را برای یک کلاس سفارشی (Custom Collection) بررسی می‌کنیم.
زمانی که شما از [with(capacity: 10), 1, 2, 3] استفاده می‌کنید، کامپایلر به دنبال متدی می‌گردد که بتواند این آرگومان‌ها را بپذیرد. این کار معمولاً از طریق ویژگی [CollectionBuilder] انجام می‌شود.

گام اول: تعریف کالکشن سفارشی
فرض کنید یک لیست مخصوص به نام MyCoolList داریم:
using System.Runtime.CompilerServices;

[CollectionBuilder(typeof(MyCoolListBuilder), nameof(MyCoolListBuilder.Create))]
public class MyCoolList<T> : IEnumerable<T>
{
    private readonly T[] _items;
    public MyCoolList(ReadOnlySpan<T> items) => _items = items.ToArray();

    // متدهای IEnumerable در اینجا پیاده‌سازی می‌شوند...
    public IEnumerator<T> GetEnumerator() => ((IEnumerable<T>)_items).GetEnumerator();
    System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() => GetEnumerator();
}

گام دوم: تعریف Builder با قابلیت پذیرش آرگومان
اینجاست که ویژگی جدید C# 15 وارد عمل می‌شود. متد Create اکنون می‌تواند پارامترهای اضافی (مانند capacity) را دریافت کند:
public static class MyCoolListBuilder
{
    // این متد توسط کامپایلر فراخوانی می‌شود
    public static MyCoolList<T> Create<T>(ReadOnlySpan<T> items, int capacity = 0)
    {
        Console.WriteLine($"ایجاد لیست با ظرفیت: {capacity}");
        // در اینجا می‌توانید از ظرفیت برای بهینه‌سازی حافظه استفاده کنید
        return new MyCoolList<T>(items);
    }
}

گام سوم: استفاده از سینتکس جدید
حالا برنامه‌نویس می‌تواند به راحتی از پارامتر capacity در عبارات مجموعه‌ای استفاده کند:
// فراخوانی متد Create و پاس دادن مقدار 50 به پارامتر capacity
MyCoolList<int> myNumbers = [with(capacity: 50), 10, 20, 30];

چرا این روش از سازنده‌های قدیمی (Constructors) بهتر است؟
در روش‌های قدیمی، ما ابتدا شیء را می‌ساختیم و سپس آیتم‌ها را به آن اضافه می‌کردیم (Add). اما در مکانیسم Collection Expressions:
  • ساخت یکباره: کامپایلر ابتدا تمام آیتم‌های داخل کروشه را شناسایی می‌کند.
  • تخصیص دقیق حافظه: اگر از with(capacity: ...) استفاده کنید، کامپایلر آن را مستقیماً به متد سازنده می‌فرستد.
  • بهینه‌سازی Span: برخلاف List.Add که در هر مرحله ممکن است باعث جابجایی حافظه شود، در اینجا داده‌ها می‌توانند به صورت یک ReadOnlySpan بسیار سریع و یکباره به مجموعه منتقل شوند.


بررسی تفاوت عملکردی (Performance)
باید به این نکته توجه کنیم که در روش‌های قدیمی، مدیریت حافظه لزوماً بهینه نبود. بیایید این دو رویکرد را از نظر تخصیص حافظه (Memory Allocation) و سرعت اجرا با هم مقایسه کنیم.
۱. مقایسه مکانیزم درونی
وقتی از روش سنتی استفاده می‌کنید، فرآیند به این صورت است:
  • ایجاد یک لیست خالی با ظرفیت پیش‌فرض (معمولاً ۴ یا ۸).
  • افزودن تک‌تک آیتم‌ها با متد .Add().
  • اگر تعداد آیتم‌ها از ظرفیت بیشتر شود، لیست باید ظرفیت خود را دو برابر کند که به معنای کپی کردن کل آرایه در یک فضای جدید از حافظه است.
اما در Collection Expressions (به‌ویژه با آرگومان capacity):
  • کامپایلر تعداد آیتم‌های ثابت (مثل 1, 2, 3) را می‌شمارد.
  • اگر از spread operator (مثل ..others) استفاده کرده باشید، طول آن را محاسبه می‌کند.
  • با استفاده از آرگومان with(capacity: ...)، دقیقاً همان مقدار فضا را یک‌بار رزرو می‌کند.
  • داده‌ها را به صورت بلاک‌های پیوسته (با استفاده از ReadOnlySpan) در حافظه می‌نویسد.

۲. بنچمارک فرضی (Benchmark)
در یک سناریوی شبیه‌سازی شده با ابزار BenchmarkDotNet، نتایج معمولاً به شرح زیر است:
روش پیاده‌سازیزمان اجرا (میانگین)تخصیص حافظه (Allocated)تعداد دفعات GC
new List۱۲۰ نانوثانیه۴۵۰ بایت۲
[with(capacity: 100), ..data]۸۵ نانوثانیه۴۰۰ بایت۰
نکته کلیدی: در روش جدید، چون ظرفیت از ابتدا مشخص است، Garbage Collector (GC) کمتر درگیر می‌شود و "فشار حافظه" به شدت کاهش می‌یابد.

۳. استفاده در Dictionary
یکی از بهترین کاربردهای این ویژگی که کمتر به آن اشاره شده، در Dictionary است. ایجاد دیکشنری با ظرفیت مشخص، از Collision (تداخل هش‌ها) جلوگیری می‌کند و سرعت جستجو (Look-up) را در ابتدای کار بالا می‌برد:
// بهینه‌سازی دیکشنری برای ۱۰۰۰ آیتم از همان لحظه تولد!
Dictionary<int, string> lookup = [with(capacity: 1000), {1, "Active"}, {2, "Pending"}];


بهترین شیوه‌ها (Best Practices) در استفاده از C# 15
۱. استفاده هوشمندانه ازcapacity
همیشه از خود بپرسید: «آیا من تعداد تقریبی عناصر را می‌دانم؟»
  • بله: اگر داده‌ها را از یک منبع با طول مشخص (مثل یک آرایه دیگر یا دیتابیس) می‌خوانید، حتماً از with(capacity: ...) استفاده کنید. این کار از "تخصیص مجدد" (Re-allocation) حافظه جلوگیری می‌کند.
  • خیر: اگر تعداد عناصر کاملاً نامشخص است، اجازه دهید خودِ دات‌نت مدیریت ظرفیت را بر عهده بگیرد. مقداردهی اشتباه به ظرفیت (مثلاً رزرو ۱۰۰۰ واحد برای ۲ عنصر) باعث هدررفت حافظه می‌شود.

۲. اولویت‌بندی در مجموعه‌های کلید-مقدار (Dictionaries)
در دیکشنری‌ها، ارسال IEqualityComparer از طریق with(comparer: ...) بسیار تمیزتر از روش قدیمی است. این کار به ویژه زمانی که می‌خواهید مقایسه‌های رشته‌ای را به صورت OrdinalIgnoreCase انجام دهید، خوانایی کد را به شدت بالا می‌برد.
// روش مدرن و خوانا در C# 15
Dictionary<string, int> counts = [with(comparer: StringComparer.OrdinalIgnoreCase), ["Key", 1]];

۳. اجتناب از پیچیدگی بیش از حد (Over-engineering)
اگر مجموعه‌ای دارید که فقط ۲ یا ۳ عضو ثابت دارد، استفاده از with(capacity: 3) تاثیری در عملکرد ندارد و فقط کد را شلوغ می‌کند. کامپایلر سی‌شارپ به اندازه کافی هوشمند است که برای تعداد کمی از آیتم‌های ثابت، بهینه‌ترین کد را تولید کند.

۴. مراقب آنالایزرهای سخت‌گیر باشید
در نسخه جدید دات‌نت، آنالایزرهای IDE ممکن است به شما پیشنهاد دهند که تمام کدهای قدیمی را به Collection Expressions تبدیل کنید.
  • توصیه: اگر کد فعلی شما (مثلاً .ToList()) کاملاً خواناست و در یک بخش حساس از نظر پرفورمنس نیست، نیازی به تغییر اجباری به with ندارید. اجازه ندهید ابزارهای تحلیل کد، خوانایی منطق برنامه شما را فدای سینتکس‌های جدید کنند.

جمع‌بندی نهایی مقاله
ویژگی Collection Expression Arguments در C# 15، قطعه گم‌شده‌ای بود که پازل عبارات مجموعه‌ای را کامل کرد. اکنون ما نه تنها سینتکس کوتاه‌تری داریم، بلکه قدرت کنترل دقیق روی ساختار داخلی اشیاء (مثل ظرفیت و مقایسه‌گرها) را نیز در اختیار داریم. این ویژگی نشان می‌دهد که سی‌شارپ به سمت زبانی حرکت می‌کند که در آن «سادگی در نوشتن» با «بهینگی در اجرا» تداخلی ندارد. با وجود انتقادات اولیه، این قابلیت به زودی به استاندارد جدیدی در پروژه‌های سطح بالا تبدیل خواهد شد.