Tentang item baru di .NET 5 dan C # 9.0

Selamat sore.



Kami telah menggunakan .NET sejak awal. Kami memiliki solusi yang ditulis dalam semua versi kerangka kerja yang diproduksi: dari yang pertama hingga yang terbaru .NET Core 3.1.



Sejarah .NET, yang selama ini kami ikuti dengan cermat, terjadi di depan mata kami: versi .NET 5, yang rencananya akan dirilis pada November, baru saja dirilis dalam bentuk Release Candidate 2. Kami telah lama diperingatkan bahwa versi kelima akan menjadi pembuatan zaman: akan berakhir begitu saja Skizofrenia .NET, ketika ada dua cabang kerangka kerja: klasik dan inti. Sekarang mereka akan bergabung dalam ekstasi, dan akan ada satu .NET berkelanjutan. RC2



dirilisAnda sudah dapat mulai menggunakannya sepenuhnya - tidak ada perubahan baru yang diharapkan sebelum rilis, hanya akan ada perbaikan bug yang ditemukan. Selain itu: RC2 sudah memiliki situs web resmi yang didedikasikan untuk .NET.



Dan kami menyajikan kepada Anda gambaran umum inovasi di .NET 5 dan C # 9. Semua informasi dengan contoh kode diambil dari blog resmi pengembang platform .NET (serta dari banyak sumber lain) dan diperiksa secara pribadi.



Jenis asli dan baru saja



C # dan .NET secara bersamaan menambahkan jenis asli:



  • nint dan nuint untuk C #
  • System.IntPtr dan System.UIntPtr yang sesuai di BCL


Inti dari menambahkan jenis ini adalah operasi dengan API tingkat rendah. Dan triknya adalah bahwa ukuran sebenarnya dari jenis ini sudah ditentukan pada saat runtime dan bergantung pada bitness sistem: untuk 32-bit, ukurannya adalah 4 byte, dan untuk 64-bit, masing-masing, 8 byte.



Kemungkinan besar, Anda tidak akan menemukan jenis ini dalam pekerjaan nyata. Seperti, bagaimanapun, dengan tipe baru lainnya: Half. Jenis ini hanya ada di BCL, belum ada analog untuk itu di C #. Ini adalah tipe 16-bit untuk nilai floating point. Ini bisa berguna untuk kasus-kasus ketika presisi yang luar biasa tidak diperlukan, dan Anda dapat memenangkan beberapa memori untuk menyimpan nilai, karena tipe float dan double menempati 4 dan 8 byte. Hal yang paling menarik adalah untuk tipe ini secara umum sejauh inioperasi aritmatika tidak ditentukan, dan Anda bahkan tidak dapat menambahkan dua variabel bertipe Half tanpa secara eksplisit mengubahnya menjadi float atau double. Artinya, tujuan dari jenis ini sekarang hanya untuk kegunaan - untuk menghemat ruang. Namun, mereka berencana untuk menambahkan aritmatika ke dalamnya di rilis berikutnya .NET dan C #. Dalam setahun.



Atribut untuk fungsi lokal



Sebelumnya, mereka dilarang, dan ini menimbulkan ketidaknyamanan. Secara khusus, tidak mungkin untuk menggantungkan atribut dari parameter fungsi lokal. Sekarang Anda dapat menyetel atribut untuk mereka, baik untuk fungsi itu sendiri maupun untuk parameternya. Misalnya seperti ini:



#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;
    }
}


Ekspresi lambda statis



Inti dari fitur ini adalah untuk memastikan bahwa ekspresi lambda tidak dapat menangkap konteks apa pun dan variabel lokal yang ada di luar ekspresi itu sendiri. Secara umum, fakta bahwa mereka dapat menangkap konteks lokal seringkali berguna dalam pembangunan. Tapi terkadang ini bisa menjadi penyebab kesalahan yang sulit ditangkap.



Untuk menghindari kesalahan seperti itu, ekspresi lambda sekarang dapat ditandai dengan kata kunci statis. Dan dalam kasus ini, mereka kehilangan akses ke setiap konteks lokal: dari variabel lokal ke ini dan basis.



Berikut contoh penggunaan yang cukup komprehensif:



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
}


Perhatikan bahwa konstanta menangkap lambda statis dengan baik.



GetEnumerator sebagai Metode Ekstensi



Sekarang metode GetEnumerator dapat menjadi metode ekstensi, yang akan memungkinkan Anda untuk mengulang melalui foreach bahkan yang tidak dapat diulangi sebelumnya. Misalnya - tupel.



Berikut adalah contoh ketika dimungkinkan untuk mengulangi ValueTuple melalui foreach menggunakan metode ekstensi yang ditulis untuknya:



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);
        }
    }
}


Kode ini mencetak angka dari 1 hingga 5 ke konsol.



Buang pola dalam parameter ekspresi lambda dan fungsi anonim



Perbaikan mikro. Jika Anda tidak memerlukan parameter dalam ekspresi lambda atau dalam fungsi anonim, Anda dapat menggantinya dengan garis bawah, sehingga mengabaikan:



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;};


Pernyataan tingkat atas di C #



Ini adalah struktur kode C # yang disederhanakan. Sekarang, menulis kode paling sederhana benar-benar terlihat sederhana:



using System;

Console.WriteLine("Hello World!");


Dan semuanya akan dikompilasi dengan baik. Artinya, sekarang Anda tidak perlu membuat metode di mana pernyataan keluaran konsol harus ditempatkan, Anda tidak perlu mendeskripsikan kelas apa pun tempat metode harus ditempatkan, dan tidak perlu menentukan ruang nama tempat kelas harus dibuat.



Ngomong-ngomong, di masa depan, pengembang C # berpikir untuk mengembangkan tema dengan sintaks yang disederhanakan dan mencoba untuk menyingkirkan penggunaan Sistem; dalam kasus yang jelas. Sementara itu, Anda bisa menghilangkannya hanya dengan menulis seperti ini:



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


Dan itu benar-benar akan menjadi program kerja satu baris.



Opsi yang lebih kompleks dapat digunakan:



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();
    }
}


Pada kenyataannya, kompilator itu sendiri akan membungkus semua kode ini dalam ruang nama dan kelas yang diperlukan, Anda tidak akan mengetahuinya.



Tentu saja fitur ini memiliki keterbatasan. Yang utama adalah ini hanya dapat dilakukan dalam satu file proyek. Sebagai aturan, masuk akal untuk melakukan ini di file tempat Anda sebelumnya membuat titik masuk ke program dalam bentuk fungsi Utama (string [] args). Pada saat yang sama, fungsi Utama itu sendiri tidak dapat didefinisikan di sana - ini adalah batasan kedua. Sebenarnya, file seperti itu sendiri dengan sintaks yang disederhanakan adalah fungsi Main, dan itu bahkan berisi variabel args secara implisit, yang merupakan array dengan parameter. Artinya, kode ini juga akan mengkompilasi dan menampilkan panjang array:



System.Console.WriteLine(args.Length);


Secara umum, fitur bukanlah yang terpenting, tetapi untuk tujuan demonstrasi dan pelatihan, fitur ini cukup cocok untuk dirinya sendiri. Detailnya di sini .



Pencocokan pola dalam pernyataan if



Bayangkan Anda perlu memeriksa variabel objek yang bukan dari tipe tertentu. Sampai sekarang, perlu menulis seperti ini:



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


Tetapi dengan C # 9.0, Anda dapat menulis secara manusiawi:



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


Ini juga menjadi mungkin untuk secara kompak merekam beberapa cek:



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


Notasi baru ini setara dengan notasi lama yang baik seperti ini:



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


Atau Anda juga bisa menulis hal yang sama dengan cara yang relatif baru (tapi ini sudah kemarin):



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


Dalam sintaks baru, Anda juga dapat menggunakan operator boolean dan, atau dan tidak, plus, tanda kurung untuk memprioritaskan:



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


Dan ini hanya peningkatan pada pencocokan pola jika. Apa yang kami tambahkan ke pencocokan pola untuk ekspresi sakelar - baca terus.



Pencocokan pola yang ditingkatkan dalam ekspresi sakelar



Ekspresi switch (jangan disamakan dengan pernyataan switch) memiliki peningkatan besar dalam pencocokan pola. Mari kita lihat contoh dari dokumentasi resmi . Contoh dikhususkan untuk menghitung tarif angkutan tertentu pada waktu tertentu. Inilah contoh pertama:



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))
};


Dua baris terakhir dalam pernyataan sakelar adalah baru. Tanda kurung kurawal mewakili objek apa pun yang bukan nol. Dan sekarang Anda dapat menggunakan kata kunci yang cocok untuk mencocokkan dengan nol.



Ini belum semuanya. Perhatikan bahwa untuk setiap pemetaan ke suatu objek, Anda harus membuat variabel: c untuk Mobil, t untuk Taksi, dan seterusnya. Tetapi variabel-variabel ini tidak digunakan. Dalam kasus seperti itu, Anda sudah dapat menggunakan pola buang di C # 8.0 sekarang:



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


Tetapi mulai dengan versi kesembilan dari C #, Anda tidak dapat menulis sama sekali dalam kasus seperti itu:



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


Perbaikan pada ekspresi sakelar tidak berhenti di situ. Sekarang lebih mudah untuk menulis ekspresi yang lebih kompleks. Misalnya, seringkali hasil yang dikembalikan harus bergantung pada nilai properti dari objek yang diteruskan. Sekarang ini bisa ditulis lebih pendek dan lebih nyaman daripada kombinasi 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,
    // ...
};


Perhatikan tiga baris pertama di sakelar: pada kenyataannya, nilai properti Penumpang dicentang, dan dalam kasus persamaan, hasil yang sesuai dikembalikan. Jika tidak ada kecocokan, maka nilai untuk varian umum akan dikembalikan (baris keempat di dalam sakelar). Ngomong-ngomong, nilai properti diperiksa hanya jika objek kendaraan yang lewat tidak null dan merupakan turunan dari kelas Car. Artinya, Anda tidak perlu takut dengan Pengecualian Referensi Null saat memeriksa.



Tapi itu belum semuanya. Sekarang, dalam ekspresi sakelar, Anda bahkan dapat menulis ekspresi untuk pencocokan yang lebih nyaman:



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,
    // ...
};


Dan itu belum semuanya. Sintaks ekspresi sakelar telah diperluas ke ekspresi sakelar bertingkat agar lebih mudah bagi kami untuk mendeskripsikan kondisi kompleks:



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
    },
    // ...
};


Akibatnya, jika Anda benar-benar merekatkan semua contoh kode yang sudah diberikan, Anda mendapatkan gambar berikut dengan semua inovasi yang dijelaskan sekaligus:



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))
};


Tapi itu belum semuanya. Berikut contoh lain: fungsi biasa yang menggunakan mekanisme ekspresi sakelar untuk menentukan beban berdasarkan waktu yang berlalu: jam sibuk pagi / sore, periode siang dan malam:



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,
    };


Seperti yang Anda lihat, di C # 9.0 juga memungkinkan untuk menggunakan operator perbandingan <,>, <=,> =, serta operator logika dan, atau dan tidak, saat mencocokkan.



Tapi ini, sial, bukanlah akhir. Sekarang Anda dapat menggunakan ... tupel dalam ekspresi sakelar. Berikut adalah contoh lengkap kode yang menghitung koefisien tertentu untuk tarif, tergantung pada hari, waktu, dan arah perjalanan (ke / dari kota):



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,
};


Metode PeakTimePremiumFull menggunakan tupel untuk pencocokan, dan ini menjadi mungkin dalam versi baru C # 9.0. Ngomong-ngomong, jika Anda melihat lebih dekat kodenya, maka dua pengoptimalan menyarankan dirinya sendiri:



  • delapan baris terakhir mengembalikan nilai yang sama;
  • lalu lintas siang dan malam memiliki koefisien yang sama.


Hasilnya, kode metode bisa sangat dikurangi menggunakan pola buang:



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,
};


Nah, jika Anda melihat lebih dekat lagi, maka Anda dapat mengurangi opsi ini, dengan menggunakan koefisien 1.0 dalam kasus umum:



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,
};


Untuk berjaga-jaga, izinkan saya mengklarifikasi: perbandingan dibuat sesuai urutan daftar mereka. Pada pertandingan pertama, nilai terkait dikembalikan dan tidak ada perbandingan lebih lanjut yang dibuat.

Perbarui



Tupel dalam ekspresi sakelar dapat digunakan di C # 8.0 juga. Pengembang tidak berharga yang menulis artikel ini menjadi sedikit lebih pintar.





Dan terakhir, inilah contoh gila lain yang mendemonstrasikan sintaks baru untuk mencocokkan dengan tupel dan properti objek:



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,
};


Ini semua terlihat agak tidak biasa. Untuk pemahaman lengkap, saya sarankan Anda melihat sumbernya , ada contoh kode lengkap.



Baru dan pada dasarnya meningkatkan pengetikan target



Dahulu kala di C # menjadi mungkin untuk menulis var daripada nama tipe, karena tipe itu sendiri dapat ditentukan dari konteks (sebenarnya, ini disebut pengetikan target). Artinya, sebagai ganti entri berikut:



SomeLongNamedType variable = new SomeLongNamedType();


menjadi mungkin untuk menulis lebih kompak:



var variable = new SomeLongNamedType()


Dan kompilator akan menebak jenis variabel itu sendiri. Selama bertahun-tahun, sintaks terbalik diterapkan:



SomeLongNamedType variable = new ();


Terima kasih khusus untuk fakta bahwa sintaks ini berfungsi tidak hanya saat mendeklarasikan variabel, tetapi juga dalam banyak kasus lain di mana kompilator dapat langsung menebak jenisnya. Misalnya, saat meneruskan parameter ke suatu metode dan mengembalikan nilai dari metode:



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

//...

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

    return new() { Passengers = 2 };
}


Dalam contoh ini, saat memanggil SomeMethod, parameter tipe DateTime dibuat menggunakan sintaks singkat. Nilai yang dikembalikan dari metode dibuat dengan cara yang sama.



Di mana benar-benar akan ada manfaat untuk sintaks ini adalah saat menentukan koleksi:



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}
};


Tidak adanya kebutuhan untuk menulis nama tipe lengkap saat membuat daftar elemen koleksi membuat kode sedikit lebih bersih.



Targetkan operator yang diketik ?? dan?:



Operator terner ?: ditingkatkan pada C # 9.0. Sebelumnya, diperlukan kepatuhan penuh pada tipe pengembalian, tetapi sekarang lebih cerdas. Berikut adalah contoh ekspresi yang tidak valid di versi bahasa sebelumnya, tetapi cukup legal di versi kesembilan:



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


Sebelumnya, diperlukan untuk secara eksplisit cast dari nol ke int? .. Sekarang tidak perlu.



Selain itu, dalam versi baru bahasa tersebut, diperbolehkan menggunakan konstruksi berikut:



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


Jenis pelanggan dan siswa, meskipun berasal dari Orang, secara teknis berbeda. Versi bahasa sebelumnya tidak mengizinkan Anda menggunakan konstruksi seperti itu tanpa transmisi tipe eksplisit. Sekarang kompilator memahami dengan baik apa yang dimaksud.



Mengganti metode tipe kembalian



Di C # 9.0, itu diizinkan untuk mengganti tipe kembalian dari metode yang diganti. Hanya ada satu persyaratan: tipe baru harus diwarisi dari aslinya (kovarian). Berikut contohnya:



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


Di kelas Tiger, nilai kembali dari metode GetFood telah didefinisikan ulang dari Food to Meat. Sekarang tidak masalah jika Daging berasal dari Makanan.



properti init sebenarnya bukan anggota yang hanya bisa dibaca



Sebuah fitur menarik telah muncul di versi baru bahasa: properti-init. Ini adalah properti yang hanya dapat disetel selama inisialisasi awal objek. Tampaknya ada anggota kelas yang hanya membaca untuk ini, tetapi sebenarnya mereka adalah hal berbeda yang memungkinkan Anda memecahkan masalah yang berbeda. Untuk memahami perbedaan dan keindahan properti init, berikut contohnya:



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


Sintaksis untuk mendeklarasikan instance kelas ini sangat mudah, terutama jika terdapat lebih banyak objek di antara properti kelas. Tetapi sintaks ini memiliki batasan: properti kelas yang sesuai harus dapat berubah . Ini karena inisialisasi properti ini terjadi setelah panggilan ke konstruktor. Artinya, kelas Person dari contoh harus dideklarasikan seperti ini:



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


Namun, pada kenyataannya, properti Name tidak dapat diubah. Saat ini, satu-satunya cara untuk membuat properti hanya-baca ini adalah dengan mendeklarasikan penyetel pribadi:



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


Namun dalam kasus ini, kami segera kehilangan kemampuan untuk menggunakan sintaks yang mudah digunakan untuk mendeklarasikan instance kelas dengan menetapkan nilai ke properti di dalam tanda kurung kurawal. Dan kita dapat menyetel nilai properti Name hanya dengan meneruskannya dalam parameter ke konstruktor kelas. Sekarang bayangkan properti CompanyAddress juga maknanya tidak berubah. Secara umum, saya menemukan diri saya dalam situasi seperti itu berkali-kali, dan saya selalu harus memilih di antara dua kejahatan:



  • konstruktor mewah dengan banyak parameter, tetapi semua properti kelas hanya-baca;
  • sintaks yang nyaman untuk membuat objek, tetapi semua properti kelas baca-tulis, dan saya harus mengingat ini dan tidak sengaja mengubahnya di suatu tempat.


Pada titik ini, seseorang mungkin mengingat anggota kelas yang hanya membaca dan menyarankan gaya kelas Person seperti ini:



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


Yang saya jawab bahwa metode ini tidak hanya tidak sesuai dengan Feng Shui, tetapi juga tidak memecahkan masalah inisialisasi yang nyaman: anggota readonly juga dapat diatur hanya dalam konstruktor, seperti properti dengan penyetel pribadi.



Tetapi di C # 9.0 masalah ini terpecahkan: jika Anda mendefinisikan properti sebagai properti init, Anda mendapatkan sintaks yang sesuai untuk membuat objek, dan properti yang sebenarnya tidak dapat diubah di masa mendatang:



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


Omong-omong, di init-properties, seperti pada konstruktor, Anda dapat menginisialisasi anggota kelas hanya-baca, dan Anda dapat menulis seperti ini:



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


Record adalah DTO yang dilegalkan



Melanjutkan topik tentang properti yang tidak dapat diubah, kita sampai pada, menurut pendapat saya, inovasi utama bahasa: jenis rekaman. Jenis ini dirancang untuk dengan mudah membuat seluruh struktur yang tidak dapat diubah, bukan hanya properti. Alasan munculnya tipe terpisah sederhana: bekerja sesuai dengan semua kanon, kami terus-menerus membuat DTO untuk mengisolasi berbagai lapisan aplikasi. DTO biasanya hanya kumpulan bidang, tanpa logika bisnis apa pun. Dan, sebagai aturan, nilai bidang ini tidak berubah selama masa DTO ini.



.



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



β€” DTO-, - -, .



DTO- - . DTO-, AutoMapper - .



, DTO- .



Jadi, setelah bertahun-tahun, pengembang C # akhirnya mencapai perbaikan yang benar-benar dibutuhkan: mereka melegalkan model DTO sebagai tipe rekaman terpisah.



Sampai saat ini, semua model DTO yang kami buat (dan kami buat dalam jumlah besar) adalah kelas biasa. Untuk kompiler dan runtime, mereka tidak berbeda dari semua kelas lainnya, meskipun dalam pengertian klasiknya tidak begitu. Hanya sedikit orang yang menggunakan struktur untuk model DTO - ini tidak selalu dapat diterima karena berbagai alasan.



Sekarang kita dapat mendefinisikan record (selanjutnya disebut record) - struktur khusus yang dirancang untuk membuat model DTO yang tidak dapat diubah. Perekaman mengambil tempat perantara antara struktur dan kelas dalam pengertiannya yang biasa. Ini adalah subclass dan suprastruktur. Rekaman masih merupakan tipe referensi dengan semua konsekuensi selanjutnya. Rekaman hampir selalu berperilaku seperti kelas biasa, mereka dapat berisi metode, mereka mengizinkan pewarisan (tetapi hanya dari catatan lain, bukan dari objek, meskipun jika catatan tidak secara eksplisit mewarisi dari apa pun, maka ia mewarisi dari objek secara implisit seperti semua yang ada di C # ) dapat mengimplementasikan antarmuka. Selain itu, Anda tidak perlu membuat rekaman sama sekali tidak dapat diubah. Lalu di manakah artinya dan apa perbedaannya?



Mari kita buat entri:



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

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


Dan sekarang inilah contoh cara menggunakannya:



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

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


Contoh ini akan dicetak sesuai dengan konsol. Jika Person adalah sebuah kelas, maka false akan dicetak ke konsol karena objek dibandingkan dengan referensi: dua variabel referensi sama hanya jika mereka merujuk ke objek yang sama. Tapi tidak demikian halnya dengan rekaman. Rekaman dibandingkan dengan nilai semua bidangnya, termasuk yang pribadi.



Melanjutkan contoh sebelumnya, mari kita lihat kode ini:



System.Console.WriteLine(p1);


Dalam kasus kelas, kami akan menerima nama lengkap kelas tersebut di konsol. Tetapi dalam kasus entri, kita akan melihat ini di konsol:



Person { LastName = McCartney, FirstName = Paul}


Faktanya adalah bahwa untuk record, metode ToString () secara implisit diganti dan tidak menampilkan nama tipe, tetapi daftar lengkap bidang publik dengan nilai. Demikian pula, untuk rekaman, == dan! = Operator secara implisit didefinisikan ulang, yang memungkinkan untuk mengubah logika perbandingan.



Mari bermain dengan warisan rekor:



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

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


Sekarang mari kita buat dua posting dari jenis yang berbeda dan bandingkan mereka:



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

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


Meskipun rekaman Guru diwarisi dari Orang, variabel p dan t tidak akan sama, salah akan dicetak ke konsol. Ini karena perbandingan dibuat tidak hanya untuk semua bidang catatan, tetapi juga untuk jenis, dan jenis di sini jelas berbeda.



Dan meskipun membandingkan jenis data yang diwariskan diperbolehkan (tapi tidak ada gunanya), membandingkan jenis catatan yang berbeda secara umum tidak diperbolehkan pada prinsipnya:



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);    //  


Entri tampaknya sama, tetapi akan ada kesalahan kompilasi di baris terakhir. Anda hanya dapat membandingkan rekaman dengan tipe yang sama atau tipe yang diwariskan.



Fitur bagus lainnya dari catatan adalah kata kunci with, yang memudahkan untuk membuat modifikasi pada model DTO Anda. Lihat contoh:



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


Dalam contoh ini, untuk catatan saudara, nilai dari semua bidang akan diisi dari catatan saya, kecuali untuk bidang NamaDepan - itu akan diubah menjadi Paul.



Sejauh ini, Anda telah melihat cara klasik membuat record - dengan definisi lengkap tentang konstruktor, properti, dan sebagainya. Tapi sekarang ada juga cara singkat:



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);


Anda dapat mendefinisikan record secara singkat, dan compiler akan membuat properti dan konstruktor untuk Anda. Namun, fitur ini memiliki fitur tambahan - Anda tidak hanya dapat menggunakan notasi singkatan untuk mendefinisikan properti dan konstruktor, tetapi pada saat yang sama Anda dapat menambahkan metode Anda sendiri ke entri:



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";
    }
}


Dalam hal ini, properti dan konstruktor record juga akan dibuat secara otomatis. Semakin sedikit kode boilerplate, tetapi hanya berlaku untuk postingan. Ini tidak berfungsi untuk kelas dan struktur.



Selain semua yang sudah dikatakan, compiler juga dapat secara otomatis membuat dekonstruktor untuk record:



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

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


Namun, di tingkat IL, record tetaplah sebuah kelas. Namun, ada satu kecurigaan yang belum ada konfirmasinya: yang pasti, pada tingkat runtime, record akan dioptimalkan secara liar di suatu tempat. Kemungkinan besar, karena fakta bahwa akan diketahui sebelumnya bahwa rekaman tertentu tidak dapat diubah. Ini membuka peluang untuk pengoptimalan, setidaknya dalam lingkungan multi-utas, dan pengembang bahkan tidak perlu melakukan upaya khusus untuk ini.



Sementara itu, kami menulis ulang semua model DTO dari kelas ke rekaman.



Generator Sumber .NET



Source Generator (selanjutnya disebut generator) adalah fitur yang cukup menarik. Generator adalah potongan kode yang dijalankan pada tahap kompilasi, memiliki kemampuan untuk menganalisis kode yang sudah dikompilasi, dan dapat menghasilkan kode tambahan yang juga akan dikompilasi. Jika tidak sepenuhnya jelas, maka berikut adalah salah satu contoh yang cukup relevan ketika generator dapat diminati.



Bayangkan aplikasi web C # / .NET yang Anda tulis di ASP.NET Core. Saat Anda meluncurkan aplikasi ini, ada sejumlah besar pekerjaan latar belakang inisialisasi untuk menganalisis dari apa aplikasi ini dibuat dan apa yang harus dilakukannya. Refleksi digunakan dengan panik. Akibatnya, waktu dari peluncuran aplikasi hingga permulaan pemrosesan permintaan pertama bisa sangat lama, yang tidak dapat diterima dalam layanan yang sangat padat. Generator dapat membantu mengurangi waktu ini: bahkan pada tahap kompilasi, ia dapat menganalisis aplikasi Anda yang sudah dikompilasi dan selain itu menghasilkan kode yang diperlukan yang akan menginisialisasi lebih cepat saat startup.



Ada juga sejumlah besar pustaka yang menggunakan refleksi untuk menentukan pada saat runtime jenis objek yang digunakan (di antaranya ada banyak paket Nuget teratas). Ini membuka cakupan besar untuk pengoptimalan menggunakan generator, dan penulis fitur ini mengharapkan peningkatan yang sesuai dari pengembang perpustakaan.



Generator kode adalah topik baru dan terlalu tidak biasa untuk dimasukkan ke dalam cakupan posting ini. Selain itu, Anda dapat melihat contoh yang paling sederhana "Halo, dunia!" generator dalam ulasan ini .



Ada dua fitur baru yang terkait dengan generator kode, yang dijelaskan di bawah ini.



Metode parsial



Kelas parsial dalam C # telah ada sejak lama, tujuan awalnya adalah untuk memisahkan kode yang dihasilkan oleh desainer tertentu dari kode yang ditulis oleh programmer. Metode parsial telah disesuaikan dalam C # 9.0. Mereka terlihat seperti ini:



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;
    }
}


Contoh pengganti ini menunjukkan bahwa metode parsial, pada kenyataannya, tidak berbeda dari yang biasa: mereka dapat mengembalikan nilai, mereka dapat menerima variabel-luar, mereka dapat memiliki pengubah akses.



Dari informasi yang tersedia, metode parsial akan terkait erat dengan generator kode, di mana mereka dimaksudkan untuk digunakan.



Penginisialisasi modul



Ada tiga alasan untuk memperkenalkan fungsi ini:



  • Izinkan pustaka memiliki semacam inisialisasi satu kali saat boot dengan overhead minimal dan tidak perlu secara eksplisit bagi pengguna untuk memanggil apa pun;
  • fungsionalitas yang ada dari konstruktor statis sangat tidak cocok untuk peran ini, karena raintime pertama-tama harus mengetahui: apakah kelas dengan konstruktor statis digunakan sama sekali (ini adalah aturannya), dan ini memberikan penundaan yang dapat diukur;
  • pembuat kode harus memiliki semacam logika inisialisasi yang tidak perlu dipanggil secara eksplisit.


Sebenarnya, poin terakhir sepertinya menjadi penentu untuk fitur yang akan disertakan dalam rilis. Hasilnya, kami menemukan atribut baru yang kami butuhkan untuk melapisi metode yaitu inisialisasi:



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


Ada beberapa batasan dalam metode ini:



  • itu harus statis;
  • itu tidak boleh memiliki parameter;
  • itu seharusnya tidak mengembalikan apapun;
  • seharusnya tidak bekerja dengan obat generik;
  • itu harus dapat diakses dari modul yang memuatnya, yaitu:

    • itu harus internal atau publik
    • itu tidak harus menjadi metode lokal


Dan bekerja seperti ini: segera setelah kompilator menemukan semua metode yang ditandai dengan atribut ModuleInitializer, ia menghasilkan kode khusus yang memanggil semuanya. Urutan pemanggilan metode penginisialisasi tidak dapat ditentukan, tetapi akan sama di setiap kompilasi.



Kesimpulan



Setelah mempublikasikan postingan, kami perhatikan bahwa ini lebih dikhususkan untuk berita dalam bahasa C # 9.0 daripada berita tentang .NET itu sendiri. Tapi ternyata bagus.



All Articles