عنوان:

C# 6 - Index Initializers


نویسنده: وحید محمدطاهری
تاریخ: ۱۳۹۴/۰۷/۲۱ ۱۰:۳۰
آدرس: www.dntips.ir
زمان زیادی از ارائه‌ی امکان Collection Initializer برای ایجاد یک متغیر از نوع Collection می‌گذرد؛ برای نمونه به مثال زیر توجه کنید:
enum USState {...}
var AreaCodeUSState = new Dictionary<string, USState>
    {
        {"408", USState.California},
        {"701", USState.NorthDakota},
        ...
    };
در پشت صحنه، کامپایلر، Collection Initializer را می‌گیرد، با استفاده از یک <Dictionary<TKey, TValue و با فراخوانی متد Add آن بر روی لیست Collection Initializer شروع به درج آن در دیکشنری ساخته شده می‌کند. Collection Initializer فقط بر روی کلاس هایی که در آن‌ها IEnumerable پیاده سازی شده باشد امکان پذیر است چرا که کامپایلر کار اضافه کردن مقادیر اولیه را به ()IEnumerable.Add می‌سپارد.

اکنون در C# 6.0 ما می‌توانیم از Index Initializer استفاده کنیم:
enum USState {...}
var AreaCodeUSState = new Dictionary<string, USState>
    {
        ["408"] = USState.California,
        ["701"] = USState.NorthDakota,
        ...
    };
اولین تفاوتی که این دو روش با هم دارند این است که در حالت استفاده‌ی از Index Initializer پس از کامپایل، ()IEnumerable.Add فراخوانی نمی‌شود. این تفاوت بسیار مهم است و کار اضافه کردن مقادیر اولیه را با استفاده از کلید (Key) ویژه انجام می‌دهد.
شبه کد مثال بالا به صورت زیر می‌شود:

Collection Initializer
create a Dictionary<string, USState>
add to new Dictionary the following items: 
     "408", USState.California
     "701", USState.NorthDakota
Index Initializer
create a Dictionary<string, USState> then
using AreaCodeUSState's default Indexed property
    set the Value of Key "408" to USState.California
    set the Value of Key "701" to USState.NorthDakota
حال به مثال زیر توجه کنید:

Collection Initializer 
enum USState {...}
var AreaCodeUSState = new Dictionary<string, USState>
        {
            { "408", USState.Confusion},
            { "701", USState.NorthDakota },
            { "408", USState.California},
            ...
        };
Console.WriteLine( AreaCodeUSState.Where(x => x.Key == "408").FirstOrDefault().Value );
Index Initializer
enum USState {...}
var AreaCodeUSState = new Dictionary<string, USState>
    {
        ["408"] = USState.Confusion,
        ["701"] = USState.NorthDakota,
        ["408"] = USState.California,
        ...
    };
Console.WriteLine( AreaCodeUSState2.Where(x => x.Key == "408").FirstOrDefault().Value );  // output = California
هر دو کد بالا با موفقیت کامپایل و اجرا می‌شود، اما در زمان اجرای Collection Initializer هنگامیکه می‌خواهد مقدار دوم "408" را اضافه کند با استثناء ArgumentException متوقف می‌شود چرا که کلید "408" از قبل وجود دارد.
اما در زمان اجرا، Index Initializer به صورت کامل و بدون خطا این کار را انجام می‌دهد و در کلید "408" مقدار USState.Confusion قرار می‌گیرد. سپس "701" مقدار USState.NorthDakota و بعد از استفاده‌ی مجدد از کلید "408" مقدار USState.California جایگزین مقدار قبلی می‌شود.

var fibonaccis = new List<int>
    {
        [0] = 1,
        [1] = 2,
        [3] = 5,
        [5] = 13
    }
این کد هم معتبر است و هم کامپایل می‌شود. البته معتبر است، ولی صحیح نیست. <List<T اجازه‌ی تخصیص اندیسی فراتر از اندازه‌ی فعلی را نمی‌دهد.
تلاش برای تخصیص مقدار 1 با کلید 0 به <List<int، سبب بروز استثناء ArguementOutOfRangeException می شود. وقتی (List<T>.Add(item فراخوانی می‌شود اندازه‌ی لیست یک واحد افزایش می‌یابد. بنابراین باید دقت داشت که Index Initializer از ()Add. استفاده نمی‌کند؛ در عوض با استفاده از خصوصیت اندیس پیش فرض، مقداری را برای یک کلید تعیین می‌کند.
برای چنین حالتی بهتر است از همان روش قدیمی Collection Initializer استفاده کنیم:
var fibonaccis = new List<int>()
    {
        1,
        3,
        5,
        13
    };

نظرات

  • وحید نصیری در ۱۴۰۳/۰۹/۰۷ ۲۲:۲۲
    اگر بخواهیم مطلب جاری را با مطلب «C# 8.0 - Ranges & Indices» ترکیب کنیم به ویژگی جدید زیر می‌رسیم:

    یک نکته‌ی تکمیلی: ویژگی implicit index access در مجموعه‌های ایندکس پذیر در C# 13

    در #C می‌توان آرایه‌ها، لیست‌ها و تمام مجموعه‌های ایندکس پذیر را به کمک ایندکس‌ها، از انتها به ابتدا مقدار دهی و آغاز کرد. پیشتر برای انجام چنین کاری نیاز بود تا بر اساس اندازه‌ی این مجموعه‌ها، محاسبات مرتبط را دستی انجام داد. در C# 13 با استفاده از hat operator، دیگر نیازی به اینکار و انجام این محاسبات نیست.
    برای مثال فرض کنید که می‌خواهیم یک آرایه را از انتها به ابتدا مقدار دهی کنیم. روش انجام اینکار در C# 13 به صورت زیر است:
    var countdown = new int[10]; // Initialize the array first
    
    countdown[^1] = 0;  //last value
    countdown[^2] = 1; //second last value
    countdown[^3] = 2;
    countdown[^4] = 3;
    countdown[^5] = 4;
    countdown[^6] = 5;
    countdown[^7] = 6;
    countdown[^8] = 7;
    countdown[^9] = 8;
    countdown[^10] = 9; //first value
    1^ به معنای آخرین عضو آرایه‌است (به آن implicit "from the end" index operator هم می‌گویند) و اولین عضو آرایه، n^ است که در اینجا n، مساوی اندازه‌ی مجموعه‌است. مزیت اینکار، بالا رفتن خوانایی این قطعه کد و عدم نیاز به انجام دستی محاسبات آن‌است.

    مثالی دیگر (ترکیب ویژگی Index Initializer مطلب جاری و استفاده از hat operator):
    class Greeter
    {
        public char[] Message { get; set; } = "Hello?".ToCharArray();
    
        public int[] SomeNums = new int[5];
    
        public override string ToString()
        {
            return $"Message = {string.Join(',', Message)}, SomeNums = {string.Join(',', SomeNums)}";
        }
    }
    
    var greeter = new Greeter { 
        Message = { 
            [^1] = '!'
        },
         SomeNums = 
         {
             [^1] = 5,
             [^2] = 4,
             [^3] = 3,
             [^4] = 2,
             [^5] = 1
         }
    };
    خروجی سطر زیر:
    Console.WriteLine(greeter);
    این مورد است:
    Message = H,e,l,l,o,!, SomeNums = 1,2,3,4,5
    چون تنظیم '!' = [1^] سبب تغییر آخرین عضو آرایه به ! شده‌است.

    و یا در نمونه مثالی دیگر، آخرین عضو لیست اول، با عدد 15 تعویض شده‌است:
    var initialList = Enumerable.Range(1, 10);
    var list = new List<int>(initialList)
    {
        [^1] = 15
    }; 
    Console.WriteLine(string.Join(" ", anotherList));
    // 1 2 3 4 5 6 7 8 9 15