عنوان:

‫کش کردن مقادیر Enum در C# 14 با استفاده از Extension Everything – راه‌حل بهینه و thread-safe


نویسنده: وحید نصیری
تاریخ: ۱۴۰۴/۱۱/۰۹ ۱۰:۳۵
آدرس: www.dntips.ir
یکی از عملیات پرکاربرد هنگام کار با انواع enum در سی‌شارپ، گرفتن لیست تمام مقادیر آن با متد Enum.GetValues است. این متد اگرچه بسیار راحت است، اما در مسیرهای داغ (hot path) برنامه یا داخل حلقه‌ها می‌تواند به دلیل استفاده از reflection داخلی، تخصیص حافظه تکراری ایجاد کند و عملکرد را تحت تأثیر قرار دهد.
از نسخه C# 14، با ویژگی Extension Everything می‌توان متدهای استاتیک extension را به شکل بسیار تمیزتری برای انواع enum نوشت. سؤال مهمی که خیلی از توسعه‌دهندگان مطرح می‌کنند این است:
آیا می‌توان نتیجهٔ Enum.GetValues را برای هر نوع enum به‌صورت جداگانه کش (cache) کرد؟
و آیا می‌توان این کار را با یک فیلد خصوصی ساده داخل بدنهٔ extension انجام داد؟
پاسخ کوتاه: بله، کاملاً می‌توان کش کرد، اما نه با یک فیلد static ساده داخل کلاس extension.

مشکل فیلد static معمولی
فرض کنید چنین کدی بنویسیم:
public static class EnumExtensions
{
    extension<T>(T _) where T : struct, Enum
    {
        private static T[] _cache;

        public static T[] GetValues()
        {
            return _cache ??= Enum.GetValues<T>();
        }
    }
}
این کد اشتباه است! دلیلش بسیار مهم و ظریف است:
فیلد static داخل کلاس معمولی (حتی اگر داخل extension نوشته شود)، برای تمام انواع T مشترک است. یعنی اگر اول MyEnum1 فراخوانی شود، _cache با مقادیر MyEnum1 پر می‌شود. سپس وقتی MyEnum2 را صدا بزنیم، همان آرایهٔ اشتباه قبلی را برمی‌گرداند!

راه‌حل استاندارد و بهینه: Generic Static Holder
بهترین و رایج‌ترین الگوی کش کردن مقادیر وابسته به نوع generic در سی‌شارپ، استفاده از کلاس static holder داخلی است:
public static class EnumExtensions
{
    extension<T>(T _) where T : struct, Enum
    {
        /// <summary>
        /// برمی‌گرداند آرایه‌ای از تمام مقادیر تعریف‌شده در این enum
        /// (کش‌شده، thread-safe و بدون تخصیص اضافی پس از بار اول)
        /// </summary>
        public static T[] GetValues() => Cache<T>.Values;
    }


    // کلاس کمکی – برای هر T یک نمونهٔ جداگانه از فیلدهای static دارد
    private static class Cache<T> where T : struct, Enum
    {
        internal static readonly T[] Values = Enum.GetValues<T>();
    }
}

چرا این الگو بهترین انتخاب است؟
ویژگیتوضیح
Lazy InitializationCLR خودش تضمین می‌کند که فیلد static readonly فقط یک‌بار مقداردهی شود
Thread-Safeبدون نیاز به lock یا Interlocked – کاملاً ایمن در محیط چندرشته‌ای
Zero allocation بعد از بار اولپس از اولین دسترسی، هیچ تخصیص heap جدیدی رخ نمی‌دهد
بدون Dictionaryنیازی به نگهداری کش در Dictionary نیست؛ سریع‌تر و سبک‌تر
تمیز و خواناکد کاملاً واضح و قابل نگهداری است

نسخهٔ امن‌تر و immutable (اختیاری)
اگر می‌خواهید مصرف‌کننده نتواند به‌طور تصادفی آرایهٔ داخلی را تغییر دهد، می‌توانید از یکی از این دو شکل استفاده کنید:
// گزینه ۱ – ساده و سبک
internal static readonly IReadOnlyList<T> Values 
    = Array.AsReadOnly(Enum.GetValues<T>());

// گزینه ۲ – کاملاً immutable (نیاز به System.Collections.Immutable)
internal static readonly ImmutableArray<T> Values 
    = Enum.GetValues<T>().ToImmutableArray();

نکتهٔ مهم دربارهٔ C# 14
ویژگی extension صرفاً یک شیرین‌سازی نحوی (syntactic sugar) است.
از نظر runtime و تولید IL، دقیقاً مثل نوشتن یک متد استاتیک generic معمولی رفتار می‌کند.
بنابراین تمام قوانین و محدودیت‌های قبلی در مورد generic static fields همچنان پابرجاست و باید از الگوی holder استفاده کنیم.

نتیجه‌گیری
با کمک Extension Everything در C# 14 می‌توانیم متدهای بسیار تمیز و طبیعی برای انواع enum بنویسیم، اما برای کش کردن مقادیر وابسته به نوع، همچنان باید به الگوی کلاسیک generic static holder تکیه کنیم. این الگو ترکیبی عالی از عملکرد، ایمنی رشته‌ای و خوانایی کد ارائه می‌دهد و در پروژه‌های بزرگ و حساس به عملکرد، بارها خودش را ثابت کرده است.
// استفاده در کد شما این‌قدر ساده و زیبا خواهد بود:
MyStatus.GetValues();           // → T[]
UserRole.GetValues();           // → آرایهٔ جدا برای این enum