Bu kod neden “Olası boş referans dönüşü” derleyici uyarısı veriyor?


70

Aşağıdaki kodu göz önünde bulundurun:

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

Bunu oluştururken, işaretlenmiş olan çizgi !!!, bir derleyici uyarı verir: warning CS8603: Possible null reference return..

Ben _testnull olmayan için salt okunur ve başlatılmış göz önüne alındığında, bunu biraz kafa karıştırıcı buluyorum .

Kodu aşağıdaki şekilde değiştirirsem uyarı kaybolur:

        public string Test()
        {
            // bool isNull = _test == null;

            if (_test == null)
                return "";
            else
                return _test;
        }

Herkes bu davranışı açıklayabilir mi?


1
Debug.Assert önemsizdir, çünkü bu bir çalışma zamanı kontrolüdür, derleyici uyarısı ise derleme zamanı kontrolüdür. Derleyicinin çalışma zamanı davranışına erişimi yok.
Polyfun

5
The Debug.Assert is irrelevant because that is a runtime check- O olduğunu , söz konusu satır out yorum yaparsanız uyarı uzağa gider çünkü alakalı.
Matthew Watson

1
@Polyfun: Derleyici Debug.Assert, test başarısız olursa bir istisna atacak potansiyel olarak (özniteliklerle) bilir .
Jon Skeet

2
Buraya birçok farklı vaka ekledim ve gerçekten ilginç sonuçlar var. Daha sonra bir cevap yazacağım - şimdilik yapmak için çalışın.
Jon Skeet

2
@EricLippert: Debug.AssertArtık condition parametresi için bir açıklama ( src ) var DoesNotReturnIf(false).
Jon Skeet

Yanıtlar:


38

Sıfırlanabilir akış analizi , değişkenlerin null durumunu izler , ancak bir booldeğişkenin değeri gibi isNullyukarıdaki durumu izlemez ( yukarıdaki gibi) ve ayrı değişkenlerin durumu (örn. isNullVe _test) arasındaki ilişkiyi izlemez .

Gerçek bir statik analiz motoru muhtemelen bu şeyleri yapardı, ama aynı zamanda bir dereceye kadar "buluşsal" veya "keyfi" olurdu: mutlaka takip ettiği kuralları söyleyemezdiniz ve bu kurallar zamanla değişebilir.

Bu doğrudan C # derleyicisinde yapabileceğimiz bir şey değil. Null olabilecek uyarılar için kurallar oldukça karmaşıktır (Jon'un analizinin gösterdiği gibi!), Ancak kurallardır ve bunlar hakkında düşünülebilir.

Bu özelliği kullanıma sunduğumuzda, çoğunlukla doğru dengeyi vurdu gibi hissediyoruz, ancak garip olarak ortaya çıkan birkaç yer var ve bunları C # 9.0 için tekrar ziyaret edeceğiz.


3
Kafes teorisini şartnameye koymak istediğinizi biliyorsunuz; kafes teorisi harika ve hiç de kafa karıştırıcı! Yap! :)
Eric Lippert

7
C # program yöneticisi yanıt verdiğinde sorunuzun yasal olduğunu biliyorsunuz!
Sam Rueby

1
@TanveerBadar: Kafes teorisi, kısmi sıraya sahip değer kümelerinin analizi ile ilgilidir; türleri iyi bir örnektir; X türünde bir değer Y türünde bir değişkene atanabilirse, bu, Y'nin X'i tutmak için "yeterince büyük" olduğunu ve bir kafes oluşturmak için yeterli olduğunu gösterir; bu da derleyicideki denetlenebilirliğin denetlenebileceğini ifade edebilir örgü teorisi açısından şartname. Bu statik analizle ilgilidir, çünkü tip tahsis edilebilirliği dışında bir analizöre ilgi duyulan pek çok konu da kafesler cinsinden ifade edilebilir.
Eric Lippert

1
@TanveerBadar: lara.epfl.ch/w/_media/sav08:schwartzbach.pdf , statik analiz motorlarının örgü teorisini nasıl kullandığına dair bazı iyi giriş örneklerine sahiptir.
Eric Lippert

1
@EricLippert Awesome sizi tanımlamaya başlamıyor. Bu bağlantı hemen okunması gereken listeme gidiyor.
Tanveer Badar

56

Burada neler olduğuna dair makul bir tahmin yapabilirim , ama hepsi biraz karmaşık :) Taslak şartnamede açıklanan null durumu ve null izlemeyi içerir . Temel olarak, geri dönmek istediğimiz noktada, derleyici ifadenin durumu "null değil" yerine "belki null" olduğunda uyarır.

Bu cevap sadece "sonuçlar burada" değil, biraz anlatı biçiminde ... Umarım bu şekilde daha faydalıdır.

Alanlardan kurtularak örneği biraz basitleştireceğim ve bu iki imzadan biriyle bir yöntem düşüneceğim:

public static string M(string? text)
public static string M(string text)

Aşağıdaki uygulamalarda, her yönteme farklı bir sayı verdim, böylece belirli örneklere açık bir şekilde başvurabilirim. Aynı zamanda tüm uygulamaların aynı programda bulunmasına izin verir.

Aşağıda açıklanan vakaların her birinde, çeşitli şeyler yapacağız, ancak geri dönmeye çalışacağız text- bu textönemli.

Koşulsuz iade

İlk olarak, doğrudan iade etmeye çalışalım:

public static string M1(string? text) => text; // Warning
public static string M2(string text) => text;  // No warning

Şimdiye kadar, çok basit. Yöntemin başlangıcında parametrenin null değeri, tür ise "belki null" ve tür string?ise "null" olur string.

Basit koşullu getiri

Şimdi ififade koşulunun içinde null olup olmadığını kontrol edelim . (Aynı etkiye sahip olacağına inandığım koşullu operatörü kullanardım, ancak soruya daha doğru kalmak istedim.)

public static string M3(string? text)
{
    if (text is null)
    {
        return "";
    }
    else
    {
        return text; // No warning
    }
}

public static string M4(string text)
{
    if (text is null)
    {
        return "";
    }
    else
    {
        return text; // No warning
    }
}

Harika, bu yüzden ifdurumun kendisinin geçersizliği kontrol ettiği bir ifade içinde, ifadenin her bir dalındaki değişkenin durumu iffarklı olabilir: elseblok içinde , durum her iki kod parçasında da "boş" değildir. Bu yüzden özellikle M3'te durum "belki null" değerinden "null değil" e değişir.

Yerel değişkenle koşullu getiri

Şimdi bu koşulu yerel bir değişkene kaldırmaya çalışalım:

public static string M5(string? text)
{
    bool isNull = text is null;
    if (isNull)
    {
        return "";
    }
    else
    {
        return text; // Warning
    }
}

public static string M6(string text)
{
    bool isNull = text is null;
    if (isNull)
    {
        return "";
    }
    else
    {
        return text; // Warning
    }
}

Hem M5 hem de M6 uyarı verir. Yani sadece M5'te (M3'te yaptığımız gibi) devlet değişikliğinin “belki null” dan “null değil” e pozitif etkisini elde etmekle kalmıyoruz ... M6'da durumun geçtiği yerdeki karşıt etkiyi elde ediyoruz ” null değil, "belki null". Bu beni gerçekten şaşırttı.

Görünüşe göre şunu öğrendik:

  • Durum bilgisini yaymak için "yerel değişkenin nasıl hesaplandığı" mantığı kullanılmaz. Daha sonra.
  • Boş bir karşılaştırma yapmak derleyiciyi daha önce boş olmadığını düşündüğü bir şeyin sonuçta boş olabileceği konusunda uyarabilir.

Yok sayılan bir karşılaştırmadan sonra koşulsuz getiri

Koşulsuz bir dönüşten önce bir karşılaştırma yaparak bu mermi noktalarının ikincisine bakalım. (Yani karşılaştırmanın sonucunu tamamen görmezden geliyoruz.):

public static string M7(string? text)
{
    bool ignored = text is null;
    return text; // Warning
}

public static string M8(string text)
{
    bool ignored = text is null;
    return text; // Warning
}

M8'in M2'ye eşdeğer olması gerektiğini nasıl hissettiğine dikkat edin - her ikisinin de koşulsuz olarak döndükleri bir null olmayan parametresi vardır - ancak null ile karşılaştırmanın başlatılması durumu "null" değerinden "belki null" değerine değiştirir. Durumdan textönce vazgeçmeye çalışarak bunun hakkında daha fazla kanıt elde edebiliriz :

public static string M9(string text)
{
    int length1 = text.Length;   // No warning
    bool ignored = text is null;
    int length2 = text.Length;   // Warning
    return text;                 // No warning
}

İfadenin returnşu anda nasıl bir uyarısı olmadığına dikkat edin: Yürütmeden sonrakitext.Length durum "boş değil" (çünkü bu ifadeyi başarıyla yürütürsek boş olamaz). Bu nedenle textparametre, türü nedeniyle "null değil" olarak başlar, null karşılaştırması nedeniyle "belki null" olur, sonra tekrar "null" olmaz text2.Length.

Hangi karşılaştırmalar devleti etkiler?

Yani bu bir karşılaştırma text is null... benzer karşılaştırmaların ne etkisi var? Aşağıda, tümü boş değerli bir dize parametresiyle başlayan dört yöntem daha vardır:

public static string M10(string text)
{
    bool ignored = text == null;
    return text; // Warning
}

public static string M11(string text)
{
    bool ignored = text is object;
    return text; // No warning
}

public static string M12(string text)
{
    bool ignored = text is { };
    return text; // No warning
}

public static string M13(string text)
{
    bool ignored = text != null;
    return text; // Warning
}

Yani rağmen x is objectşu ana önerilen bir alternatiftir x != nullaynı etkisi yoktur,: Sadece bir karşılaştırma null (herhangi biriyle is, ==ya !=) "belki boş" ile "değil boş" dan durumunu değiştirir.

Durumun kaldırılmasının neden bir etkisi var?

Daha önce ilk kurşun noktamıza geri dönersek, neden M5 ve M6 yerel değişkene yol açan durumu dikkate almıyor? Bu beni başkalarını şaşırtacak kadar şaşırtmıyor. Derleyiciye ve belirtime bu tür bir mantık oluşturmak çok fazla iştir ve nispeten az yarar sağlar. İşte bir şeyin satır içi yapmanın bir etkisi olduğu nullabilite ile ilgisi olmayan başka bir örnek:

public static int X1()
{
    if (true)
    {
        return 1;
    }
}

public static int X2()
{
    bool alwaysTrue = true;
    if (alwaysTrue)
    {
        return 1;
    }
    // Error: not all code paths return a value
}

Olsa biz biliyoruz alwaysTrueher zaman doğru olacaktır, bu sonra kod yapmak şartnamede gereksinimlerini tatmin etmiyor ifihtiyacımız olan şey ulaşılamaz açıklamada,.

İşte kesin atamanın etrafında başka bir örnek:

public static void X3()
{
    string x;
    bool condition = DateTime.UtcNow.Year == 2020;
    if (condition)
    {
        x = "It's 2020.";
    }
    if (!condition)
    {
        x = "It's not 2020.";
    }
    // Error: x is not definitely assigned
    Console.WriteLine(x);
}

Olsa biz kod olanların tam olarak bir gireceğini biliyorum ifdeyimi organları, işe spec dikkat hiçbir şey yok. Statik analiz araçları bunu yapabilir, ancak dil spesifikasyonuna koymaya çalışmak kötü bir fikir olacaktır, IMO - statik analiz araçlarının zaman içinde gelişebilen, ancak çok fazla olmayan her türlü buluşsal yöntemlere sahip olması iyidir. bir dil belirtimi için.


7
Harika analiz Jon. Coverity denetleyicisini incelerken öğrendiğim en önemli şey, kodun yazarlarının inançlarının kanıtı olduğudur . Kodun yazarlarının, denetimin gerekli olduğuna inandığını bize bildirmesi gereken bir boş denetim gördüğümüzde. Dama aslında yazarların inançlarının tutarsız olduğuna dair kanıt aramaktadır, çünkü hataların olduğu gibi, tutarsızlıkla ilgili tutarsız inançlar gördüğümüz yerlerdir.
Eric Lippert

6
Örneğin if (x != null) x.foo(); x.bar();gördüğümüzde iki kanıtımız var; ifAçıklamada "yazar o x foo çağrısı önce boş olabilir inanmaktadır" önermenin delildir ve şu ifadenin bu çelişki açar "yazar çağrı çubuk önce x boş değil inanmaktadır" kanıtı olduğunu ve bir hata olduğu sonucuna varır. Hata, gereksiz bir null denetiminin nispeten iyi huylu hatası veya potansiyel olarak çökme hatasıdır. Hangi hata asıl hata açık değil, ama bir tane olduğu açık.
Eric Lippert

1
Yerlilerin anlamlarını takip etmeyen ve "yanlış yolları" budamayan görece karmaşık olmayan damaların - insanların size imkansız olduğunu söyleyebileceği akış yollarını kontrol etme - hassas bir şekilde doğru bir şekilde modellenmedikleri için yanlış pozitifler üretme eğilimi yazarların inançları. Bu biraz zor!
Eric Lippert

3
"Nesne", "{}" ve "! = Null" arasındaki tutarsızlık, son birkaç haftadır dahili olarak tartıştığımız bir öğedir. Bunları çok yakın bir zamanda LDM'de gündeme getirecek ve bunları saf null kontroller olarak düşünüp düşünmeyeceğimize karar vereceğiz (bu da davranışı tutarlı hale getiriyor).
JaredPar

1
@ArnonAxelrod Boş olması anlamına gelmediğini söylüyor . Null olabilecek referans türleri yalnızca bir derleyici ipucu olduğu için yine de boş olabilir. (Örnekler: M8 (boş!); Veya C # 7 kodundan çağırmak veya uyarıları yok saymak.) Platformun geri kalanının tip güvenliği gibi değil.
Jon Skeet

29

Bu değişkeni üreten program akışı algoritmasının, yerel değişkenlerde kodlanan anlamları izlemek söz konusu olduğunda nispeten karmaşık olmadığını gösteren kanıtlar keşfettiniz.

Akış denetleyicisinin uygulaması hakkında özel bir bilgim yok, ancak geçmişte benzer kod uygulamaları üzerinde çalışarak, bazı eğitimli tahminler yapabilirim. Akış denetleyicisi muhtemelen yanlış pozitif durumda iki şeyi çıkarır: (1) _testboş olabilir, çünkü eğer olamazsa, ilk etapta karşılaştırmaya sahip olmazsınız ve (2) isNulldoğru veya yanlış olabilir - çünkü eğer yapamazsa, bir if. Ancak, null return _test;değilse yalnızca çalışan _testbağlantı bu bağlantı yapılmaz.

Bu şaşırtıcı derecede zor bir sorundur ve derleyicinin uzmanlar tarafından çok yıllarca çalışmış olan araçların karmaşıklığını elde etmesinin biraz zaman alacağını beklemelisiniz. Örneğin, Coverity akış denetleyicisinin, iki varyasyonunuzun hiçbirinin boş vermediği sonucunu çıkarmakta hiç sorun yaşamazsınız, ancak Coverity akış denetleyicisinin kurumsal müşteriler için ciddi paraya mal olması gerekir.

Ayrıca, Coverity denetleyicileri bir gecede büyük kod tabanlarında çalışacak şekilde tasarlanmıştır ; C # derleyicisinin analizi , makul bir şekilde gerçekleştirebileceğiniz derinlemesine analiz türlerini önemli ölçüde değiştiren düzenleyicideki tuş vuruşları arasında çalışmalıdır.


"Sofistike olmayan" doğru - koşullu gibi şeylere rastlarsa affedilebilir olduğunu düşünüyorum, çünkü hepimiz durma sorununun bu tür konularda biraz zor bir somun olduğunu biliyoruz, ancak bool b = x != nullvs bool b = x is { }( her iki atama da gerçekte kullanılmaz!) null kontroller için tanınan kalıpların bile sorgulanabilir olduğunu gösterir. Bu işi çoğunlukla gerçek, kullanımda kod tabanları için olması gerektiği gibi yapmak için kuşkusuz sıkı çalışmayı küçümsememek - analizin sermaye-P pragmatik olduğu anlaşılıyor.
Jeroen Mostert

@JeroenMostert: Jared Par, Jon Skeet'in Microsoft'un bu sorunu dahili olarak tartıştığı cevabı üzerine yaptığı bir yorumda bahsediyor .
Brian

8

Diğer tüm cevaplar hemen hemen doğrudur.

Herkesin merak etmesi durumunda, derleyicinin mantığını https://github.com/dotnet/roslyn/issues/36927#issuecomment-508595947 adresinde olabildiğince açık bir şekilde anlatmaya çalıştım.

Bahsetilmeyen tek parça, eğer bir null kontrolün "saf" olarak kabul edilip edilmeyeceğine nasıl karar verdiğimizdir, eğer bunu yaparsanız, null değerinin bir olasılık olup olmadığını ciddiye almalıyız. Başka bir şey yapmanın bir parçası olarak null için test yaptığınız C # 'da bir çok "tesadüfi" null kontrol var, bu yüzden kontrol kümesini insanların kasıtlı olarak yaptığından emin olduğumuzlara daraltmak istediğimize karar verdik. Biz ile geldi sezgisel en niçin böylece, "sözcüğü null adlı içeriyor" idi x != nullve x is objectfarklı sonuçlar üretir.

Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.