Чому ми отримуємо можливе попередження про нульове посилання, коли нульове посилання видається неможливим?


9

Прочитавши це запитання в HNQ, я продовжив читати про Nullable Reference Types в C # 8 і зробив кілька експериментів.

Я дуже усвідомлюю, що 9 разів з 10, а то й частіше, коли хтось каже "Я знайшов помилку компілятора!" це насправді задумом і власним непорозумінням. І оскільки я почав вивчати цю особливість лише сьогодні, я ясно, що я не дуже добре її розумію. Якщо це не вдається, давайте подивимось на цей код:

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

Прочитавши документацію, до якої я посилався вище, я би сподівався, що s == nullрядок надішле мене попередженням - адже sявно нерегульований, тому порівнювати його nullне має сенсу.

Натомість я отримую попередження в наступному рядку, і попередження говорить про те, що sможлива нульова посилання, хоча для людини очевидно, що це не так.

Більше того, попередження не відображається, якщо ми не порівнюємо sїх null.

Я зробив деякий Googling, і я потрапив на проблему GitHub , яка виявилася зовсім іншою, але в процесі роботи я провів розмову з дописувачем, який дав трохи більше розуміння такої поведінки (наприклад, "Нульові перевірки часто є корисним способом сказати компілятору відновити його попереднє висновок про мінливість змінної. " ). Однак це все ще не залишило мене на головне питання без відповіді.

Замість того, щоб створити нове видання GitHub і, можливо, зайняти час надзвичайно напружених учасників проекту, я передаю це спільноті.

Не могли б ви пояснити мені, що відбувається і чому? Зокрема, чому на цьому s == nullрядку не створюються попередження , і чому ми маємо, CS8602коли це не здається, що nullтут можлива посилання? Якщо висновок про нульовість не є кульовим, як підказує пов'язана нитка GitHub, як це може піти не так? Які б це були приклади?


Здається, що компілятор сам встановлює поведінку, що в цей момент змінна "s" може бути нульовою. У будь-якому випадку, якщо я коли-небудь використовувати рядки або об'єкти, то перед тим, як викликати функцію, слід завжди перевіряти. "s? .Length" повинен зробити трюк, і саме попередження має зникнути.
Chg

1
@chg, у цьому не повинно бути потреби, ?оскільки sце не зводиться до нуля. Це не стає нульовим, просто тому, що ми були досить нерозумні, щоб порівняти його null.
Андрій Савіних

Я слідкував за попереднім запитанням (вибачте, не можу його знайти), де він був розміщений, якщо ви додасте перевірку, що значення є нульовим, тоді компілятор приймає це як "натяк", що значення може бути нульовим, навіть якщо це це, мабуть, не так.
Стюард

@stuartd, так, це, здається, так і є. Тож тепер питання: чому це корисно?
Андрій Савіних

1
@chg, ну ось що я кажу в тілі запитання, чи не так?
Андрій Савіних

Відповіді:


7

Це фактично дублікат відповіді, з якою пов'язував @stuartd, тому я не збираюся тут вникати у надто глибокі деталі. Але суть справи полягає в тому, що це не мовна помилка, ані помилка компілятора, але це призначена поведінка саме так, як реалізована. Відстежуємо нульовий стан змінної. Коли ви спочатку оголошуєте змінну, цей стан є NotNull, оскільки ви явно ініціалізуєте його зі значенням, яке не є нульовим. Але ми не відстежуємо, звідки взявся той NotNull. Наприклад, це фактично еквівалентний код:

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

В обох випадках ви явно перевіряєте sнаявність null. Ми сприймаємо це як вхід до аналізу потоку так само, як Мадс відповів на це запитання: https://stackoverflow.com/a/59328672/2672518 . У цій відповіді виходить, що ви отримуєте попередження про повернення. У цьому випадку відповідь полягає в тому, що ви отримуєте попередження про те, що ви скасували можливу нульову посилання.

Це не стає нульовим, просто тому, що ми були досить нерозумні, щоб порівняти його null.

Так, це насправді так. До компілятора. Як люди, ми можемо розглянути цей код і, очевидно, зрозуміти, що він не може кинути нульове виняткове посилання. Але те, як реалізується аналіз нульового потоку в компіляторі, він не може. Ми обговорювали деяку кількість вдосконалень цього аналізу, де додавали додаткові стани, виходячи з того, звідки походить цінність, але ми вирішили, що це додає великої складності в реалізації не для великої вигоди, тому що єдині місця, де це було б корисно для таких випадків, коли користувач ініціалізує змінну з a newабо постійним значенням, а потім перевіряє її на nullбудь-який вигляд .


Дякую. В основному це стосується подібності з іншим питанням, я хотів би детальніше вирішити розбіжності. Наприклад, чому s == nullне створюється попередження?
Андрій Савіних

Також я просто зрозумів, що з #nullable enable; string s = "";s = null;компілює і працює (він все ще створює попередження), які переваги, від реалізації, яка дозволяє призначати null до "нерегульованої посилання" у включеному контексті нульової анотації?
Андрій Савіних

Відповідь Меда зосереджена на тому, що "[компілятор] не відслідковує взаємозв'язок між станом окремих змінних", у нас в цьому прикладі немає змінних змін, тому у мене виникають проблеми із застосуванням решти відповіді Меда до цього випадку.
Андрій Савіних

Само собою зрозуміло, але пам’ятайте, я тут не для критики, а для того, щоб вчитися. Я використовував C # з часу, коли він з'явився в 2001 році. Навіть не дивлячись на мову, спосіб поведінки компілятора мене здивував. Мета цього питання - обґрунтувати, чому така поведінка корисна для людини.
Андрій Савіних

Є вагомі причини перевірки s == null. Можливо, наприклад, ви користуєтесь загальнодоступним методом і хочете зробити перевірку параметрів. Або, можливо, ви використовуєте бібліотеку, котру зазначили неправильно, і поки вони не виправлять цю помилку, вам доведеться мати справу з null там, де вона не була оголошена. У будь-якому з цих випадків, якби ми попередили, це був би поганий досвід. Що стосується дозволу на призначення: місцеві анотації змінної призначені лише для читання. Вони взагалі не впливають на час виконання. Насправді ми поміщаємо всі ці попередження в один код помилки, щоб ви могли їх вимкнути, якщо ви хочете зменшити розміщення коду.
333фред

0

Якщо висновок про нестабільність не є кулезахисним, [..] як воно може піти не так?

Я із задоволенням прийняв нульові посилання на C # 8, як тільки вони були доступні. Коли я звик використовувати позначення ReSharper [NotNull] (і т.д.), я помітив деякі відмінності між ними.

Компілятор C # можна обдурити, але він схильний помилятися з боку обережності (як правило, не завжди).

Як орієнтир для майбутніх відвідувачів, це такі сценарії, в яких я бачив, що компілятор дуже заплутаний (я припускаю, що всі ці випадки є дизайном):

  • Ніщо прощає нуль . Часто використовується, щоб уникнути попередження про відмежування, але зберігаючи об'єкт ненульовим. Схоже, хочеться тримати ногу в двох черевиках.
    string s = null!; //No warning

  • Поверхневий аналіз . На противагу ReSharper (це робить це за допомогою анотації коду ), компілятор C # досі не підтримує повний спектр атрибутів для обробки нульових посилань.
    void DoSomethingWith(string? s)
    {    
        ThrowIfNull(s);
        var split = s.Split(' '); //Dereference warning
    }

Це, однак, дозволяє використовувати деяку конструкцію для перевірки на нікчемність, яка також позбавляється від попередження:

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

або (ще досить класні) атрибути (знайдіть їх усіх тут ):

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

  • Аналіз сприйнятливого коду . Це те, що ти показав на світ. Компілятор повинен робити припущення, щоб працювати, і іноді вони можуть здаватися контрінтуїтивними (як мінімум для людей).
    void DoSomethingWith(string s)
    {    
        var b = s == null;
        var i = s.Length; // Dereference warning
    }

  • Проблеми з дженериками . На питання тут і пояснюється дуже добре тут ( то ж стаття , як і раніше, пункт «Питання з Т?»). Генеріки складні, тому що вони повинні робити щасливими і посилання, і цінності. Основна відмінність полягає в тому, що, хоча string?це лише рядок, int?стає a Nullable<int>і змушує компілятор обробляти їх абсолютно різними способами. Також тут компілятор вибирає безпечний шлях, змушуючи вказати, що ви очікуєте:
    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.
    }

Вирішено надання обмежень:

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

Але якщо ми не будемо використовувати обмеження та видалити '?' з даних, ми все ще можемо ввести нульові значення в нього за допомогою ключового слова "за замовчуванням":

    [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.

Останній здається мені складнішим, оскільки він дозволяє писати небезпечний код.

Сподіваюся, що це комусь допоможе.


Я б запропонував надати документам атрибути, що зводяться до змін, прочитати: docs.microsoft.com/en-us/dotnet/csharp/nullable-attributes . Вони вирішать деякі ваші проблеми, зокрема з розділу аналізу поверхні та загальної інформації.
333фред

Дякуємо за посилання @ 333fred. Незважаючи на те, що є атрибути, з якими можна грати, атрибут, який вирішує проблему, яку я опублікував (те, що говорить мені, що ThrowIfNull(s);запевняє мене, sне є нульовим), не існує. Також у статті пояснюється, як обробляти ненульовані дженерики, в той час як я показував, як можна «обдурити» компілятор, маючи нульове значення, але не попереджуючи про це.
Елвін Сартор

Насправді атрибут існує. Я подав помилку на документи, щоб додати її. Ви шукаєте DoesNotReturnIf(bool).
333фред

@ 333fred насправді я шукаю щось подібне DoesNotReturnIfNull(nullable).
Елвін Сартор
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.