Pertimbangkan kode berikut:
using System;
#nullable enable
namespace Demo
{
public sealed class TestClass
{
public string Test()
{
bool isNull = _test == null;
if (isNull)
return "";
else
return _test; // !!!
}
readonly string _test = "";
}
}
Ketika saya membangun ini, garis ditandai dengan !!!
memberikan peringatan compiler: warning CS8603: Possible null reference return.
.
Saya menemukan ini agak membingungkan, mengingat yang _test
dibaca dan diinisialisasi ke non-null.
Jika saya mengubah kode menjadi yang berikut, peringatan hilang:
public string Test()
{
// bool isNull = _test == null;
if (_test == null)
return "";
else
return _test;
}
Adakah yang bisa menjelaskan perilaku ini?
c#
nullable-reference-types
Matthew Watson
sumber
sumber
The Debug.Assert is irrelevant because that is a runtime check
- Ini adalah relevan karena jika Anda komentar bahwa garis keluar, peringatan itu hilang.Debug.Assert
akan melempar pengecualian jika tes gagal.Debug.Assert
sekarang memiliki anotasi ( src )DoesNotReturnIf(false)
untuk parameter kondisi.Jawaban:
Analisis aliran nullable melacak keadaan nol variabel, tetapi tidak melacak keadaan lainnya, seperti nilai
bool
variabel (seperti diisNull
atas), dan tidak melacak hubungan antara keadaan variabel yang terpisah (misalnyaisNull
dan_test
).Mesin analisis statis yang sebenarnya mungkin akan melakukan hal-hal itu, tetapi juga akan menjadi "heuristik" atau "sewenang-wenang" sampai taraf tertentu: Anda tidak bisa selalu mengatakan aturan yang diikuti, dan aturan itu bahkan mungkin berubah seiring waktu.
Itu bukan sesuatu yang bisa kita lakukan langsung di kompiler C #. Aturan untuk peringatan yang tidak dapat dibatalkan cukup canggih (seperti yang ditunjukkan oleh analisis Jon!), Tetapi itu adalah aturan, dan dapat dipertimbangkan.
Saat kami meluncurkan fitur, rasanya seperti kami sebagian besar mencapai keseimbangan yang tepat, tetapi ada beberapa tempat yang tampil canggung, dan kami akan meninjau kembali untuk C # 9.0.
sumber
Saya dapat membuat perkiraan yang masuk akal tentang apa yang terjadi di sini, tapi itu semua sedikit rumit :) Ini melibatkan keadaan nol dan pelacakan nol yang dijelaskan dalam spesifikasi draf . Pada dasarnya, pada titik di mana kita ingin kembali, kompiler akan memperingatkan jika keadaan ekspresi "mungkin nol" bukannya "bukan nol".
Jawaban ini agak dalam bentuk naratif daripada hanya "inilah kesimpulannya" ... Saya harap itu lebih berguna seperti itu.
Saya akan menyederhanakan contoh sedikit dengan menyingkirkan bidang, dan mempertimbangkan metode dengan salah satu dari dua tanda tangan ini:
Dalam implementasi di bawah ini saya telah memberikan masing-masing metode nomor yang berbeda sehingga saya dapat merujuk pada contoh spesifik dengan jelas. Ini juga memungkinkan semua implementasi untuk hadir dalam program yang sama.
Dalam setiap kasus yang dijelaskan di bawah ini, kami akan melakukan berbagai hal tetapi akhirnya berusaha untuk kembali
text
- jadi keadaan noltext
yang penting.Pengembalian tanpa syarat
Pertama, mari kita coba mengembalikannya secara langsung:
Sejauh ini, sangat sederhana. Status parameter yang dapat dibatalkan pada awal metode adalah "mungkin nol" jika bertipe
string?
dan "tidak nol" jika bertipestring
.Pengembalian bersyarat sederhana
Sekarang mari kita periksa nol dalam
if
kondisi pernyataan itu sendiri. (Saya akan menggunakan operator kondisional, yang saya percaya akan memiliki efek yang sama, tetapi saya ingin tetap lebih benar dengan pertanyaan itu.)Hebat, jadi seperti di dalam
if
pernyataan di mana kondisi itu sendiri memeriksa nullity, keadaan variabel dalam setiap cabangif
pernyataan bisa berbeda: dalamelse
blok, negara adalah "tidak nol" di kedua potongan kode. Jadi khususnya, dalam M3 keadaan berubah dari "mungkin nol" menjadi "tidak nol".Pengembalian bersyarat dengan variabel lokal
Sekarang mari kita coba menaikkan kondisi itu ke variabel lokal:
Baik M5 dan M6 mengeluarkan peringatan. Jadi tidak hanya kita tidak mendapatkan efek positif dari perubahan status dari "mungkin nol" menjadi "tidak nol" di M5 (seperti yang kita lakukan dalam M3) ... kita mendapatkan efek sebaliknya di M6, di mana negara beralih dari " bukan null "menjadi" mungkin null ". Itu benar-benar mengejutkan saya.
Jadi sepertinya kita telah belajar bahwa:
Pengembalian tanpa syarat setelah perbandingan yang diabaikan
Mari kita lihat poin poin kedua, dengan memperkenalkan perbandingan sebelum pengembalian tanpa syarat. (Jadi kami sepenuhnya mengabaikan hasil perbandingan.):
Perhatikan bagaimana M8 terasa seperti itu harus setara dengan M2 - keduanya memiliki parameter tidak-nol yang mereka kembalikan tanpa syarat - tetapi pengenalan perbandingan dengan nol mengubah status dari "tidak nol" menjadi "mungkin nol". Kita bisa mendapatkan bukti lebih lanjut tentang ini dengan mencoba melakukan dereferensi
text
sebelum kondisinya:Perhatikan bagaimana
return
pernyataan itu tidak memiliki peringatan sekarang: negara setelah mengeksekusitext.Length
adalah "tidak nol" (karena jika kita berhasil mengeksekusi ekspresi itu, itu tidak bisa menjadi nol). Jaditext
parameter dimulai sebagai "bukan nol" karena jenisnya, menjadi "mungkin nol" karena perbandingan nol, kemudian menjadi "bukan nol" lagi setelahnyatext2.Length
.Apa perbandingan yang memengaruhi status?
Jadi itu perbandingan
text is null
... apa pengaruh perbandingan serupa? Berikut adalah empat metode lagi, semua dimulai dengan parameter string yang tidak dapat dibatalkan:Jadi meskipun
x is object
sekarang merupakan alternatif yang disarankan untukx != null
, mereka tidak memiliki efek yang sama: hanya perbandingan dengan nol (dengan salah satu dariis
,==
atau!=
) mengubah negara dari "tidak nol" menjadi "mungkin nol".Mengapa mengangkat kondisi itu berpengaruh?
Kembali ke poin pertama kita sebelumnya, mengapa M5 dan M6 tidak memperhitungkan kondisi yang menyebabkan variabel lokal? Ini tidak mengejutkan saya seperti halnya mengejutkan orang lain. Membangun semacam logika ke kompiler dan spesifikasi banyak pekerjaan, dan untuk manfaat yang relatif sedikit. Berikut ini contoh lain yang tidak ada hubungannya dengan nullability di mana inlining memiliki efek:
Meskipun kita tahu itu
alwaysTrue
akan selalu benar, itu tidak memenuhi persyaratan dalam spesifikasi yang membuat kode setelahif
pernyataan tidak terjangkau, yang merupakan apa yang kita butuhkan.Berikut contoh lain, seputar penugasan yang pasti:
Meskipun kita tahu bahwa kode akan memasukkan tepat salah satu dari
if
badan pernyataan itu, tidak ada dalam spesifikasi untuk menyelesaikannya. Alat analisis statis mungkin dapat melakukannya, tetapi mencoba memasukkannya ke dalam spesifikasi bahasa akan menjadi ide yang buruk, IMO - tidak apa-apa jika alat analisis statis memiliki semua jenis heuristik yang dapat berkembang seiring waktu, tetapi tidak begitu banyak untuk spesifikasi bahasa.sumber
if (x != null) x.foo(); x.bar();
kita memiliki dua bukti; yangif
pernyataan bukti dari proposisi "penulis percaya bahwa x bisa menjadi nol sebelum panggilan ke foo" dan pernyataan berikut bukti dari "penulis percaya x tidak null sebelum panggilan ke bar", dan kontradiksi ini mengarah ke kesimpulan bahwa ada bug. Bugnya adalah bug yang relatif jinak dari pemeriksaan nol yang tidak perlu, atau bug yang berpotensi menabrak. Bug mana yang merupakan bug asli tidak jelas, tetapi jelas bahwa ada bug.Anda telah menemukan bukti bahwa algoritma program-flow yang menghasilkan peringatan ini relatif tidak canggih ketika datang untuk melacak makna yang dikodekan dalam variabel lokal.
Saya tidak memiliki pengetahuan khusus tentang implementasi flow checker, tetapi setelah bekerja pada implementasi kode yang sama di masa lalu, saya dapat membuat beberapa tebakan yang berpendidikan. Pemeriksa aliran kemungkinan menyimpulkan dua hal dalam kasus positif palsu: (1)
_test
bisa nol, karena jika tidak bisa, Anda tidak akan memiliki perbandingan di tempat pertama, dan (2)isNull
bisa benar atau salah - karena jika tidak bisa, Anda tidak akan memilikinya dalamif
. Tetapi koneksi yangreturn _test;
hanya berjalan jika_test
bukan nol, koneksi itu tidak dibuat.Ini adalah masalah yang sangat sulit, dan Anda harus berharap bahwa akan membutuhkan waktu bagi penyusun untuk mencapai kecanggihan alat yang telah bertahun-tahun dikerjakan oleh para ahli. Pemeriksa aliran Coverity, misalnya, tidak akan memiliki masalah sama sekali dalam menyimpulkan bahwa tak satu pun dari dua variasi Anda memiliki pengembalian nol, tetapi pemeriksa aliran Coverity biaya uang serius bagi pelanggan korporat.
Selain itu, checker Cakupan dirancang untuk berjalan pada basis kode besar dalam semalam ; analisis C # compiler harus dijalankan di antara penekanan tombol di editor , yang secara signifikan mengubah jenis analisis mendalam yang dapat Anda lakukan secara wajar.
sumber
bool b = x != null
vsbool b = x is { }
(dengan tidak ada penugasan yang benar-benar digunakan!) menunjukkan bahwa bahkan pola yang dikenal untuk pemeriksaan nol dipertanyakan. Bukan untuk meremehkan kerja keras tim yang tak diragukan lagi untuk menjadikan pekerjaan ini sebagaimana mestinya untuk basis kode yang nyata dan sedang digunakan - sepertinya analisisnya adalah pragmatis kapital-P.Semua jawaban lain benar-benar tepat.
Jika ada yang penasaran, saya mencoba mengeja logika kompiler sejelas mungkin di https://github.com/dotnet/roslyn/issues/36927#issuecomment-508595947
Satu hal yang tidak disebutkan adalah bagaimana kita memutuskan apakah cek nol harus dianggap "murni", dalam arti bahwa jika Anda melakukannya, kita harus mempertimbangkan dengan serius apakah nol adalah suatu kemungkinan. Ada banyak cek nol "insidental" di C #, tempat Anda menguji nol sebagai bagian dari melakukan sesuatu yang lain, jadi kami memutuskan bahwa kami ingin mempersempit set cek ke cek yang kami yakin orang melakukannya dengan sengaja. Heuristik yang kami temukan adalah "mengandung kata null", jadi itu sebabnya
x != null
danx is object
menghasilkan hasil yang berbeda.sumber