Mengapa kita mendapatkan peringatan referensi nol dereferensi yang mungkin, ketika referensi nol sepertinya tidak mungkin?

9

Setelah membaca pertanyaan ini di HNQ, saya melanjutkan membaca tentang Jenis Referensi Nullable di C # 8 , dan membuat beberapa percobaan.

Saya sangat sadar bahwa 9 kali dari 10, atau bahkan lebih sering, ketika seseorang berkata "Saya menemukan bug penyusun!" ini sebenarnya oleh desain, dan kesalahpahaman mereka sendiri. Dan karena saya mulai melihat ke fitur ini hanya hari ini, jelas saya tidak memiliki pemahaman yang sangat baik tentang itu. Dengan ini, mari kita lihat kode ini:

#nullable enable
class Program
{
    static void Main()
    {
        var s = "";
        var b = s == null; // If you comment this line out, the warning on the line below disappears
        var i = s.Length; // warning CS8602: Dereference of a possibly null reference
    }
}

Setelah membaca dokumentasi yang saya tautkan di atas, saya berharap s == nullkalimat itu memberi saya peringatan — setelah semua sjelas tidak dapat dibatalkan, jadi membandingkannya dengan nulltidak masuk akal.

Alih-alih, saya mendapatkan peringatan di baris berikutnya , dan peringatan itu mengatakan bahwa situ mungkin merupakan referensi nol, meskipun, bagi manusia, jelas itu bukan.

Terlebih lagi, peringatan tersebut tidak ditampilkan jika kita tidak membandingkan suntuk null.

Saya melakukan beberapa Googling dan saya mengenai masalah GitHub , yang ternyata sepenuhnya tentang hal lain, tetapi dalam prosesnya saya melakukan percakapan dengan seorang kontributor yang memberikan beberapa wawasan lebih dalam perilaku ini (mis. "Null cheque seringkali merupakan cara yang bermanfaat. memberitahu kompiler untuk mengatur ulang kesimpulan sebelumnya tentang nullability suatu variabel. " ). Ini masih meninggalkan saya dengan pertanyaan utama yang belum terjawab.

Alih-alih menciptakan masalah GitHub baru, dan berpotensi mengambil waktu dari kontributor proyek yang sangat sibuk, saya menempatkan ini untuk komunitas.

Bisakah Anda jelaskan apa yang terjadi dan mengapa? Khususnya, mengapa tidak ada peringatan yang dihasilkan di s == nulltelepon, dan mengapa kita memiliki peringatan yang CS8602sepertinya tidak nullmungkin ada di sini? Jika inferensi nullability tidak tahan peluru, seperti yang disarankan oleh thread GitHub yang tertaut, bagaimana bisa salah? Apa yang akan menjadi contohnya?

Andrew Savinykh
sumber
Tampaknya kompilator itu sendiri menetapkan perilaku bahwa pada titik ini variabel "s" bisa menjadi nol. Lagi pula, jika saya pernah menggunakan string atau objek maka harus selalu ada cek sebelum Anda memanggil suatu fungsi. "s?. Panjang" harus melakukan trik dan peringatan itu sendiri harus menghilang.
chg
1
@chg, seharusnya tidak perlu ?karena stidak dapat dibatalkan. Itu tidak menjadi nullable, hanya karena kami cukup bodoh untuk membandingkannya null.
Andrew Savinykh
Saya mengikuti pertanyaan sebelumnya (maaf, tidak dapat menemukannya) di mana dinyatakan bahwa jika Anda menambahkan cek bahwa nilai adalah nol, maka kompiler menganggapnya sebagai 'petunjuk' bahwa nilainya mungkin nol, bahkan jika itu terbukti tidak demikian.
stuartd
@stuartd, ya, begitulah tampaknya. Jadi sekarang pertanyaannya adalah: mengapa ini berguna?
Andrew Savinykh
1
@chg, well, itu yang saya katakan di badan pertanyaan, bukan?
Andrew Savinykh

Jawaban:

7

Ini secara efektif merupakan duplikat dari jawaban yang dikaitkan @stuartd, jadi saya tidak akan membahas detail yang lebih dalam di sini. Tetapi akar masalahnya adalah bahwa ini bukan bug bahasa atau bug penyusun, tetapi perilaku yang dimaksudkan persis seperti yang diterapkan. Kami melacak status nol variabel. Ketika Anda awalnya mendeklarasikan variabel, status itu adalah NotNull karena Anda secara eksplisit menginisialisasi dengan nilai yang tidak nol. Tapi kami tidak melacak dari mana NotNull itu berasal. Ini, misalnya, adalah kode yang secara efektif setara:

#nullable enable
class Program
{
    static void Main()
    {
        M("");
    }
    static void M(string s)
    {
        var b = s == null;
        var i = s.Length; // warning CS8602: Dereference of a possibly null reference
    }
}

Dalam kedua kasus, Anda secara eksplisit menguji suntuk null. Kami mengambil ini sebagai masukan untuk analisis aliran, seperti yang dijawab Mads dalam pertanyaan ini: https://stackoverflow.com/a/59328672/2672518 . Dalam jawaban itu, hasilnya adalah Anda mendapatkan peringatan saat kembali. Dalam hal ini, jawabannya adalah bahwa Anda mendapatkan peringatan bahwa Anda mendereferensi referensi yang mungkin nol.

Itu tidak menjadi nullable, hanya karena kami cukup bodoh untuk membandingkannya null.

Yap, sebenarnya. Ke kompiler. Sebagai manusia, kita dapat melihat kode ini dan jelas memahami bahwa itu tidak dapat membuang pengecualian referensi nol. Tetapi cara analisis aliran nullable diimplementasikan dalam kompiler, tidak bisa. Kami memang membahas sejumlah peningkatan pada analisis ini di mana kami menambahkan status tambahan berdasarkan dari mana nilai itu berasal, tetapi kami memutuskan bahwa ini menambahkan banyak kerumitan pada implementasi karena bukan banyak keuntungan, karena satu-satunya tempat di mana ini akan berguna untuk kasus-kasus seperti ini, di mana pengguna menginisialisasi variabel dengan newnilai konstan atau dan kemudian memeriksanya null.

Terkadang
sumber
Terima kasih. Ini sebagian besar membahas kesamaan dengan pertanyaan lain, saya ingin membahas lebih banyak perbedaan. Misalnya, mengapa s == nulltidak menghasilkan peringatan?
Andrew Savinykh
Saya juga baru menyadari bahwa dengan #nullable enable; string s = "";s = null;mengkompilasi dan bekerja (masih menghasilkan peringatan) apa manfaatnya, dari implementasi yang memungkinkan penetapan nol ke "referensi yang tidak dapat dibatalkan" dalam konteks anotasi nol yang diaktifkan?
Andrew Savinykh
Jawaban Mad berfokus pada fakta bahwa "[kompiler] tidak melacak hubungan antara keadaan variabel yang terpisah" kami tidak memiliki variabel yang terpisah dalam contoh ini, jadi saya mengalami kesulitan menerapkan sisa jawaban Mad untuk kasus ini.
Andrew Savinykh
Tak usah dikatakan lagi, tetapi ingat, saya di sini bukan untuk mengkritik, tetapi untuk belajar. Saya telah menggunakan C # sejak pertama kali keluar pada tahun 2001. Meskipun saya tidak baru dalam bahasa ini, cara perilakunya sangat mengejutkan bagi saya. Tujuan dari pertanyaan ini adalah untuk memahami alasan mengapa perilaku ini bermanfaat bagi manusia.
Andrew Savinykh
Ada alasan yang sah untuk memeriksa s == null. Mungkin, misalnya, Anda menggunakan metode publik, dan Anda ingin melakukan validasi parameter. Atau, mungkin Anda menggunakan pustaka yang beranotasi tidak benar, dan sampai mereka memperbaiki bug itu Anda harus berurusan dengan nol di mana tidak dinyatakan. Dalam salah satu dari kasus-kasus itu, jika kami memperingatkan, itu akan menjadi pengalaman yang buruk. Sedangkan untuk memungkinkan penugasan: anotasi variabel lokal hanya untuk membaca. Mereka tidak memengaruhi runtime sama sekali. Faktanya, kami menempatkan semua peringatan itu dalam satu kode kesalahan sehingga Anda dapat mematikannya jika Anda ingin mengurangi churn kode.
333
0

Jika inferensi nullability tidak tahan peluru, [..] bagaimana bisa salah?

Saya dengan senang hati mengadopsi referensi nullable dari C # 8 segera setelah tersedia. Karena saya terbiasa menggunakan notasi [NotNull] (dll) ReSharper, saya memang melihat beberapa perbedaan antara keduanya.

Kompiler C # dapat dikelabui, tetapi cenderung salah di sisi hati-hati (biasanya, tidak selalu).

Sebagai referensi untuk pengunjung masa depan, ini adalah skenario yang saya lihat membuat kompiler bingung (saya menganggap semua kasus ini sesuai desain):

  • Null memaafkan nol . Sering digunakan untuk menghindari peringatan dereference, tetapi menjaga objek tidak dapat dibatalkan. Sepertinya ingin tetap memakai dua sepatu.
    string s = null!; //No warning

  • Analisis permukaan . Bertentangan dengan ReSharper (yang melakukannya menggunakan anotasi kode ), kompiler C # masih tidak mendukung berbagai atribut untuk menangani referensi nullable.
    void DoSomethingWith(string? s)
    {    
        ThrowIfNull(s);
        var split = s.Split(' '); //Dereference warning
    }

Namun, hal itu memungkinkan untuk menggunakan beberapa konstruk untuk memeriksa nullability yang juga menghilangkan peringatan:

    public static void DoSomethingWith(string? s)
    {
        Debug.Assert(s != null, nameof(s) + " != null");
        var split = s.Split(' ');  //No warning
    }

atau atribut (masih sangat keren) (temukan semuanya di sini ):

    public static bool IsNullOrEmpty([NotNullWhen(false)] string? value)
    {
        ...
    }

  • Analisis kode yang rentan . Inilah yang Anda kemukakan. Kompiler harus membuat asumsi untuk berfungsi dan kadang-kadang mereka mungkin tampak kontra-intuitif (untuk manusia, setidaknya).
    void DoSomethingWith(string s)
    {    
        var b = s == null;
        var i = s.Length; // Dereference warning
    }

  • Masalah dengan obat generik . Ditanyakan di sini dan dijelaskan dengan sangat baik di sini (artikel yang sama seperti sebelumnya, paragraf "Masalah dengan T?"). Generik rumit karena mereka harus membuat referensi dan nilai-nilai bahagia. Perbedaan utama adalah bahwa sementara string?hanya string, int?menjadi Nullable<int>dan memaksa kompiler untuk menanganinya dengan cara yang sangat berbeda. Juga di sini, kompiler memilih jalur aman, memaksa Anda untuk menentukan apa yang Anda harapkan:
    public interface IResult<out T> : IResult
    {
        T? Data { get; } //Warning/Error: A nullable type parameter must be known to be a value type or non-nullable reference type.
    }

Menyelesaikan kendala pemberian:

    public interface IResult<out T> : IResult where T : class { T? Data { get; }}
    public interface IResult<T> : IResult where T : struct { T? Data { get; }}

Tetapi jika kita tidak menggunakan batasan dan menghapus '?' dari Data, kami masih dapat memasukkan nilai nol di dalamnya menggunakan kata kunci 'default':

    [Pure]
    public static Result<T> Failure(string description, T data = default)
        => new Result<T>(ResultOutcome.Failure, data, description); 
        // data here is definitely null. No warning though.

Yang terakhir tampaknya lebih sulit bagi saya, karena memang memungkinkan untuk menulis kode yang tidak aman.

Semoga ini bisa membantu seseorang.

Alvin Sartor
sumber
Saya menyarankan agar dokumen pada atribut nullable dibaca: docs.microsoft.com/en-us/dotnet/csharp/nullable-attributes . Mereka akan menyelesaikan beberapa masalah Anda, terutama dengan bagian analisis permukaan dan obat generik.
333
Terima kasih atas tautannya @ 333fred. Meskipun ada atribut yang dapat Anda mainkan, atribut yang memecahkan masalah yang saya posting (sesuatu yang memberi tahu saya yang ThrowIfNull(s);meyakinkan saya bahwa stidak nol), tidak ada. Juga artikel menjelaskan cara Handler non-nullable obat generik, sementara aku menunjukkan bagaimana Anda dapat "menipu" compiler, memiliki nilai nol tetapi tidak ada peringatan tentang hal itu.
Alvin Sartor
Sebenarnya, atribut memang ada. Saya mengajukan bug pada dokumen untuk menambahkannya. Anda sedang mencari DoesNotReturnIf(bool).
333 Februari
@ 333fred sebenarnya saya sedang mencari sesuatu yang lebih seperti DoesNotReturnIfNull(nullable).
Alvin Sartor