Почему мы получаем предупреждение о нулевой ссылке с возможным разыменованием, когда нулевая ссылка кажется невозможной?

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.

Я немного погуглил и столкнулся с проблемой GitHub , которая, как оказалось, была полностью о другом, но в процессе я поговорил с участником, который дал некоторое представление об этом поведении (например, «Нулевые проверки часто являются полезным способом»). сказать компилятору сбросить свой предыдущий вывод об обнуляемости переменной. " ) Однако это все еще оставило меня без ответа на главный вопрос.

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

Не могли бы вы объяснить мне, что происходит и почему? В частности, почему на s == nullлинии не генерируются предупреждения , и почему мы имеем, CS8602когда это не похоже на nullссылку здесь? Если вывод обнуления не является пуленепробиваемым, как предполагает связанный поток GitHub, как он может пойти не так? Каковы были бы некоторые примеры этого?

Андрей Савиных
источник
Кажется, что компилятор сам устанавливает поведение, что в этот момент переменная "s" может быть нулевой. В любом случае, если я когда-либо использую строки или объекты, перед вызовом функции всегда должна быть проверка. «s? .Length» должно сработать, а само предупреждение должно исчезнуть.
Chg
1
@chg, в этом не должно быть необходимости, ?потому что sэто не nullable. Это не становится обнуляемым просто потому, что мы были достаточно глупы, чтобы сравнивать это с null.
Андрей Савиных
Я следовал за более ранним вопросом (извините, не могу его найти), где было указано, что если вы добавите проверку, что значение равно нулю, то компилятор воспримет это как «подсказку», что значение может быть равно нулю, даже если это это явно не тот случай.
Stuartd
@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любом случае.

333fred
источник
Спасибо. В основном это касается сходства с другим вопросом, я бы хотел больше рассмотреть различия. Например, почему s == nullне выдает предупреждение?
Андрей Савиных
Также я только что понял, что с #nullable enable; string s = "";s = null;компилирует и работает (он все еще выдает предупреждение), каковы преимущества реализации, которая позволяет присваивать значение null «ненулевой ссылке» в включенном контексте аннотации null?
Андрей Савиных
Ответ Мэда фокусируется на том факте, что «[компилятор] не отслеживает связь между состоянием отдельных переменных», в этом примере у нас нет отдельных переменных, поэтому у меня возникают проблемы с применением остальной части ответа Мэда к этому случаю.
Андрей Савиных
Само собой разумеется, но помните, я здесь не для того, чтобы критиковать, а чтобы учиться. Я использую C # с момента его появления в 2001 году. Хотя я не новичок в языке, поведение компилятора меня удивило. Цель этого вопроса - обосновать, почему это поведение полезно для людей.
Андрей Савиных
Есть веские причины для проверки s == null. Возможно, вы используете публичный метод и хотите проверить параметры. Или, возможно, вы используете библиотеку, которая аннотировалась неправильно, и до тех пор, пока она не исправит эту ошибку, вам придется иметь дело с нулем, где она не была объявлена. В любом из этих случаев, если мы предупреждали, это был бы плохой опыт. Что касается разрешения присваивания: локальные переменные аннотации только для чтения. Они не влияют на время выполнения вообще. Фактически, мы помещаем все эти предупреждения в один код ошибки, чтобы вы могли отключить их, если хотите уменьшить отток кода.
333
0

Если логический вывод не является пуленепробиваемым, [..] как он может пойти не так?

Я с радостью принял обнуляемые ссылки на C # 8, как только они стали доступны. Поскольку я привык использовать нотацию [NotNull] (и т. Д.) В ReSharper, я заметил некоторые различия между ними.

Компилятор 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
    }

  • Проблемы с дженериками . Спросил здесь и объяснил очень хорошо здесь (та же статья, что и раньше, пункт «Вопрос с T?»). Обобщения являются сложными, поскольку они должны сделать счастливыми и ссылки, и ценности. Основное отличие состоит в том, что, хотя string?это просто строка, она int?становится 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.

Последний вариант кажется мне более сложным, поскольку он позволяет писать небезопасный код.

Надеюсь, это кому-нибудь поможет.

Элвин Сартор
источник
Я бы посоветовал предоставить документацию по атрибутам Nullable следующим образом : docs.microsoft.com/en-us/dotnet/csharp/nullable-attributes . Они решат некоторые из ваших проблем, в частности, с помощью раздела анализа поверхности и обобщений.
333
Спасибо за ссылку @ 333fred. Несмотря на то, что есть атрибуты, с которыми вы можете поиграть, атрибут, который решает проблему, которую я опубликовал (что-то, что говорит мне, ThrowIfNull(s);что sоно не является нулевым), не существует. Также в статье объясняется, как обрабатывать ненулевые обобщения, в то время как я показывал, как можно «обмануть» компилятор, имея нулевое значение, но без предупреждения об этом.
Элвин Сартор
На самом деле, атрибут существует. Я добавил ошибку в документы, чтобы добавить ее. Ты ищешь DoesNotReturnIf(bool).
333
@ 333fred фактически я ищу что-то более похожее DoesNotReturnIfNull(nullable).
Элвин Сартор