بررسی قابلیت های جدید سی شارپ 8.0

C# 8.0 نسخه اصلی بعدی سی شارپ است و مدت زمان زیادی (حتی قبل از زمانی که نسخه های فرعی 7.1، 7.2 و 7.3 منتشر شوند) در حال توسعه بوده است. در این نسخه جدید از سی شارپ قابلیت های هیجان انگیز بسیاری وجود دارد.

برنامه فعلی این است که C# 8.0 همزمان با ‎.NET Core 3.0 عرضه شود . اما قابلیت هایی که درباره آنها صحبت می کنیم به همراه Visual Studio 2019 ارائه خواهد شد. همزمان با ارائه این قابلیت ها ما در مورد هر یک از آنها به ترتیب صحبت خواهیم کرد. هدف از این پست این است که به شما بگوییم در C# 8 منتظر چه چیزی باشید و هر یک ممکن است در چه زمانی ارائه شوند.

ویژگی های جدید C# 8.0:

در اینجا مهم ترین ویژگی های C# 8.0 را مرور خواهیم کرد. پیشرفت های کوچکتری نیز در این نسخه جدید وجود دارند که قطعا طی ماه های آینده بیشتر از آن ها صحبت خواهیم کرد.

Reference Type های Nullable

هدف از این ویژگی کمک به جلوگیری از بروز Exception های Null Reference است که همه جا با آنها برخورد می کنیم  و نزدیک به نیم قرن جزو معماهای برنامه نویسی شیء گرا بوده است.
این ویژگی از قرار دادن  مقادیر null در Reference Type های پایه مانند string جلوگیری می کند و آن Type ها را به Non-Nullable تبدیل می کند! این جلوگیری از طریق نمایش اخطار (Warning) صورت می گیرد و نه با استفاده از Error ها.

اما در پروژه های موجود از قبل Warning های جدیدی دریافت خواهید کرد، بنابراین باید انتخاب کنید که می خواهید از این قابلیت استفاده کنید یا نه (می توانید این قابلیت را در سطح، پروژه، فایل و یا حتی یک خط از کد فعال یا غیرفعال کنید). به عنوان مثال، به کد زیر توجه کنید:

string s = null; // Warning: Assignment of null to non-nullable reference type

اما چه می شود اگر بخواهید مقدار null را داشته باشید؟ در این صورت باید نوع  از Nullable Reference Type استفاده کنید . مثال:

string? s = null; // Ok

زمانی که شما سعی می کنید از Nullable Reference استفاده کنید ابتدا باید آن را برای مقدار null بررسی کنید . کامپایلر گردش کار کد شما را تجزیه و تحلیل می کند تا ببیند آیا شما یک مقدار null در نقطه ای که می خواهید از آبجکت مورد نظرتان استفاده کنید تحویل خواهید گرفت یا نه:

void M(string? s)
{
    Console.WriteLine(s.Length); // Warning: Possible null reference exception
    if (s != null)
    {
        Console.WriteLine(s.Length); // Ok: You won't get here if s is null
    }
}

نتیجه ماجرا این خواهد بود که سی شارپ به شما اجازه می دهد هدف تان از نحوه کاربرد null را بیان کنید و اگر از آن هدف پیروی نکردید این موضوع را در قالب یک اخطار به شما نمایش می دهد.

Stream های Async

ویژگی async / await از C# 5.0 به شما این امکان را می دهد که نتایج غیر همزمان را بدون نیاز به callback استفاده یا تولید کنید:

async Task GetBigResultAsync()
{
    var result = await GetResultAsync();
    if (result > 20) return result; 
    else return -1;
}

اما زمانی که می خواهید نتایج ادامه دار را استفاده (یا تولید) کنید خیلی کاربرد ندارد؛ مثل زمانی که می خواهید از دستگاه های IOT یا سرویس های ابری داده ای را مستمرا بخوانید و یا به آن ارسال کنید. برای این کار باید از Stream های Async استفاده کنید.
در ورژن جدید IAsyncEnumerable<T>‎ معرفی شده است، چیزی که دقیقا نیاز داشتیم، یک نسخه Async (غیرهمزمان) از IEnumerable<T>‎ .
این نسخه از زبان به شما اجازه استفاده از await foreach بر روی این گونه کالکشن ها برای خواندن ردیف هایشان، و پشتیبانی از دستور yield return  هنگامی که چنین کالکشنی را تولید می کنید می دهد:

async IAsyncEnumerable GetBigResultsAsync()
{
    await foreach (var result in GetResultsAsync())
    {
        if (result > 20) yield return result; 
    }
}

Range ها و Index ها

یک نوع داده به نام Index اضافه شده است که برای ایندکس گذاری استفاده می شود. شما می توانید آنرا با استفاده از یک int جهت شروع شمارش از ابتدا ایجاد کنید، و یا با استفاده از اپراتور ^ شمارش از آخر به اول را پیاده سازی کنید:

Index i1 = 3;  // number 3 from beginning
Index i2 = ^4; // number 4 from end
int[] a = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };
Console.WriteLine($"{a[i1]}, {a[i2]}"); // "3, 6"

همچنین یک نوع داده Range اضافه شده است که از دو ایندکس تشکیل شده است، اولی نقطه شروع و دومی نقطه پایان را مشخص می کند و نحوه نوشتن آن به صورت یک عبارت x..y است. با استفاده از Range می توانید یک تکه از یک کالکشن را مشخص کنید:

var slice = a[i1..i2]; // { 3, 4, 5 }

پیاده سازی پیش فرض برای اعضای Interface

در حال حاضر وقتی شما یک interface را تعریف می کنید دیگر کار تمام است و نمی توانید عضو جدیدی را به آن اضافه کنید بدون این که ترکیب کلاس های ارث بری شده از آن به هم نخورد.
در C# 8.0 اجازه تعریف بدنه برای اعضای interface را خواهید داشت. بنابراین اگر این عضو جدید در کلاس های زیرمجموعه پیاده سازی نشده باشد (احتمال دارد زمانی کلاس فرزند نوشته شده باشد که هنوز این عضو جدید وجود نداشته است) خود به خود از بدنه پیش فرض به صورتی که در interface تعریف شده است در کلاس فرزند استفاده خواهد شد:

interface ILogger
{
    void Log(LogLevel level, string message);
    void Log(Exception ex) => Log(LogLevel.Error, ex.ToString()); // New overload
}

class ConsoleLogger : ILogger
{
    public void Log(LogLevel level, string message) { ... }
    // Log(Exception) gets default implementation
}

در مثال فوق کلاس ConsoleLogger مجبور نیست متد Overload جدید ILogger را پیاده سازی کند، چرا که این Overload یک پیاده سازی پیش فرض در Interface دارد. با این توصیف شما می توانید اعضای جدید به Interface های موجود اضافه کنید بدون اینکه کلاس های فرزند را مجبور کنید آن را پیاده سازی کنند و به این صورت هیچ یک از کلاس های فرزند این Interface هنگام افزودن اعضای جدید به Interface از کار نخواهد افتاد.

الگوهای Recursive

فرض کنید با استفاده از یک حلقه foreach قرار است یک کالکشن به نام People که جنس اعضای آن ممکن است object باشد باشد را پیمایش کنیم، و اگر Type ردیف کنونی خوانده شده توسط foreach از نوع Student بود و مقدار پراپرتی Graduated آن false بود و پراپرتی Name آن از نوع String، و دارای مقداری غیر از null بود مقدار پراپرتی Name را در قالب متغیری به نام name خوانده و آن را return کنیم. برای انجام پروسه فوق می توانیم با نوشتن یک pattern کار را انجام دهیم:

IEnumerable GetEnrollees()
{
    foreach (var p in People)
    {
        if (p is Student { Graduated: false, Name: string name }) yield return name;
    }
}

عبارت های Switch

استفاده از عبارت های Switch با استفاده از Pattern ها در در C# 7.0 میسر بود، اما نوشتن آنها کار سختی بود. در C# 8.0 به راحتی می توان یک عبارت Switch نوشت، طوری که همه case های آن خود یک عبارت باشند. به مثال زیر توجه کنید:

var area = figure switch 
{
    Line l      => 0,
    Rectangle r => r.Width * r.Height,
    Circle c    => Math.PI * c.Radius * c.Radius,
    _           => throw new UnknownFigureException(figure)
};

در دستور فوق قرار است یک ناحیه از یک شکل را انتخاب کرده و Return کنیم. عبارت switch فوق به این صورت خوانده می شود: متغیر area بر اساس مقدار و نوع figure تعریف شود و مقدار آن به این صورت محاسبه شود:

  • اگر  شکل مورد ارزیابی (figure) از جنس Line است مقدار 0 را برگردان.
  • اگر شکل مورد ارزیابی از جنس Rectangle است، متغیر r به آن اشاره کند، و به عنوان نتیجه بازگشتی مقدار عرض r ضربدر طول r شود.
  • اگر شکل مورد ارزیابی از جنس Circle است متغیر r در عبارت به آن اشاره کند، و مقدار بازگشتی حاصل ضرب عدد Pi در  شعاع، در شعاع آن دایره باشد.
  • خط آخر (مشابه حالت default در دستور switch) نیز می گوید اگر figure با هیچ یک از ارزیابی های فوق تطبیق نداشت یک خطا از نوع UnknownFigureException(figure) ایجاد شده و تحویل کد فرخوان شود.

تشخیص خودکار Type ای که قرار بوده جلوی دستور new نوشته شود

در بسیاری از موارد، زمانی که می خواهید یک Object جدید تعرف کنید نوع آن Object بر اساس کد نوشته شده قابل حدس زدن است. در این گونه موارد اجازه خواهید داشت نام Type را ذکر نکنید. به مثال زیر توجه کنید:

Point[] ps = { new (1, 4), new (3,-2), new (9, 5) }; // All Points

در مثال فوق هر یک از مواردی که داخل بلوک سمت راست مساوی نوشته شده (مثلا new (1,4)‎) از نوع Point محسوب خواهند شد.
پیاده سازی این مورد با کمک یکی از اعضای جامعه برنامه نویس به نام علیرضا حبیبی انجام شده است. با تشکر از ایشان!

وابستگی های سکوی اجرا

اکثر ویژگیهای زبان C# 8.0 در هر نسخه از ‎.NET اجرا خواهند شد. با این وجود، تعداد کمی از آنها به پلت فرم وابستگی دارند. Stream های Async، ‏Index ها و Range ها همه وابسته به Type های معرفی شده در Framework جدید هستند که بخشی از ‎.NET Standard 2.1 خواهند بود.
همان طور که Immo در مقاله Announcing .NET Standard 2.1 توضیح می دهد، دات نت کور  3.0 و نیز Xamarin ، Unity  و Mono همگی الزامات ‎.Net Standard 2.1 را پیاده سازی کرده اند، اما ‎.Net Framework 4.8 این طور نیست. این بدان معنی است که Type های مورد نیاز برای استفاده از این ویژگی ها هنگام استفاده از C# 8.0 بر روی ‎.Net Framework 4.8 وجود ندارند.
مثل همیشه، کامپایلر C#‎ در برخورد با Data Type ها رفتار ملایمی دارد. اگر بتواند یک Type را با نام و شکل مناسب پیدا کند از آن استفاده خواهد کرد.
تعریف پیاده سازی های پیش فرض در Interface ها وابسته به امکانات جدید است و این امکانات جدید در ‎.Net Framework 4.8 وجود ندارند، بنابراین این ویژگی بر روی ‎.Net Framework 4.8 و ورژن های قدیمی تر ‎.Net کار نمی کند.
تلاش در جهت پایدار (Stable) نگه داشتن Runtime نزدیک به بیش از یک دهه دست تیم تولید زبان C#‎ را از افزودن قابلیت های جدید بسته است. اما هم اکنون با توجه به این که تولید Runtime های جدید شانه به شانه و با کمک جامعه اوپن سورس در حال انجام است احساس می کنیم می توان دوباره به نسبت به تکامل و افزودن قابلیت های جدید به آنها با آزادی بیشتر اقدام کرد. این مورد توسط Scott در مقاله ای تحت عنوان Update on .Net Core 3.0 and .Net Framework 4.8 بررسی شده است و او نیز تاکید کرده است تکامل و بهبودهای کمتری را در آینده در ‎.Net Framework 4.8 شاهد خواهیم بود، در عوض تغییرات عمده ای که می توان در آن مشاهده کرد در زمینه بهبود پایداری و اتکاپذیری خواهد بود.

چگونه می توانم بیشتر یاد بگیرم؟

پروسه طراحی زبان C#‎ اوپن سورس است و در ریپوزیتوری github.com/dotnet/csharplang قرار دارد. اگر شما به طور مرتب پیگیر این رپیوزیتوری نباشید مراجعه به آن کمی برای شما گیج کننده خواهد بود. ضربان قلب طراحی این زبان، جلسات طراحی زبان است که در C# Language Design Notes تشکیل می شوند.
حدود یک سال پیش یک مقاله تحت عنوان Introducing Nullable Reference Types in C#‎ منتشر شد که هنوز هم می تواند آموزنده باشد.

همچنین شما می توانید فیلم های آموزشی مانند The future of C#‎‎ را از Microsoft Build 2018 یا What’s Coming to C#‎ از ‎.NET Conf 2018 مشاهده کنید. در این فیلم ها برخی از ویژگی های جدید C#‎ بررسی شده اند.
Kathleen  یک پست عالی در مورد برنامه ریزی قابلیت های ویژوال بیسیک در دات نت کور 3.0  به اشتراک گذاشته است.
ما در حین انتشار ویژگی های جدید در قالب Preview های Visual Studio 2019 مطالب بیشتری درباره هر یک از این ویژگی ها منتشر خواهیم کرد.
من به شخصه روزشماری می کنم تا به محض انتشار این ویژگی ها آنها را به نظر و سمع شما برسانم.

تگ ها:

C#‎ 2 New Features 4 سی شارپ 1 قابلیت های جدید 1