حول العناصر الجديدة في .NET 5 و C # 9.0

طاب مسائك.



لقد استخدمنا .NET منذ إنشائها. لدينا حلول مكتوبة في جميع إصدارات إطار العمل قيد الإنتاج: من أول إصدار إلى أحدث إصدار من .NET Core 3.1.



إن تاريخ .NET ، الذي كنا نتابعه عن كثب طوال هذا الوقت ، يحدث أمام أعيننا: إصدار .NET 5 ، المقرر إصداره في نوفمبر ، تم إطلاقه للتو في شكل Release Candidate 2. لقد حذرنا منذ فترة طويلة من أن الإصدار الخامس سيكون عصريًا: سينتهي به انفصام الشخصية .NET ، عندما كان هناك فرعين للإطار: كلاسيكي و Core. الآن سوف يندمجون في النشوة ، وسيكون هناك .NET واحد مستمر.



تم إصدار RC2يمكنك بالفعل البدء في استخدامه بالكامل - لا يُتوقع حدوث تغييرات جديدة قبل الإصدار ، لن يكون هناك سوى إصلاح للأخطاء التي تم العثور عليها. علاوة على ذلك: لدى RC2 بالفعل موقع ويب رسمي مخصص لـ .NET.



ونقدم لك نظرة عامة على الابتكارات في .NET 5 و C # 9. تم أخذ جميع المعلومات مع أمثلة التعليمات البرمجية من المدونة الرسمية لمطوري منصة .NET (وكذلك من العديد من المصادر الأخرى) وتم فحصها شخصيًا.



أصلي جديد وأنواع جديدة فقط



أضافت C # و .NET الأنواع الأصلية في وقت واحد:



  • nint و nuint لـ C #
  • System.IntPtr و System.UIntPtr المطابق في BCL


الهدف من إضافة هذه الأنواع هو العمليات باستخدام واجهات برمجة التطبيقات منخفضة المستوى. والحيلة هي أن الحجم الحقيقي لهذه الأنواع يتم تحديده بالفعل في وقت التشغيل ويعتمد على درجة وضوح النظام: بالنسبة إلى 32 بت ، سيكون حجمها 4 بايت ، و 64 بت ، على التوالي ، 8 بايت.



على الأرجح ، لن تواجه هذه الأنواع في العمل الحقيقي. كما هو الحال مع نوع جديد آخر: النصف. هذا النوع موجود فقط في BCL ، ولا يوجد تناظرية له في C # حتى الآن. إنه نوع 16 بت لقيم الفاصلة العائمة. يمكن أن يكون مفيدًا لتلك الحالات عندما لا تكون الدقة الجهنمية مطلوبة ، ويمكنك الفوز ببعض الذاكرة لتخزين القيم ، لأن النوعين float و double يحتلان 4 و 8 بايت. الشيء الأكثر إثارة للاهتمام هو أنه بالنسبة لهذا النوع بشكل عام حتى الآنلم يتم تعريف العمليات الحسابية ، ولا يمكنك حتى إضافة متغيرين من النوع Half بدون تحويلهما صراحة إلى عائم أو مزدوج. وهذا يعني أن الغرض من هذا النوع الآن نفعي بحت - لتوفير المساحة. ومع ذلك ، فإنهم يخططون لإضافة الحساب إليه في الإصدار التالي من .NET و C #. في سنة.



سمات الوظائف المحلية



في السابق ، كانت محظورة ، مما تسبب في بعض الإزعاج. على وجه الخصوص ، كان من المستحيل تعليق سمات معلمات الوظائف المحلية. يمكنك الآن تعيين سمات لها ، لكل من الوظيفة نفسها ومعلماتها. على سبيل المثال ، مثل هذا:



#nullable enable
private static void Process(string?[] lines, string mark)
{
    foreach (var line in lines)
    {
        if (IsValid(line))
        {
            // Processing logic...
        }
    }

    bool IsValid([NotNullWhen(true)] string? line)
    {
        return !string.IsNullOrEmpty(line) && line.Length >= mark.Length;
    }
}


تعابير لامدا الثابتة



الهدف من الميزة هو التأكد من أن تعبيرات lambda لا يمكنها التقاط أي سياق ومتغيرات محلية موجودة خارج التعبير نفسه. بشكل عام، وحقيقة أنها يمكن التقاط السياق المحلي هو مفيد في كثير من الأحيان في التنمية. لكن في بعض الأحيان قد يكون هذا سببًا في حدوث أخطاء يصعب اكتشافها.



لتجنب مثل هذه الأخطاء ، يمكن الآن تمييز تعبيرات lambda بالكلمة الأساسية الثابتة. وفي هذه الحالة ، يفقدون الوصول إلى أي سياق محلي: من المتغيرات المحلية إلى هذا والقاعدة.



إليك مثال استخدام شامل:



static void SomeFunc(Func<int, int> f)
{
    Console.WriteLine(f(5));
}

static void Main(string[] args)
{
    int y1 = 10;
    const int y2 = 10;
    SomeFunc(i => i + y1);          //  15
    SomeFunc(static i => i + y1);   //  : y1    
    SomeFunc(static i => i + y2);   //  15
}


لاحظ أن الثوابت تلتقط لامدا ثابتة على ما يرام.



GetEnumerator كطريقة تمديد



الآن يمكن أن تكون طريقة GetEnumerator طريقة تمديد ، والتي ستسمح لك بالتكرار خلال foreach حتى التي لم يكن من الممكن تكرارها من قبل. على سبيل المثال - tuples.



فيما يلي مثال عندما يصبح من الممكن التكرار عبر ValueTuple عبر foreach باستخدام التابع extension المكتوب من أجله:



static class Program
{
    public static IEnumerator<T> GetEnumerator<T>(this ValueTuple<T, T, T, T, T> source)
    {
        yield return source.Item1;
        yield return source.Item2;
        yield return source.Item3;
        yield return source.Item4;
        yield return source.Item5;
    }

    static void Main(string[] args)
    {
        foreach(var item in (1,2,3,4,5))
        {
            System.Console.WriteLine(item);
        }
    }
}


يقوم هذا الرمز بطباعة الأرقام من 1 إلى 5 إلى وحدة التحكم.



تجاهل النمط في معاملات تعبيرات لامدا والوظائف المجهولة



التحسين الجزئي. في حال لم تكن بحاجة إلى معلمات في تعبير lambda أو في دالة مجهولة ، يمكنك استبدالها بشرطة سفلية ، وبالتالي تجاهل:



Func<int, int, int> someFunc1 = (_, _) => {return 5;};
Func<int, int, int> someFunc2 = (int _, int _) => {return 5;};
Func<int, int, int> someFunc3 = delegate (int _, int _) {return 5;};


بيانات المستوى الأعلى في C #



هذا هو هيكل رمز C # مبسط. الآن ، تبدو كتابة أبسط رمز أمرًا بسيطًا حقًا:



using System;

Console.WriteLine("Hello World!");


وسوف يتم تجميع كل شيء على ما يرام. أي أنك الآن لا تحتاج إلى إنشاء طريقة يتم فيها وضع بيان مخرجات وحدة التحكم ، ولست بحاجة إلى وصف أي فئة يجب أن توضع فيها الطريقة ، وليست هناك حاجة لتحديد مساحة الاسم التي يجب إنشاء الفئة بها.



بالمناسبة ، في المستقبل ، يفكر مطورو C # في تطوير سمة ذات بناء جملة مبسط ومحاولة التخلص من النظام المستخدم ؛ في حالات واضحة. في غضون ذلك ، يمكنك التخلص منه ببساطة عن طريق كتابة مثل هذا:



System.Console.WriteLine("Hello World!");


وسيكون حقًا برنامج عمل ذو سطر واحد.



يمكن استخدام خيارات أكثر تعقيدًا:



using System;
using System.Runtime.InteropServices;

Console.WriteLine("Hello World!");
FromWhom();
Show.Excitement("Top-level programs can be brief, and can grow as slowly or quickly in complexity as you'd like", 8);

void FromWhom()
{
    Console.WriteLine($"From {RuntimeInformation.FrameworkDescription}");
}

internal class Show
{
    internal static void Excitement(string message, int levelOf)
    {
        Console.Write(message);

        for (int i = 0; i < levelOf; i++)
        {
            Console.Write("!");
        }

        Console.WriteLine();
    }
}


في الواقع ، سيقوم المترجم نفسه بتغليف كل هذه التعليمات البرمجية في مساحات الأسماء والفئات الضرورية ، ولن تعرف عنها.



بالطبع ، هذه الميزة لها حدود. العامل الرئيسي هو أنه لا يمكن القيام بذلك إلا في ملف مشروع واحد. كقاعدة عامة ، من المنطقي القيام بذلك في الملف حيث قمت مسبقًا بإنشاء نقطة الدخول إلى البرنامج في شكل الوظيفة الرئيسية (سلسلة [] args). في الوقت نفسه ، لا يمكن تحديد الوظيفة الرئيسية نفسها هناك - وهذا هو القيد الثاني. في الواقع ، مثل هذا الملف نفسه مع بناء جملة مبسط هو الوظيفة الرئيسية ، وحتى أنه يحتوي على متغير args ضمنيًا ، وهو مصفوفة ذات معلمات. أي أن هذا الكود سيجمع ويعرض طول المصفوفة:



System.Console.WriteLine(args.Length);


بشكل عام ، الميزة ليست هي الأهم ، ولكنها مناسبة تمامًا لأغراض العرض والتدريب. التفاصيل هنا .



مطابقة النمط في بيان if



تخيل أنك بحاجة إلى التحقق من متغير كائن أنه ليس من نوع معين. حتى الآن ، كان من الضروري أن تكتب مثل هذا:



if (!(vehicle is Car)) { ... }


لكن مع C # 9.0 ، يمكنك أن تكتب بشكل إنساني:



if (vehicle is not Car) { ... }


أصبح من الممكن أيضًا تسجيل بعض الشيكات بشكل مضغوط:



if (context is {IsReachable: true, Length: > 1 })
{
    Console.WriteLine(context.Name);
}


هذا الترميز الجديد مكافئ للرمز القديم الجيد مثل هذا:



if (context is object && context.IsReachable && context.Length > 1 )
{
    Console.WriteLine(context.Name);
}


أو يمكنك أيضًا كتابة نفس الشيء بطريقة جديدة نسبيًا (ولكن هذا بالفعل بالأمس):



if (context?.IsReachable && context?.Length > 1 )
{
    Console.WriteLine(context.Name);
}


في الصيغة الجديدة ، يمكنك أيضًا استخدام عوامل التشغيل المنطقية و ، أو لا ، أقواس الجمع لتحديد الأولويات:



if (context is {Length: > 0 and (< 10 or 25) })
{
    Console.WriteLine(context.Name);
}


وهذه مجرد تحسينات لمطابقة الأنماط في حالة عادية. ما أضفناه إلى النمط المطابق لتعبير التبديل - اقرأ.



مطابقة النمط المحسن في تعبير التبديل



تم تحسين تعبير التبديل (يجب عدم الخلط بينه وبين عبارة التبديل) بشكل كبير من حيث مطابقة النمط. دعونا نلقي نظرة على أمثلة من الوثائق الرسمية . يتم تخصيص الأمثلة لحساب أجرة نقل معينة في وقت معين. إليك المثال الأول:



public decimal CalculateToll(object vehicle) =>
    vehicle switch
{
    Car c           => 2.00m,
    Taxi t          => 3.50m,
    Bus b           => 5.00m,
    DeliveryTruck t => 10.00m,
    { }             => throw new ArgumentException("Unknown vehicle type", nameof(vehicle)),
    null            => throw new ArgumentNullException(nameof(vehicle))
};


آخر سطرين في بيان التبديل جديدان. تمثل الأقواس المتعرجة أي كائن ليس فارغًا. ويمكنك الآن استخدام الكلمة الرئيسية المطابقة للمطابقة مقابل null.



هذا ليس كل شيء. لاحظ أنه لكل تعيين لكائن ما ، يجب عليك إنشاء متغير: c للسيارة ، و t للتاكسي ، وهكذا. لكن لا يتم استخدام هذه المتغيرات. في مثل هذه الحالات ، يمكنك بالفعل استخدام نمط الإهمال في C # 8.0 الآن:



public decimal CalculateToll(object vehicle) =>
    vehicle switch
{
    Car _           => 2.00m,
    Taxi _          => 3.50m,
    Bus _           => 5.00m,
    DeliveryTruck _ => 10.00m,
    // ...
};


لكن بدءًا من الإصدار التاسع من C # ، لا يمكنك كتابة أي شيء على الإطلاق في مثل هذه الحالات:



public decimal CalculateToll(object vehicle) =>
    vehicle switch
{
    Car             => 2.00m,
    Taxi            => 3.50m,
    Bus             => 5.00m,
    DeliveryTruck   => 10.00m,
    // ...
};


لا تنتهي التحسينات على تعبير التبديل عند هذا الحد. أصبح من الأسهل الآن كتابة تعبيرات أكثر تعقيدًا. على سبيل المثال ، يجب أن تعتمد النتيجة التي يتم إرجاعها غالبًا على قيم خصائص الكائن الذي تم تمريره. الآن يمكن كتابة هذا بشكل أقصر وأكثر ملاءمة من مجموعة من if:



public static decimal CalculateToll(object vehicle) =>
    vehicle switch
{
    Car { Passengers: 0 } => 2.00m + 0.50m,
    Car { Passengers: 1 } => 2.0m,
    Car { Passengers: 2 } => 2.0m - 0.50m,
    Car => 2.00m - 1.0m,
    // ...
};


انتبه إلى الأسطر الثلاثة الأولى في التبديل: في الواقع ، يتم التحقق من قيمة خاصية Passengers ، وفي حالة المساواة ، يتم إرجاع النتيجة المقابلة. إذا لم يكن هناك تطابق ، فسيتم إرجاع قيمة المتغير العام (السطر الرابع داخل المحول). بالمناسبة ، يتم فحص قيم الخصائص فقط إذا كان كائن السيارة الذي تم تمريره ليس فارغًا وكان مثيلًا لفئة السيارة. بمعنى ، يجب ألا تخاف من الاستثناء المرجعي الفارغ عند التحقق.



لكن هذا ليس كل شيء. الآن ، في تعبير التبديل ، يمكنك حتى كتابة تعبيرات لمطابقة أكثر ملاءمة:



public static decimal CalculateToll(object vehicle) =>
    vehicle switch
{
    Bus b when ((double)b.Riders / (double)b.Capacity) < 0.50 => 5.00m + 2.00m,
    Bus b when ((double)b.Riders / (double)b.Capacity) > 0.90 => 5.00m - 1.00m,
    Bus => 5.00m,

    DeliveryTruck t when (t.GrossWeightClass >= 5000) => 10.00m + 5.00m,
    DeliveryTruck t when (t.GrossWeightClass >= 3000 && t.GrossWeightClass < 5000) => 10.00m,
    DeliveryTruck => 8.00m,
    // ...
};


وهذا ليس كل شيء. تم توسيع بناء جملة تعبيرات التبديل لتشمل تعبيرات التبديل المتداخلة لتسهيل وصف الشروط المعقدة:



public static decimal CalculateToll(object vehicle) =>
    vehicle switch
{
    Car c => c.Passengers switch
    {
        0 => 2.00m + 0.5m,
        1 => 2.0m,
        2 => 2.0m - 0.5m,
        _ => 2.00m - 1.0m
    },
    // ...
};


نتيجة لذلك ، إذا قمت بلصق جميع أمثلة التعليمات البرمجية المقدمة بالفعل ، فستحصل على هذه الصورة مع جميع الابتكارات الموصوفة مرة واحدة:



public static decimal CalculateToll(object vehicle) =>
    vehicle switch
{
    Car c => c.Passengers switch
    {
        0 => 2.00m + 0.5m,
        1 => 2.0m,
        2 => 2.0m - 0.5m,
        _ => 2.00m - 1.0m
    },

    Taxi t => t.Fares switch
    {
        0 => 3.50m + 1.00m,
        1 => 3.50m,
        2 => 3.50m - 0.50m,
        _ => 3.50m - 1.00m
    },

    Bus b when ((double)b.Riders / (double)b.Capacity) < 0.50 => 5.00m + 2.00m,
    Bus b when ((double)b.Riders / (double)b.Capacity) > 0.90 => 5.00m - 1.00m,
    Bus => 5.00m,

    DeliveryTruck t when (t.GrossWeightClass >= 5000) => 10.00m + 5.00m,
    DeliveryTruck t when (t.GrossWeightClass >= 3000 && t.GrossWeightClass < 5000) => 10.00m,
    DeliveryTruck => 8.00m,

    null => throw new ArgumentNullException(nameof(vehicle)),
    _ => throw new ArgumentException(nameof(vehicle))
};


لكن هذا ليس كل شيء أيضًا. إليك مثال آخر: وظيفة عادية تستخدم آلية تعبير التبديل لتحديد الحمل بناءً على الوقت المنقضي: ساعة الذروة الصباحية / المسائية ، فترات النهار والليل:



private enum TimeBand
{
    MorningRush,
    Daytime,
    EveningRush,
    Overnight
}

private static TimeBand GetTimeBand(DateTime timeOfToll) =>
    timeOfToll.Hour switch
    {
        < 6 or > 19 => TimeBand.Overnight,
        < 10 => TimeBand.MorningRush,
        < 16 => TimeBand.Daytime,
        _ => TimeBand.EveningRush,
    };


كما ترى ، في C # 9.0 ، من الممكن أيضًا استخدام عوامل المقارنة <،> ، <= ،> = ، بالإضافة إلى العوامل المنطقية و ، أو لا ، عند المطابقة.



لكن هذه ، اللعنة ، ليست النهاية. يمكنك الآن استخدام ... tuples في تعبير التبديل. فيما يلي مثال كامل للرمز الذي يحسب معاملًا معينًا للأجرة ، اعتمادًا على يوم الأسبوع والوقت من اليوم واتجاه السفر (من / إلى المدينة):



private enum TimeBand
{
    MorningRush,
    Daytime,
    EveningRush,
    Overnight
}

private static bool IsWeekDay(DateTime timeOfToll) =>
    timeOfToll.DayOfWeek switch
{
    DayOfWeek.Saturday => false,
    DayOfWeek.Sunday => false,
    _ => true
};

private static TimeBand GetTimeBand(DateTime timeOfToll) =>
    timeOfToll.Hour switch
{
    < 6 or > 19 => TimeBand.Overnight,
    < 10 => TimeBand.MorningRush,
    < 16 => TimeBand.Daytime,
    _ => TimeBand.EveningRush,
};

public static decimal PeakTimePremiumFull(DateTime timeOfToll, bool inbound) =>
    (IsWeekDay(timeOfToll), GetTimeBand(timeOfToll), inbound) switch
{
    (true, TimeBand.MorningRush, true) => 2.00m,
    (true, TimeBand.MorningRush, false) => 1.00m,
    (true, TimeBand.Daytime, true) => 1.50m,
    (true, TimeBand.Daytime, false) => 1.50m,
    (true, TimeBand.EveningRush, true) => 1.00m,
    (true, TimeBand.EveningRush, false) => 2.00m,
    (true, TimeBand.Overnight, true) => 0.75m,
    (true, TimeBand.Overnight, false) => 0.75m,
    (false, TimeBand.MorningRush, true) => 1.00m,
    (false, TimeBand.MorningRush, false) => 1.00m,
    (false, TimeBand.Daytime, true) => 1.00m,
    (false, TimeBand.Daytime, false) => 1.00m,
    (false, TimeBand.EveningRush, true) => 1.00m,
    (false, TimeBand.EveningRush, false) => 1.00m,
    (false, TimeBand.Overnight, true) => 1.00m,
    (false, TimeBand.Overnight, false) => 1.00m,
};


تستخدم طريقة PeakTimePremiumFull المجموعات للمطابقة ، وقد أصبح هذا ممكنًا في الإصدار الجديد من C # 9.0. بالمناسبة ، إذا نظرت عن كثب إلى الكود ، فإن اثنين من التحسينات يقترحان نفسيهما:



  • آخر ثمانية أسطر ترجع نفس القيمة ؛
  • حركة المرور ليلا ونهارا لها نفس المعامل.


نتيجة لذلك ، يمكن تقليل رمز الطريقة بشكل كبير باستخدام نمط الاستبعاد:



public static decimal PeakTimePremiumFull(DateTime timeOfToll, bool inbound) =>
    (IsWeekDay(timeOfToll), GetTimeBand(timeOfToll), inbound) switch
{
    (true, TimeBand.MorningRush, true)  => 2.00m,
    (true, TimeBand.MorningRush, false) => 1.00m,
    (true, TimeBand.Daytime,     _)     => 1.50m,
    (true, TimeBand.EveningRush, true)  => 1.00m,
    (true, TimeBand.EveningRush, false) => 2.00m,
    (true, TimeBand.Overnight,   _)     => 0.75m,
    (false, _,                   _)     => 1.00m,
};


حسنًا ، إذا نظرت عن كثب ، فيمكنك تقليل هذا الخيار ، بإخراج المعامل 1.0 في الحالة العامة:



public static decimal PeakTimePremiumFull(DateTime timeOfToll, bool inbound) =>
    (IsWeekDay(timeOfToll), GetTimeBand(timeOfToll), inbound) switch
{
    (true, TimeBand.Overnight, _) => 0.75m,
    (true, TimeBand.Daytime, _) => 1.5m,
    (true, TimeBand.MorningRush, true) => 2.0m,
    (true, TimeBand.EveningRush, false) => 2.0m,
    _ => 1.0m,
};


فقط في حالة ، دعني أوضح: يتم إجراء المقارنات بالترتيب الذي تم سردها به. في المباراة الأولى ، يتم إرجاع القيمة المقابلة ولا يتم إجراء مقارنات أخرى.





يمكن أيضًا استخدام Update Tuples في تعبير التبديل في C # 8.0. المطور عديم القيمة الذي كتب هذا المقال أصبح أكثر ذكاءً.





وأخيرًا ، إليك مثال آخر مجنون يوضح الصيغة الجديدة للمطابقة مع كل من خصائص المجموعات وخصائص الكائن:



public static bool IsAccessOkOfficial(Person user, Content content, int season) => 
    (user, content, season) switch 
{
    ({Type: Child}, {Type: ChildsPlay}, _)          => true,
    ({Type: Child}, _, _)                           => false,
    (_ , {Type: Public}, _)                         => true,
    ({Type: Monarch}, {Type: ForHerEyesOnly}, _)    => true,
    (OpenCaseFile f, {Type: ChildsPlay}, 4) when f.Name == "Sherlock Holmes"  => true,
    {Item1: OpenCaseFile {Type: var type}, Item2: {Name: var name}} 
        when type == PoorlyDefined && name.Contains("Sherrinford") && season >= 3 => true,
    (OpenCaseFile, var c, 4) when c.Name.Contains("Sherrinford")              => true,
    (OpenCaseFile {RiskLevel: >50 and <100 }, {Type: StateSecret}, 3) => true,
    _                                               => false,
};


كل هذا يبدو غير عادي إلى حد ما. لفهم كامل ، أوصي بأن تنظر إلى المصدر ، هناك مثال كامل للشفرة.



جديد وكذلك تحسين الكتابة الهدف بشكل أساسي



منذ وقت طويل في C # أصبح من الممكن كتابة var بدلاً من اسم النوع ، لأن النوع نفسه يمكن تحديده من السياق (في الواقع ، هذا يسمى الكتابة الهدف). أي بدلاً من الإدخال التالي:



SomeLongNamedType variable = new SomeLongNamedType();


أصبح من الممكن أن تكتب بشكل أكثر إحكاما:



var variable = new SomeLongNamedType()


وسيخمن المترجم نوع المتغير نفسه. على مر السنين ، تم تنفيذ بناء الجملة العكسي:



SomeLongNamedType variable = new ();


شكر خاص لحقيقة أن بناء الجملة هذا لا يعمل فقط عند التصريح عن متغير ، ولكن أيضًا في العديد من الحالات الأخرى حيث يمكن للمترجم تخمين النوع على الفور. على سبيل المثال ، عند تمرير المعلمات إلى طريقة وإرجاع قيمة من الطريقة:



var result = SomeMethod(new (2020,10,01));

//...

public Car SomeMethod(DateTime p)
{
    //...

    return new() { Passengers = 2 };
}


في هذا المثال ، عند استدعاء SomeMethod ، يتم إنشاء معلمة نوع DateTime باستخدام بناء جملة الاختصار. يتم إنشاء القيمة التي تم إرجاعها من الطريقة بنفس الطريقة.



حيث ستكون هناك فائدة حقًا من هذا النحو عند تحديد المجموعات:



List<DateTime> datesList = new()
{
    new(2020, 10, 01),
    new(2020, 10, 02),
    new(2020, 10, 03),
    new(2020, 10, 04),
    new(2020, 10, 05)
};

Car[] cars = 
{
    new() {Passengers = 2},
    new() {Passengers = 3},
    new() {Passengers = 4}
};


إن عدم الحاجة إلى كتابة اسم النوع الكامل عند سرد عناصر المجموعة يجعل الكود أكثر وضوحًا.



الهدف المكتوب المشغلين ؟؟ و ؟:



العامل الثلاثي؟: تم تحسينه في C # 9.0. في السابق ، كان يتطلب الامتثال الكامل لأنواع الإرجاع ، ولكنه الآن أكثر ذكاءً. فيما يلي مثال على تعبير كان غير صالح في الإصدارات السابقة من اللغة ، ولكنه قانوني تمامًا في الإصدار التاسع:



int? result = b ? 0 : null; // nullable value type


في السابق ، كان مطلوبًا إرسال صريح من صفر إلى عدد صحيح؟ .. الآن هذا ليس ضروريًا.



أيضًا في النسخة الجديدة من اللغة ، يجوز استخدام البناء التالي:



Person person = student ?? customer; // Shared base type


يختلف نوع العميل والطلاب ، على الرغم من اشتقاقهما من الشخص ، من الناحية الفنية. لم يسمح لك الإصدار السابق من اللغة باستخدام مثل هذا البناء دون كتابة نوع صريح. الآن يفهم المترجم جيدًا ما هو المقصود.



تجاوز نوع الإرجاع من الطرق



في C # 9.0 ، سُمح بتجاوز نوع الإرجاع للطرق التي تم تجاوزها. هناك متطلب واحد فقط: يجب أن يكون النوع الجديد موروثًا من الأصل (متغير). هذا مثال:



abstract class Animal
{
    public abstract Food GetFood();
    ...
}
class Tiger : Animal
{
    public override Meat GetFood() => ...;
}


في فئة Tiger ، تم إعادة تعريف القيمة المرجعة لطريقة GetFood من Food إلى Meat. من الجيد الآن اشتقاق اللحوم من الغذاء.



خصائص init ليست أعضاء للقراءة فقط



ظهرت ميزة مثيرة للاهتمام في الإصدار الجديد من اللغة: init-properties. هذه هي الخصائص التي لا يمكن تعيينها إلا أثناء التهيئة الأولية للكائن. يبدو أن أعضاء الفصل فقط موجودون لهذا الغرض ، لكنهم في الواقع أشياء مختلفة تسمح لك بحل المشكلات المختلفة. لفهم ما هو الفرق ، وما هو جمال خصائص init ، إليك مثال:



Person employee = new () {
    Name = "Paul McCartney",
    Company = "High Technologies Center",
    CompanyAddress = new () {
        Country = "Russia",
        City = "Izhevsk",
        Line1 = "246, Karl Marx St."
    }
}


يعتبر بناء الجملة هذا للإعلان عن مثيل فئة مناسبًا جدًا ، خاصةً عندما يكون هناك المزيد من الكائنات بين خصائص الفئة. لكن لهذه الصيغة قيود: يجب أن تكون خصائص الفئة المقابلة قابلة للتغيير . وذلك لأن تهيئة هذه الخصائص تحدث بعد استدعاء المُنشئ. بمعنى ، يجب الإعلان عن فئة الشخص من المثال على النحو التالي:



class Person {
    //...
    public string Name {get; set;}
    public string Company {get; set;}
    public Address CompanyAddress {get; set;}
    //...
}


ومع ذلك ، في الواقع ، خاصية الاسم غير قابلة للتغيير. الطريقة الوحيدة حاليًا لجعل هذه الخاصية للقراءة فقط هي الإعلان عن مُعيِّن خاص:



class Person {
    //...
    public string Name {get; private set;}
    //...
}


لكن في هذه الحالة ، نفقد على الفور القدرة على استخدام بناء الجملة المناسب للإعلان عن مثيل فئة عن طريق تعيين قيم للخصائص داخل الأقواس المتعرجة. ويمكننا تعيين قيمة خاصية الاسم فقط بتمريرها في معاملات إلى مُنشئ الفئة. تخيل الآن أن خاصية CompanyAddress هي ، في الواقع ، غير قابلة للتغيير أيضًا. بشكل عام ، وجدت نفسي في مثل هذا الموقف عدة مرات ، وكان علي دائمًا الاختيار بين شرين:



  • مُنشِئات خيالية مع مجموعة من المعلمات ، ولكن جميع خصائص فئة القراءة فقط ؛
  • بناء جملة مناسب لإنشاء كائن ، ولكن جميع خصائص فئة القراءة والكتابة ، ويجب أن أتذكر هذا ولا أغيرها عن طريق الخطأ في مكان ما.


في هذه المرحلة ، قد يتذكر شخص ما أعضاء الفصل المقروء فقط ويقترح تصميم فئة الشخص على النحو التالي:



class Person {
    //...
    public readonly string Name;
    public readonly string Company;
    public readonly string CompanyAddress;
    //...
}


أجبت على أن هذه الطريقة ليست فقط وفقًا لـ Feng Shui ، ولكنها أيضًا لا تحل مشكلة التهيئة المريحة: يمكن أيضًا تعيين الأعضاء للقراءة فقط في المُنشئ ، مثل الخصائص ذات الضبط الخاص.



ولكن في C # 9.0 ، تم حل هذه المشكلة: إذا قمت بتعريف خاصية كخاصية init ، فستحصل على صيغة ملائمة لإنشاء كائن ، وخاصية غير قابلة للتغيير بالفعل فيما بعد:



class Person {
    public string Name { get; init; }
    public string Company { get; init; }
    public Address CompanyAddress { get; init; }
}


بالمناسبة ، في init-properties ، كما في المُنشئ ، يمكنك تهيئة أعضاء الفصل للقراءة فقط ، ويمكنك كتابة هذا:



public class Person
{
    private readonly string name;
       
    public string Name
    { 
        get => name; 
        init => name = (value ?? throw new ArgumentNullException(nameof(Name)));
    }
}


السجل هو DTO مقنن



استمرارًا لموضوع الخصائص غير القابلة للتغيير ، نأتي إلى الموضوع الرئيسي ، في رأيي ، ابتكار اللغة: نوع السجل. تم تصميم هذا النوع لإنشاء هياكل كاملة غير قابلة للتغيير بسهولة ، وليس فقط خصائص. سبب ظهور نوع منفصل بسيط: العمل وفقًا لجميع القواعد ، نقوم باستمرار بإنشاء DTO لعزل طبقات مختلفة من التطبيق. عادةً ما تكون DTOs مجرد مجموعة من الحقول ، بدون أي منطق أعمال. وكقاعدة عامة ، لا تتغير قيم هذه الحقول خلال عمر DTO's.



.



DTO – Data Transfer Object. (DAL, BL, PL) - . «». -DTO' DAL BL, , DTO-, , DTO-, - ( HTML- JSON-).



— DTO-, - -, .



DTO- - . DTO-, AutoMapper - .



, DTO- .



لذلك ، بعد سنوات عديدة ، حصل مطورو C # على التحسين المطلوب حقًا: قاموا بإضفاء الشرعية على نماذج DTO كنوع تسجيل منفصل.



حتى الآن ، كانت جميع نماذج DTO التي أنشأناها (وقمنا بإنشائها بكميات كبيرة) فئات عادية. بالنسبة للمترجم ولوقت التشغيل ، لم يختلفوا عن جميع الفئات الأخرى ، على الرغم من أنهم لم يكونوا كذلك بالمعنى الكلاسيكي. قلة من الأشخاص استخدموا هياكل لنماذج DTO - لم يكن هذا مقبولًا دائمًا لأسباب مختلفة.



الآن يمكننا تحديد السجل (المشار إليه فيما يلي باسم السجل) - هيكل خاص مصمم لإنشاء نماذج DTO غير قابلة للتغيير. يأخذ التسجيل مكانًا وسيطًا بين الهياكل والطبقات بمعناها المعتاد. إنها طبقة فرعية وبنية فوقية. لا يزال السجل نوعًا مرجعيًا مع جميع النتائج المترتبة على ذلك. تتصرف السجلات دائمًا تقريبًا مثل فئة عادية ، ويمكن أن تحتوي على عمليات ، وتسمح بالوراثة (ولكن فقط من السجلات الأخرى ، وليس من الكائنات ، على الرغم من أنه إذا لم يرث السجل صراحةً من أي شيء ، فإنه يرث من الكائن ضمنيًا مثل كل شيء في C # ) يمكن تنفيذ واجهات. علاوة على ذلك ، لا يتعين عليك إنشاء سجلات ثابتة تمامًا على الإطلاق. وأين المعنى إذن وما هو الفرق؟



لنقم فقط بإنشاء إدخال:



public record Person 
{
    public string LastName { get; }
    public string FirstName { get; }

    public Person(string first, string last) => (FirstName, LastName) = (first, last);
}


والآن إليك مثال على كيفية استخدامه:



Person p1 = new ("Paul", "McCartney");
Person p2 = new ("Paul", "McCartney");

System.Console.WriteLine(p1 == p2);


هذا المثال سوف يطبع بشكل صحيح على وحدة التحكم. إذا كان الشخص عبارة عن فئة ، فسيتم طباعة خطأ على وحدة التحكم لأنه تتم مقارنة الكائنات بالمرجع: يتساوى متغيرين مرجعيين فقط إذا كانوا يشيران إلى نفس الكائن. لكن هذا ليس هو الحال مع التسجيلات. تتم مقارنة السجلات بقيمة جميع الحقول ، بما في ذلك الحقول الخاصة.



استمرارًا من المثال السابق ، لنلقِ نظرة على هذا الرمز:



System.Console.WriteLine(p1);


في حالة الفصل ، سوف نتلقى الاسم الكامل للفصل في وحدة التحكم. لكن في حالة السجلات ، سنرى هذا في وحدة التحكم:



Person { LastName = McCartney, FirstName = Paul}


الحقيقة هي أنه بالنسبة للتسجيلات ، يتم تجاوز طريقة ToString () ضمنيًا ولا تعرض اسم النوع ، ولكن قائمة كاملة من الحقول العامة ذات القيم. وبالمثل ، بالنسبة للسجلات ، فإن == و! = يتم إعادة تعريف العوامل بشكل ضمني ، مما يجعل من الممكن تغيير منطق المقارنة.



هيا نلعب مع الميراث القياسي:



public record Teacher : Person
{
    public string Subject { get; }

    public Teacher(string first, string last, string sub)
        : base(first, last) => Subject = sub;
}


لنقم الآن بإنشاء منشورين من نوعين مختلفين ومقارنتهما:



Person p = new("Paul", "McCartney");
Teacher t = new("Paul", "McCartney", "Programming");

System.Console.WriteLine(p == t);


بالرغم من أن سجل المدرس موروث من الشخص ، فإن المتغيرين p و t لن يكونا متساويين ، ستتم طباعة خطأ على وحدة التحكم. هذا لأن المقارنة تتم ليس فقط لجميع حقول السجلات ، ولكن أيضًا للأنواع ، والأنواع هنا مختلفة بشكل واضح.



وعلى الرغم من أن المقارنة بين أنواع السجلات الموروثة مسموح بها (ولكن لا جدوى منها) ، إلا أن مقارنة أنواع السجلات المختلفة بشكل عام غير مسموح به من حيث المبدأ:



public record Person
{
    public string LastName { get; }
    public string FirstName { get; }

    public Person(string first, string last) => (FirstName, LastName) = (first, last);
}

public record Person2
{
    public string LastName { get; }
    public string FirstName { get; }

    public Person2(string first, string last) => (FirstName, LastName) = (first, last);
}

// ...

Person p = new("Paul", "McCartney");
Person2 p2 = new("Paul", "McCartney");
System.Console.WriteLine(p == p2);    //  


يبدو أن الإدخالات هي نفسها ، ولكن سيكون هناك خطأ في التجميع في السطر الأخير. يمكنك فقط مقارنة السجلات من نفس النوع أو الأنواع الموروثة.



ميزة أخرى رائعة للسجلات هي الكلمة الأساسية with ، والتي تجعل من السهل إنشاء تعديلات على نماذج DTO الخاصة بك. ألق نظرة على مثال:



Person me = new("Steve", "Brown");
Person brother = me with { FirstName = "Paul" };


في هذا المثال ، بالنسبة لسجل Brother ، سيتم ملء جميع قيم الحقول من السجل الخاص بي ، باستثناء حقل الاسم الأول - سيتم تغييره إلى Paul.



لقد رأيت حتى الآن الطريقة الكلاسيكية لإنشاء السجلات - مع التعريفات الكاملة للمُنشئين والخصائص وما إلى ذلك. ولكن هناك الآن طريقة مقتضبة:



public record Person(string FirstName, string LastName);

public record Teacher(string FirstName, string LastName,
    string Subject)
    : Person(FirstName, LastName);

public sealed record Student(string FirstName,
    string LastName, int Level)
    : Person(FirstName, LastName);


يمكنك تحديد السجلات باختصار ، وسيقوم المترجم بإنشاء الخصائص والمنشئ نيابة عنك. ومع ذلك ، تحتوي هذه الميزة على ميزة إضافية - لا يمكنك فقط استخدام تدوين مختصر لتحديد الخصائص والمنشئ ، ولكن في نفس الوقت يمكنك إضافة طريقتك الخاصة إلى الإدخال:



public record Pet(string Name)
{
    public void ShredTheFurniture() =>
        Console.WriteLine("Shredding furniture");
}

public record Dog(string Name) : Pet(Name)
{
    public void WagTail() =>
        Console.WriteLine("It's tail wagging time");

    public override string ToString()
    {
        StringBuilder s = new();
        base.PrintMembers(s);
        return $"{s.ToString()} is a dog";
    }
}


في هذه الحالة ، سيتم أيضًا إنشاء الخصائص ومنشئ السجلات تلقائيًا. رمز معياري أقل وأقل ، ولكنه ينطبق فقط على المشاركات. هذا لا يعمل مع الطبقات والهياكل.



بالإضافة إلى كل ما قيل بالفعل ، يمكن للمجمع أيضًا إنشاء مُنشئ للسجلات تلقائيًا:



var person = new Person("Bill", "Wagner");

var (first, last) = person; //    
Console.WriteLine(first);
Console.WriteLine(last);


ومع ذلك ، على مستوى IL ، لا تزال السجلات فئة. ومع ذلك ، هناك شك واحد لم يتم العثور على تأكيد له حتى الآن: سيتم تحسين السجلات بشكل كبير في مكان ما على مستوى وقت التشغيل. على الأرجح ، بسبب حقيقة أنه سيكون معروفًا مسبقًا أن سجلًا معينًا غير قابل للتغيير. هذا يفتح فرصًا للتحسين ، على الأقل في بيئة متعددة الخيوط ، ولا يحتاج المطور حتى إلى بذل جهود خاصة لهذا الغرض.



في غضون ذلك ، نقوم بإعادة كتابة جميع نماذج DTO من الفئات إلى السجلات.



مولدات المصدر. NET



يعد Source Generator (المشار إليه فيما يلي باسم مجرد مولد) ميزة مثيرة للاهتمام. المولد هو جزء من الكود يتم تنفيذه في مرحلة التجميع ، ولديه القدرة على تحليل الكود المترجم بالفعل ، ويمكنه إنشاء رمز إضافي سيتم تجميعه أيضًا. إذا لم يكن الأمر واضحًا تمامًا ، فإليك مثالًا ذا صلة بالموضوع عندما يكون المولد مطلوبًا.



تخيل تطبيق ويب C # /. NET تكتبه في ASP.NET Core. عند تشغيل هذا التطبيق ، هناك قدر كبير من عمل خلفية التهيئة لتحليل ما يتكون منه هذا التطبيق وما يجب القيام به على الإطلاق. يستخدم الانعكاس بشكل محموم. نتيجة لذلك ، يمكن أن يكون الوقت من بدء تشغيل التطبيق إلى بدء معالجة الطلب الأول طويلاً بشكل فاحش ، وهو أمر غير مقبول في الخدمات المحملة بشكل كبير. يمكن أن يساعد المولد في تقليل هذا الوقت: حتى في مرحلة التجميع ، يمكنه تحليل التطبيق المترجم بالفعل وإنشاء الكود اللازم الذي سيعمل على تهيئته بشكل أسرع عند بدء التشغيل.



يوجد أيضًا عدد كبير نسبيًا من المكتبات التي تستخدم الانعكاس لتحديد أنواع الكائنات المستخدمة في وقت التشغيل (من بينها العديد من حزم Nuget الأعلى). يفتح هذا مجالًا كبيرًا للتحسين باستخدام المولدات ، ويتوقع مؤلفو هذه الميزة تحسينات مناسبة من مطوري المكتبة.



تعد مولدات الأكواد موضوعًا جديدًا وغير مألوف جدًا بحيث لا تلائم نطاق هذا المنشور. بالإضافة إلى ذلك ، يمكنك مشاهدة مثال على أبسط عبارة "Hello، world!" مولد في هذا الاستعراض .



هناك ميزتان جديدتان مرتبطتان بمولدات الرموز ، موضحة أدناه.



الطرق الجزئية



كانت الفئات الجزئية في C # موجودة لفترة طويلة ، والغرض الأصلي منها هو فصل الكود الذي تم إنشاؤه بواسطة مصمم معين عن الكود الذي كتبه المبرمج. تم تصميم الطرق الجزئية في C # 9.0. تبدو مثل هذا:



public partial class MyClass
{
    public partial int DoSomeWork(out string p);
}
public partial class MyClass
{
    public partial int DoSomeWork(out string p)
    {
        p = "test";
        System.Console.WriteLine("Partial method");
        return 5;
    }
}


يوضح هذا المثال البديل أن الطرق الجزئية لا تختلف جوهريًا عن الأساليب العادية: يمكنها إرجاع القيم ، ويمكنها قبول المتغيرات الخارجية ، ويمكنها الحصول على مُعدِّلات الوصول.



من المعلومات المتاحة ، ستكون الطرق الجزئية مرتبطة ارتباطًا وثيقًا بمولدات الكود ، حيث يُقصد استخدامها.



مُهيئ الوحدة



هناك ثلاثة أسباب لإدخال هذه الوظيفة:



  • السماح للمكتبات بأن يكون لها نوع من التهيئة لمرة واحدة عند التمهيد بأقل قدر من النفقات العامة ولا توجد حاجة صريحة للمستخدم لاستدعاء أي شيء ؛
  • الوظيفة الحالية للمُنشئين الساكنين ليست مناسبة جدًا لهذا الدور ، لأنه يجب أن يكتشف وقت التشغيل أولاً ما إذا كان يتم استخدام فئة ذات مُنشئ ثابت على الإطلاق (هذه هي القواعد) ، وهذا يعطي تأخيرات قابلة للقياس ؛
  • يجب أن يكون لمولدات الكود نوع من منطق التهيئة الذي لا يحتاج إلى استدعاء صراحة.


في الواقع ، يبدو أن النقطة الأخيرة أصبحت حاسمة بالنسبة للميزة التي سيتم تضمينها في الإصدار. نتيجة لذلك ، توصلنا إلى سمة جديدة نحتاجها لتغطية الطريقة التي يتم التهيئة:



using System.Runtime.CompilerServices;
class C
{
    [ModuleInitializer]
    internal static void M1()
    {
        // ...
    }
}


هناك بعض القيود على الطريقة:



  • يجب أن تكون ثابتة ؛
  • يجب ألا تحتوي على معلمات ؛
  • لا ينبغي أن تعيد أي شيء ؛
  • لا ينبغي أن تعمل مع الأدوية الجنيسة.
  • يجب أن يكون الوصول إليها من الوحدة المحتوية ، أي:

    • يجب أن تكون داخلية أو عامة
    • لا يجب أن تكون طريقة محلية


وهي تعمل على النحو التالي: بمجرد أن يجد المترجم جميع الطرق المميزة بالسمة ModuleInitializer ، فإنه يولد رمزًا خاصًا يستدعيهم جميعًا. لا يمكن تحديد ترتيب استدعاء طرق التهيئة ، لكنه سيكون هو نفسه في كل تجميع.



خاتمة



بعد نشر المنشور بالفعل ، لاحظنا أنه مخصص للأخبار بلغة C # 9.0 أكثر من تكريسه لأخبار .NET نفسها. لكن اتضح بشكل جيد.



All Articles