C # 8 dan validitas nol. Bagaimana kita hidup dengan ini

Halo rekan! Sudah waktunya untuk menyebutkan bahwa kami memiliki rencana untuk merilis buku fundamental Ian Griffiths di C # 8:


Sementara itu, di blog - nya, penulis telah menerbitkan dua artikel terkait di mana ia mempertimbangkan seluk-beluk fenomena baru seperti nullability, null-obliviousness, dan null-awareness. Kami telah menerjemahkan kedua artikel dalam satu tajuk dan menyarankan untuk membahasnya.

Fitur baru yang paling ambisius di C # 8.0 disebut referensi nullable .

Tujuan dari fitur baru ini adalah untuk memperlancar kerusakan dari hal yang berbahaya, yang pernah disebut oleh ilmuwan komputer Tony Hoar sebagai " kesalahan miliaran dolar ." C # memiliki kata kuncinull(padanan yang ditemukan dalam banyak bahasa lain), dan akar kata kunci ini dapat ditelusuri kembali ke bahasa Algol W, dalam perkembangan yang diikuti Hoar. Dalam bahasa kuno ini (muncul pada tahun 1966), variabel yang merujuk pada instance dari jenis tertentu dapat menerima makna khusus, menunjukkan bahwa saat ini variabel ini tidak dirujuk di mana pun. Peluang ini sangat banyak dipinjam, dan saat ini banyak ahli (termasuk Hoar sendiri) percaya bahwa ini telah menjadi sumber kesalahan perangkat lunak yang paling mahal sepanjang masa.

Apa yang salah dengan mengasumsikan nol? Di dunia di mana tautan apa pun bisa mengarah ke nol, Anda harus mempertimbangkan ini di mana pun tautan apa pun digunakan dalam kode Anda, jika tidak, Anda berisiko ditolak saat runtime. Terkadang tidak terlalu membebani; jika Anda menginisialisasi variabel dengan ekspresi newdi tempat yang sama di mana Anda mendeklarasikannya, maka Anda tahu bahwa variabel ini tidak sama dengan nol. Tetapi bahkan contoh sederhana seperti itu penuh dengan beban kognitif: sebelum rilis C # 8, kompiler tidak dapat memberi tahu Anda jika Anda melakukan sesuatu yang dapat mengubah nilai ini menjadi null. Tetapi, segera setelah Anda mulai menjahit fragmen kode yang berbeda, menjadi jauh lebih sulit untuk menilai dengan pasti tentang hal-hal seperti itu: seberapa besar kemungkinan properti ini yang saya baca sekarang dapat kembali null? Apakah diizinkan mengirimnullke dalam metode itu? Dalam situasi apa saya dapat yakin bahwa metode yang saya panggil akan mengatur argumen ini outbukan untuk null, tetapi ke nilai yang berbeda? Selain itu, masalahnya bahkan tidak terbatas pada mengingat untuk memeriksa hal-hal seperti itu; tidak sepenuhnya jelas apa yang harus Anda lakukan jika Anda mencapai nol.

Dengan tipe numerik dalam C # tidak ada masalah seperti itu: jika Anda menulis fungsi yang mengambil beberapa angka sebagai input dan sebagai hasilnya, Anda tidak perlu bertanya-tanya apakah nilai yang ditransmisikan benar-benar angka, dan jika ada di antara mereka yang bisa digabungkan. Saat memanggil fungsi seperti itu, tidak perlu dipikirkan apakah ia dapat mengembalikan sesuatu sebagai ganti nomor. Kecuali jika perkembangan acara semacam itu menarik minat Anda sebagai opsi: dalam hal ini, Anda dapat mendeklarasikan parameter atau hasil dari tipe tersebutint?, menunjukkan bahwa dalam kasus khusus ini Anda benar-benar ingin mengizinkan transmisi atau pengembalian nilai nol. Jadi, untuk tipe numerik dan, dalam arti yang lebih umum, tipe signifikan, toleransi nol selalu menjadi salah satu hal yang dilakukan secara sukarela, sebagai pilihan.

Adapun jenis referensi, sebelum C # 8.0, izin nol tidak hanya ditetapkan secara default, tetapi juga tidak dapat dinonaktifkan.

Bahkan, untuk alasan kompatibilitas ke belakang, validitas nol terus beroperasi secara default bahkan di C # 8.0, karena fungsi bahasa baru di area ini tetap dinonaktifkan hingga Anda secara eksplisit memintanya.

Namun, segera setelah Anda mengaktifkan fitur baru ini - semuanya berubah. Cara termudah untuk mengaktifkannya adalah menambahkannya <Nullablegt;enable</Nullablegt;di dalam elemen.<PropertyGroup>dalam file Anda .csproj. (Saya perhatikan bahwa lebih banyak kontrol kerawang juga tersedia. Jika Anda benar-benar membutuhkannya, Anda dapat mengonfigurasi perilaku yang diizinkan nullsecara terpisah di setiap baris. Namun, ketika kami baru-baru ini memutuskan untuk memasukkan fitur ini di semua proyek kami, ternyata itu akan diaktifkan pada skala satu proyek pada satu waktu adalah tugas yang bisa dilakukan.)

Ketika tautan yang diizinkan dalam C # 8.0 nulldiaktifkan sepenuhnya, situasinya berubah: sekarang, secara default, diasumsikan bahwa tautan tidak mengizinkan nol hanya jika Anda sendiri tidak menentukan yang sebaliknya, persis seperti jenis yang signifikan ( bahkan sintaksnya sama: Anda dapat menulis int ?, jika Anda benar-benar ingin nilai integer menjadi opsional. Sekarang Anda menulis string ?, jika Anda bermaksud bahwa Anda menginginkan referensi string ataunull.)

Ini adalah perubahan yang sangat signifikan, dan, pertama-tama, karena signifikansinya, fitur baru ini dinonaktifkan secara default. Microsoft dapat merancang fitur bahasa ini secara berbeda: Anda dapat membiarkan tautan default tidak dapat dihapus dan memperkenalkan sintaksis baru yang memungkinkan Anda menentukan bahwa Anda ingin memastikan bahwa itu tidak diizinkan null. Mungkin ini akan menurunkan bilah ketika menjelajahi kemungkinan ini, tetapi dalam jangka panjang solusi seperti itu akan salah, karena dalam praktiknya sebagian besar tautan dalam massa besar kode C # tidak dirancang untuk menunjukkan null.

Mengasumsikan nol adalah pengecualian, bukan aturan, dan itulah sebabnya, ketika fitur bahasa baru ini diaktifkan, mencegah nol menjadi standar baru. Ini tercermin bahkan dalam nama fitur asli: "referensi nullable." Namanya penasaran, mengingat bahwa tautan dapat mengarah nullkembali ke C # 1.0. Tetapi para pengembang memilih untuk menekankan bahwa sekarang asumsi nol masuk ke dalam kategori hal-hal yang perlu diminta secara eksplisit.

C # 8.0 memperlancar proses memperkenalkan tautan permisif null, karena memungkinkan Anda untuk memperkenalkan fitur ini secara bertahap. Seseorang tidak harus membuat pilihan ya atau tidak. Ini sangat berbeda dari fitur yang async/awaitditambahkan dalam C # 5.0, yang cenderung menyebar: pada kenyataannya, operasi asinkron mengharuskan pemanggil untukasync, dan oleh karena itu, kode yang memanggil penelepon ini harus async, dan seterusnya, ke paling atas tumpukan. Untungnya, jenis yang memungkinkan nulldibangun secara berbeda: mereka dapat diimplementasikan secara selektif dan bertahap. Anda dapat mengerjakan file satu per satu, atau bahkan baris demi baris, jika perlu.

Aspek yang paling penting dari jenis memungkinkannull(Terima kasih yang transisi ke mereka disederhanakan), adalah bahwa secara default mereka dinonaktifkan. Kalau tidak, sebagian besar pengembang akan menolak untuk menggunakan C # 8.0, karena transisi seperti itu akan menyebabkan peringatan di hampir semua basis kode. Namun, untuk alasan yang sama, ambang entri untuk menggunakan fitur baru ini terasa agak tinggi: jika fitur baru membuat perubahan dramatis sehingga dinonaktifkan secara default, maka Anda mungkin tidak ingin mengacaukannya, tetapi ada masalah yang terkait dengan beralih ke fitur ini. akan selalu tampak tidak perlu repot. Tapi ini akan memalukan, karena fitur ini sangat berharga. Ini membantu untuk menemukan bug dalam kode sebelum pengguna melakukannya untuk Anda.

Jadi, jika Anda mempertimbangkan untuk memperkenalkan jenis yang memungkinkannull, pastikan untuk mencatat bahwa Anda dapat memperkenalkan fitur ini langkah demi langkah.

Hanya peringatan

Level kontrol paling kasar atas seluruh proyek setelah nyala / mati yang sederhana adalah kemampuan untuk mengaktifkan peringatan tanpa memperhatikan anotasi. Misalnya, jika saya sepenuhnya mengaktifkan asumsi nol untuk Corvus.ContentHandling.Json di repositori Corvus.ContentHandling kami , menambah <Nullablegt;enable</Nullablegt;grup properti dalam file proyek, maka dalam keadaan saat ini 20 peringatan dari kompiler akan segera muncul. Namun, jika saya menggunakannya, saya <Nullablegt;warnings</Nullablegt;hanya akan mendapatkan satu peringatan.

Tapi tunggu! Mengapa lebih sedikit peringatan ditampilkan kepada saya? Pada akhirnya, di sini saya hanya meminta peringatan. Jawaban untuk pertanyaan ini tidak sepenuhnya jelas: faktanya adalah bahwa beberapa variabel dan ekspresi dapat menjadi nullnol-lupa.

Null Neutrality

C # mendukung dua interpretasi validitas nol. Pertama, variabel apa pun dari tipe referensi dapat dinyatakan sebagai diterima atau tidak null, dan kedua, kompiler akan nullkapan pun memungkinkan secara logis menyimpulkan apakah variabel ini dapat berada pada titik tertentu dalam kode. Artikel ini hanya membahas jenis pertama yang dapat diterimanull, Yaitu, tentang jenis statis variabel (pada kenyataannya, ini tidak hanya berlaku untuk variabel dan parameter dan bidang yang dekat dengan mereka dalam roh, baik statis dan diterimanya secara logis deducible yang nullditentukan untuk setiap ekspresi di C #.) Bahkan, diterimanya nulldalam arti pertama , yang sedang kami pertimbangkan adalah perpanjangan dari sistem tipe.

Namun, ternyata jika kita hanya fokus pada penerimaan nol untuk suatu jenis, situasinya tidak akan koheren seperti yang diduga. Ini bukan hanya kontras antara "validitas nol" dan "tidak validnull". Bahkan, ada dua kemungkinan lagi. Ada kategori "tidak dikenal", yang wajib karena ketersediaan obat generik; jika Anda memiliki parameter tipe tidak terbatas, maka tidak akan mungkin menemukan apa pun tentang validitasnya null: kode yang menggunakan metode atau tipe umum yang sesuai dapat menggantikan argumen di dalamnya, baik mengizinkan atau tidak mengizinkan null. Anda dapat menambahkan pembatasan, tetapi sering kali pembatasan seperti itu tidak diinginkan, karena mereka mempersempit ruang lingkup jenis atau metode umum. Jadi, untuk variabel atau ekspresi dari beberapa parameter tipe tidak terbatas, Tvaliditas (non) nol harus tidak diketahui; mungkin, dalam setiap kasus, pertanyaan penerimaannullitu akan diputuskan secara terpisah untuk mereka, tetapi kami tidak tahu opsi mana yang akan muncul dalam kode umum, karena itu akan bergantung pada argumen tipe.

Kategori terakhir disebut "netral". Dengan prinsip "netralitas" semuanya berfungsi sebelum C # 8.0, dan itu akan berfungsi jika Anda tidak mengaktifkan kemampuan untuk bekerja dengan tautan yang dapat dibatalkan. (Pada dasarnya, ini adalah contoh dari retroaktif . Meskipun gagasan netralitas nol pertama kali diperkenalkan dalam C # 8.0 sebagai keadaan alami kode sebelum mengaktifkan validitas nol untuk referensi, desainer C # bersikeras bahwa properti ini tidak pernah benar-benar asing bagi C #.)

Mungkin Anda tidak perlu menjelaskan apa arti "netralitas" dalam kasus ini, karena dalam nada inilah C # selalu bekerja, jadi Anda sendiri memahami segalanya ... meskipun, mungkin, ini sedikit tidak jujur. Jadi dengarkan: di dunia di mana diketahui tentang penerimaan null, karakteristik paling penting dari nullekspresi-netral adalah bahwa mereka tidak menyebabkan peringatan tentang penerimaan nol. Anda dapat menetapkan ekspresi nol-netral sebagai nullvariabel yang diizinkan , tetapi tidak diizinkan. Null-variabel netral (serta properti, bidang, dll.), Anda dapat menetapkan ekspresi yang oleh kompiler dianggap "mungkin null" atau "tidak null".

Itu sebabnya, jika Anda hanya mengaktifkan peringatan, maka tidak akan ada banyak peringatan baru. Semua kode tetap dalam konteks anotasi dinonaktifkan yang dinonaktifkan null, sehingga semua variabel, parameter, bidang, dan properti akan nullnetral, dan ini berarti bahwa Anda tidak akan menerima peringatan apa pun jika Anda mencoba menggunakannya bersama dengan entitas yang mempertimbangkan null.

Jadi, mengapa saya mendapat peringatan sama sekali? Alasan umum adalah karena upaya untuk menjalin pertemanan dengan cara yang tidak dapat diterima dua potong kode yang memperhitungkan null. Sebagai contoh, misalkan saya memiliki perpustakaan di mana tautan permisif sepenuhnya disertakan null, dan perpustakaan ini memiliki kelas yang dibuat sangat mendalam sebagai berikut:

public static class NullableAwareClass
	{
	    public static string? GetNullable() => DateTime.Now.Hour > 12 ? null : "morning";
	

	    public static int RequireNonNull(string s) => s.Length;
	}

Selanjutnya, dalam proyek lain, saya dapat menulis kode ini dalam konteks di mana peringatan validitas nol diaktifkan, tetapi anotasi yang sesuai dinonaktifkan:

static int UseString(string x) => NullableAwareClass.RequireNonNull(x);

Karena anotasi tentang validitas nol dinonaktifkan, parameter di xsini nullnetral. Ini berarti bahwa kompilator tidak dapat menentukan apakah kode ini benar atau tidak. Jika penyusun mengeluarkan peringatan dalam kasus di mana nullekspresi netral dicampur dengan yang memperhitungkan null, sebagian besar dari peringatan ini dapat dianggap meragukan - oleh karena itu, peringatan tidak dikeluarkan.

Dengan pembungkus ini, saya sebenarnya menyembunyikan fakta bahwa kode memperhitungkan validitas akun null. Ini berarti bahwa sekarang saya dapat menulis seperti ini:

	int x = UseString(NullableAwareClass.GetNullable());

Kompiler tahu apa yang GetNullablebisa dikembalikan null, tetapi karena saya memanggil metode dengan parameter nol-netral, program tidak tahu apakah ini benar atau salah. Menggunakan nullpembungkus -neutral, saya melucuti kompiler, yang sekarang tidak melihat masalah di sini. Namun, jika saya menggabungkan kedua metode ini secara langsung, semuanya akan berbeda:

int y = NullableAwareClass.RequireNonNull(NullableAwareClass.GetNullable());

Di sini saya menyampaikan hasilnya GetNullablekepada RequireNonNull. Jika saya mencoba melakukan ini dalam konteks di mana peringatan nol diaktifkan, kompiler akan menghasilkan peringatan, terlepas dari apakah saya menghidupkan atau mematikan konteks anotasi yang sesuai. Dalam kasus khusus ini, konteks anotasi tidak masalah, karena tidak ada deklarasi dengan tipe referensi. Jika Anda mengaktifkan peringatan tentang asumsi nol, tetapi menonaktifkan anotasi yang sesuai, maka semua deklarasi akan menjadi nullnetral, yang, bagaimanapun, tidak berarti bahwa semua ekspresi menjadi seperti itu. Jadi, kita tahu bahwa hasilnya GetNullablenol. Karena itu, kami mendapat peringatan.

Meringkas: karena semua deklarasi dalam konteks anotasi dinonaktifkan yang memungkinkan nulladalahnull-neutral, kami tidak akan mendapatkan banyak peringatan, karena sebagian besar ekspresi adalah null-neutral. Tetapi kompiler masih akan dapat menangkap kesalahan yang terkait dengan asumsi nulldalam kasus-kasus tersebut ketika ekspresi tidak melewati beberapa perantara null-netral. Selain itu, manfaat terbesar dalam hal ini adalah dari mendeteksi kesalahan yang terkait dengan upaya untuk melakukan dereferensi nilai null potensial menggunakan ., misalnya:

int z = NullableAwareClass.GetNullable().Length;

Jika kode Anda dirancang dengan baik, maka seharusnya tidak ada banyak kesalahan seperti ini.

Anotasi bertahap dari seluruh proyek

Setelah Anda mengambil langkah pertama - cukup aktifkan peringatan, maka Anda dapat melanjutkan ke aktivasi bertahap anotasi, file per file. Sangat mudah untuk memasukkan mereka segera di seluruh proyek, lihat di mana file peringatan muncul - dan kemudian pilih file di mana ada relatif sedikit peringatan. Sekali lagi, nonaktifkan mereka di tingkat seluruh proyek, dan tulis di bagian atas file yang Anda pilih #nullable enable. Ini akan sepenuhnya mengaktifkan asumsi null(baik untuk peringatan dan untuk anotasi) di seluruh file (kecuali jika Anda mematikannya lagi menggunakan arahan lain#nullable) Lalu, Anda dapat menelusuri seluruh file dan memastikan bahwa semua entitas yang kemungkinan nol diberi anotasi sebagai membolehkan null(mis., Tambahkan ?), lalu berurusan dengan peringatan di file ini, jika ada yang tersisa.

Mungkin ternyata menambahkan semua penjelasan yang diperlukan adalah yang diperlukan untuk menghilangkan semua peringatan. Kebalikannya juga dimungkinkan: Anda mungkin memperhatikan bahwa ketika Anda dengan rapi mencatat satu file tentang validitasnull, peringatan lain muncul di file lain yang menggunakannya. Biasanya, tidak ada banyak peringatan seperti itu, dan Anda punya waktu untuk memperbaikinya dengan cepat. Tetapi, jika karena alasan tertentu setelah langkah ini Anda hanya tenggelam dalam peringatan, maka Anda masih memiliki beberapa solusi. Pertama, Anda dapat membatalkan pilihan, meninggalkan file ini dan mengambil yang lain. Kedua, Anda dapat mematikan anotasi secara selektif untuk anggota yang menurut Anda paling banyak menimbulkan masalah. ( #nullableAnda dapat menggunakan arahan sebanyak yang Anda inginkan, sehingga Anda dapat mengontrol pengaturan validitas nol bahkan baris demi baris, jika Anda mau.) Mungkin jika Anda kembali ke file ini nanti ketika Anda sudah mengaktifkan validitas nol di sebagian besar proyek, Anda akan melihat lebih sedikit peringatan dari yang pertama kali.

Ada saat-saat ketika masalah tidak dapat diselesaikan dengan cara yang begitu mudah. Jadi, dalam skenario tertentu yang terkait dengan serialisasi (misalnya, ketika menggunakan Json.NET atau Entity Framework), pekerjaan mungkin lebih sulit. Saya pikir masalah ini layak mendapat artikel terpisah.

Tautan dengan asumsi nullmeningkatkan ekspresifitas kode Anda dan meningkatkan kemungkinan kompiler akan menangkap kesalahan Anda sebelum pengguna menabraknya. Karena itu, lebih baik untuk memasukkan fitur ini jika memungkinkan. Dan, jika Anda memasukkannya secara selektif, maka manfaatnya akan mulai terasa lebih cepat.

All Articles