حسین احمدی
بنیانگذار توسینسو و برنامه نویس و توسعه دهنده ارشد وب

معرفی قابلیت های جدید سی شارپ 8

آخرین نسخه سی شارپ یعنی نسخه 8 توسط مایکروسافت معرفی شد و در این نسخه یکسری امکانات جدید به این زبان برنامه نویسی محبوب اضافه شده که در این مطلب قصد داریم این امکانات رو بررسی کنیم. نسخه نهایی سی شارپ 8 به همراه .NET Core 3 عرضه شده و برای استفاده از این امکانات می تونید از Visual Stusio 2019 آخرین به روزرسانی استفاده کنید. (برای استفاده از کدهای این مطلب یک پروژه از نوع کنسول و مبتنی بر .NET Core 3 ایجاد کنید)

دوره های شبکه، برنامه نویسی، مجازی سازی، امنیت، نفوذ و ... با برترین های ایران

تعریف member های struct به صورت readonly

در سی شارپ نسخه 7.2 قابلیتی اضافه شد که می توانستید یک struct را به عنوان readonly تعریف کنید، در حقیقت struct تعریف شده تبدیل به یک نوع immutable می شد (مثل نوع داده string). در سی شارپ 8 این امکان وجود داره تا از کلمه کلیدی readonly برای اعضای یک struct استفاده کنیم. برای مثال، struct زیر رو در نظر بگیرید:

public struct Point
{
    public double X { get; set; }
    public double Y { get; set; }

    public double Distance => Math.Sqrt(X * X + Y * Y);

    public override string ToString() => $"{X}, {Y} is {Distance} from the origin";
}

در کد بالا متد ToString هیچ یک از مقادیر تعریف شده struct رو تغییر نمیده، به همین خاطر می تونید این متد رو به صورت زیر readonly تعریف کنید:

public override readonly string ToString() => $"{X}, {Y} is {Distance} from the origin";

با اضافه کردن کد بالا، یک پیام warning دریافت می کنیم که خصوصیت Distance به صورت readonly تعریف نشده و ما داخل متد ToString از این خصوصیت استفاده کردیم. برای رفع این warning خصوصیت Distance رو هم به صورت readonly تعریف می کنیم:

public readonly double Distance => Math.Sqrt(X * X + Y * Y);

به این نکته توجه کنید که اگر خصوصیتی فقط بخش get رو داشته باشه به صورت readonly در نظر گرفته نمیشه و حتماً باید از کلمه کلیدی readonly استفاده کنید.
مورد بعدی اگر متدی تعریف کنید که داخلش یکی از خصوصیت های struct تغییر کنه و برای متد از readonly استفاده کنید، پیام خطا دریافت می کنید:

public readonly void ChangeValues(int x, int y)
{
    X = x;
    Y = y;
}

با تعریف متد بالا داخل struct تعریف شده، کد ما کامپایل نمیشه.

پیاده سازی پیش فرض اعضا در اینترفیس ها
یکی از قابلیت های جدید سی شارپ 8، امکان پیاده سازی پیش فرض اعضاء یک اینترفیس می باشد. برای مثال، اینترفیس زیر و کلاسی که از اون ارث بری کرده رو می بینیم:

public interface IPerson
{
    string FirstName { get; set; }
    string LastName { get; set; }

    string Fullname()
    {
        return FirstName + " " + LastName;
    }
}

public class Person : IPerson
{
    public string FirstName { get; set; }
    public string LastName { get; set; }
}

همونطور که می بینید ما برای مدت Fullname یک پیاده سازی پیش فرض داخل اینترفیس IPerson داریم و داخل کلاس Person پیاده سازی برای این متد انجام نشده. حالا به صورت زیر از این متد استفاده می کنیم:

IPerson person = new Person()
{
    FirstName = "Hossein",
    LastName = "Ahmadi"
};
Console.WriteLine(person.Fullname());


استفاده از Pattern Matching در قسمت های جدید

قبلاً در وب سایت توسینسو در مورد قابلیت Pattern Matching توضیح دادیم. با معرفی سی شارپ 8 امکان استفاده از Pattern Matching در بخش های جدیدی از کدها وجود خواهد داشت.
یکی از این بخش ها switch expression ها هستند. کلاس زیر رو در نظر بگیرید:

public class Person
{
    public string Type { get; }

    public Person(string type)
    {
        this.Type = type;
    }
}

در متد Main ما یک ورودی از کاربر گرفته و بر اساس آن یک کلاس Person ایجاد می کنیم:

static void Main(string[] args)
{
    var type = Console.ReadLine();
}

public static Person CreatePerson(string type)
{
    switch (type)
    {
        case "CEO":
            return new Person("Chief Executive Officer");
        case "CO":
            return new Person("Co Founder");
        default:
            throw new ArgumentException("Invalid person type");
    }
}

تا اینجا هیچ چیزی تغییر نکرده، کد متد CreatePerson رو بوسیله switch expression بازنویسی می کنیم:

public static Person CreatePerson(string type) => type switch
{
    "CEO" => new Person("Chief Executive Officer"),
    "CO" => new Person("Co Founder"),
    _ => throw new ArgumentException("Invalid person type")
};

همونطور که می بینیم مقدار کد ما خلاصه تر شده و خبری از عبارت case و }{ ها نیست، این قابلیت برای زمانی که تعداد case زیاد هست خیلی می تونه کاربردی باشه.

مورد بعدی امکان استفاده از مقادیر خصوصیت های پارامتر ورودی برای تطبیق بخش مورد نظر کد هست، متد زیر بر اساس نوع Person مقدار حقوق رو برمیگردونه:

public static decimal CalculateSalary(Person person) => person switch
{
    { Type: "CEO" } => 120000,
    { Type: "CO" } => 200000,
    _ => throw new ArgumentException("Invalid person type")
};


قابلیت tupple pattern

اگر برای تطبیق نیاز به استفاده از چند پارامتر باشه میشه از tupple pattern استفاده کرد. در کد زیر بوسیله پارامتر های ورودی نام کاربری و کلمه عبور نوع دسترسی کاربر رو بر می گردونیم:

public static string GetPermission(string username, string password) => (username, password) switch
{
    ("admin", "123") => "total admin",
    ("user1", "159") => "content manager",
    ("user2", "456") => "comment manager",
    _ => "invalid username or password"
};

بخش pattern matching قابلیت های زیادی داره که ما در این مطلب بخشی از اون ها رو بررسی کردیم. برای آشنایی با موارد پیشرفته تر می تونید از این لینک استفاده کنید.


تعریف متغیرها بوسیله using

حتماً با using و کاربردهای اون آشنایی دارید. نمونه کدی از این قابلیت رو در زیر می بینید:

using (var file = File.Open("D:\\myfile.txt",FileMode.Open))
{
    // process file
}

در سی شارپ 8 نوشتن این عبارت ساده تر شده و به صورت زیر می تونید عبارت بالا رو بنویسید:

using var file = File.Open("D:\\myfile.txt", FileMode.Open);
// process file

با نوشتن کد بالا، به صورت خودکار بعد از اتمام scope ای که متغیر file داخلش تعریف شده، متد Dispose برای متغیر file فراخوانی میشه.


تعریف static local function

بوسیله این قابلیت می توانید داخل یک local function از نوع static تعریف کنید. کد زیر نمونه ساده ای از این قابلیت است:

int Func()
{
    int x = 1;
    int y = 2;
    return Add(x, y);

    static int Add(int n1, int n2) => n1 + n2;
}

تعریف reference type های nullable

بوسیله این قابلیت شما دقیقاً مانند تعریف value type های nullable، امکان تعریف reference type های nullable را خواهید داشت. اما شاید این سوال پیش بیاد که تو نسخه های قبلی هم امکان مقداردهی null برای reference type ها وجود داشت، پس چه نیازی به reference type های nullable داریم؟ اینطور میشه که گفت که نحوه رفتار کامپایلر با متغیر های nullable و غیر nullable متفاوت میشه. بهتره این موضوع رو با یک مثال بررسی کنیم.
در قدم اول پروژه ای از نوع کنسول و .NET Core 3 ایجاد می کنیم. فایل csproj رو ویرایش کنید و داخل بخش PropertyGroup مورد زیر رو اضافه کنید:

<Nullable>enable</Nullable>

با این کار به صورت سرتاسری قابلیت Nullable به پروژه شما اضافه میشه. نمونه کد زیر رو در نظر بگیرید:

class Program
{
    static void Main(string[] args)
    {
        Person person = CreatePerson("CO");

        Console.WriteLine(person.Firstname);
    }

    public static Person CreatePerson(string type) => type switch
    {
        "CEO" => new Person() {Type = "Chief Executive Officer"},
        _ => null
    };
}

public class Person
{
    public string Firstname { get; set; }
    public string Lastname { get; set; }
    public string Type { get; set; }
}

تو کد بالا در شرایط فعلی هیچ مشکلی نیست و هیچ Warning ای دریافت نمی کنیم. اما کافیه در ابتدای کد ها directive زیر رو اضافه کنیم:

#nullable enable

با اضافه کردن این directive یکسری warning به ما نمایش داده میشه:

امکانات سی شارپ 8

دلیل این موضوع این هست که با اضافه کردن این directive کامپایلر هر جایی که احتمال مقدار null وجود داشته باشه رو به صورت warning به ما نمایش میده. حالا فرض کنید که ما بخواییم خصوصیت Firstname داخل کلاس Person امکان دریافت مقدار null رو داشته باشه. برای اینکار کافیه این خصوصیت رو به صورت nullable تعریف کنیم:

public class Person
{
    public string? Firstname { get; set; }
    public string Lastname { get; set; }
    public string Type { get; set; }
}

یا متد CreatePerson هم همین مشکل وجود داره، کافیه این متد رو به صورت زیر تغییر بدیم تا پیام warning حذف بشه:

public static Person CreatePerson(string type) => type switch
{
    "CEO" => new Person() {Type = "Chief Executive Officer"},
    _ => throw new ArgumentException("Invalid CEO")
};

مبحث nullable reference types خیلی گسترده هست که ما در اینجا توضیح مختصری راجع به این قابلیت دادیم. برای آشنایی بیشتر می تونید این لینک رو مطالعه کنید.

stream های asynchronous

قابلیت جالب بعدی در سی شارپ 8، امکان تعریف Asynchronous Stream هست. برای تعریف متدی که استریم های Asynchronous رو برمیگردونه سه کار باید انجام بدید:

  1. متد باید به صورت async تعریف بشه
  2. مقدار بازگشتی متد باید IAsyncEnumerable باشه
  3. داخل متد باید از yield return استفاده کنید

همچنین زمان استفاده از asynchronous stream باید از عبارت await foreach استفاده کنید. یک مثال ساده می زنیم. متد زیر اعداد 1 تا 100 رو برای ما بر میگردونه:

public static async IAsyncEnumerable<int> CreateList()
{
    for (int counter = 1; counter <= 100; counter++)
    {
        await Task.Delay(100);
        yield return counter;
    }
}

همونطور که شرایط گفته شده در بالا داخل این متد رعایت شده. برای استفاده از این متد به صورت زیر عمل می کنیم:

static async Task Main(string[] args)
{
    await foreach (var number in CreateList())
    {
        Console.WriteLine(number);
    }
}

Indice ها و Range ها

این قابلیت هم خیلی جالبه و کاربردهای زیادی داره. برای استفاده از این قابلیت یک کلاس و یک Struct جدید در دات نت اضافه شده:

  1. System.Index: نمایانگر یک اندیس در لیست هست. علامت مورد استفاده ^ هست که بر اساس عدد انتخابی آیتمی از لیست رو برای برمیگردونه.
  2. System.Range: می توان بوسیله این کلاس متغیری از نوع range تعریف کرد

با فرض اینکه یک آرایه با نام names داریم:

اندیس [0^]names برابر با names[names.Length] هست یا اندیس [1^]names برابر با names[names.Length-1] هست. به طور کل مقداری که بعد از علامت ^ میاد، نشون دهنده n-1 در لیست هست.

لیست زیر رو در نظر بگیرید:

var names = new[]
{
    "Hosein",
    "Ali",
    "Mohammad",
    "Reza",
    "Mehdi",
    "Hassan",
    "Pejman",
    "Akbar",
    "Abbas"
};

حالات مختلف برای استخراج آیتم های لیست رو بررسی می کنیم:

1. استخراج آخرین آیتم از لیست:

Console.WriteLine(names[^1]);

2. استخراج آیتم های 1 تا 3:

foreach (var name in names[1..4])
{
    Console.WriteLine(name);
}

نکته ای که باید بهش توجه کنید این هست که در کد بالا مقدار [4]names شامل آیتم های خروجی نمیشه.

3. استخراج سه آیتم آخر لیست:

foreach (var name in names[^3..^0])
{
    Console.WriteLine(name);
}

کد بالا رو میشه به صورت زیر هم نوشت:

foreach (var name in names[^3..])
{
    Console.WriteLine(name);
}

نوشتن 0^ اختیاری هست.

اما کاربر Range چیست، با مثال زیر متوجه کاربرد این کلاس می شوید:

Range range = ^3..^1;

foreach (var name in names[range])
{
    Console.WriteLine(name);
}

همانطور که می بینید بوسیله Range می توانید متغیری تعریف کرد که شامل محدوده اندیس می باشد.

عملگر انتساب Null-Coalescing

بوسیله این عملگر می توانید عملیات مقدار دهی متغیر را تنها در صورتی انجام داد که مقدارش Null باشد:

string firstname = "Hossein";
string lastname = null;

firstname ??= "Mohammad";
lastname ??= "Ahmadi";

Console.WriteLine($"{firstname} {lastname}");

در کد بالا تنها مقدار lastname تغییر می کند، زیرا مقدار اولیه آن Null است، اما مقدار firstname به دلیل اینکه مقدارش Null نیست، تغییر نمی کند.


حسین احمدی
حسین احمدی

بنیانگذار توسینسو و برنامه نویس و توسعه دهنده ارشد وب

حسین احمدی ، بنیانگذار TOSINSO ، توسعه دهنده وب و برنامه نویس ، بیش از 12 سال سابقه فعالیت حرفه ای در سطح کلان ، مشاور ، مدیر پروژه و مدرس نهادهای مالی و اعتباری ، تخصص در پلتفرم دات نت و زبان سی شارپ ، طراحی و توسعه وب ، امنیت نرم افزار ، تحلیل سیستم های اطلاعاتی و داده کاوی ...

18 آبان 1398 این مطلب را ارسال کرده

نظرات