Рассмотрим следующий код:
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 = "";
}
}
Когда я строй это линия , отмеченная !!!
выдает предупреждение компилятора: warning CS8603: Possible null reference return.
.
Я нахожу это немного запутанным, учитывая, что _test
оно доступно только для чтения и инициализировано как ненулевое.
Если я изменю код на следующее, предупреждение исчезнет:
public string Test()
{
// bool isNull = _test == null;
if (_test == null)
return "";
else
return _test;
}
Кто-нибудь может объяснить это поведение?
c#
nullable-reference-types
Мэтью Уотсон
источник
источник
The Debug.Assert is irrelevant because that is a runtime check
- Это является уместным , потому что если вы прокомментируете эту линию, предупреждение уходит.Debug.Assert
выдаст исключение, если тест не пройден.Debug.Assert
теперь есть аннотация ( src )DoesNotReturnIf(false)
для параметра условия.Ответы:
Анализ обнуляемого потока отслеживает нулевое состояние переменных, но не отслеживает другое состояние, например, значение
bool
переменной (какisNull
указано выше), и не отслеживает взаимосвязь между состоянием отдельных переменных (например,isNull
и_test
).Фактический механизм статического анализа, вероятно, будет делать эти вещи, но также будет в некоторой степени «эвристическим» или «произвольным»: вы не можете обязательно сказать правила, которым он следует, и эти правила могут даже измениться со временем.
Это не то, что мы можем сделать непосредственно в компиляторе C #. Правила для обнуляемых предупреждений довольно сложны (как показывает анализ Джона!), Но они являются правилами и могут быть обоснованы.
Когда мы разворачиваем эту функцию, мы чувствуем, что в основном мы нашли правильный баланс, но есть несколько мест, которые кажутся неловкими, и мы вернемся к ним для C # 9.0.
источник
Я могу сделать разумное предположение относительно того, что здесь происходит, но все это немного сложно :) Это включает в себя нулевое состояние и нулевое отслеживание, описанные в проекте спецификации . По сути, в тот момент, когда мы хотим вернуться, компилятор предупредит, если состояние выражения «возможно, нулевое» вместо «не нулевое».
Этот ответ в несколько повествовательной форме, а не просто «вот выводы» ... Я надеюсь, что он более полезен.
Я собираюсь немного упростить пример, избавившись от полей, и рассмотрим метод с одной из этих двух подписей:
В приведенных ниже реализациях я дал каждому методу различное число, чтобы я мог однозначно сослаться на конкретные примеры. Это также позволяет всем реализациям присутствовать в одной и той же программе.
В каждом из случаев, описанных ниже, мы будем делать разные вещи, но в конечном итоге попытаемся вернуться
text
- такtext
что важно нулевое состояние .Безусловный возврат
Во-первых, давайте просто попробуем вернуть его напрямую:
Пока все просто. Обнуляемое состояние параметра в начале метода имеет значение «возможно, нулевое», если оно имеет тип,
string?
и «не нулевое», если оно имеет типstring
.Простой условный возврат
Теперь давайте проверим нулевое значение внутри самого
if
условия оператора. (Я бы использовал условный оператор, который, как я полагаю, будет иметь тот же эффект, но я хотел бы остаться честнее с вопросом.)Отлично, так выглядит в
if
операторе, где само условие проверяет на нулевое значение, состояние переменной в каждой ветвиif
оператора может быть различным: в пределахelse
блока состояние «не равно нулю» в обеих частях кода. Так, в частности, в M3 состояние меняется с «возможно, нулевое» на «не нулевое».Условный возврат с локальной переменной
Теперь давайте попробуем перенести это условие в локальную переменную:
И M5, и M6 выдают предупреждения. Таким образом, мы не только не получаем положительный эффект изменения состояния с «возможно, нулевого» на «не нулевой» в M5 (как мы это делали в M3) ... мы получаем противоположный эффект в M6, откуда происходит состояние « не ноль "до" может быть ноль ". Это действительно удивило меня.
Похоже, мы узнали, что:
Безусловный возврат после игнорируемого сравнения
Давайте посмотрим на второй из этих пунктов, введя сравнение перед безусловным возвратом. (Таким образом, мы полностью игнорируем результат сравнения.):
Обратите внимание, что M8 чувствует, что он должен быть эквивалентен M2 - оба имеют ненулевой параметр, который они возвращают безоговорочно - но введение сравнения с нулевым изменяет состояние с «не нулевое» на «возможно нулевое». Мы можем получить дополнительные доказательства этого, пытаясь разыменовать
text
до условия:Обратите внимание, что у
return
оператора нет предупреждения: состояние после выполненияtext.Length
«не равно нулю» (потому что если мы успешно выполним это выражение, оно не может быть нулевым). Таким образом,text
параметр начинается с «not null» из-за его типа, становится «возможно null» из-за нулевого сравнения, а затем снова становится «not null»text2.Length
.Какие сравнения влияют на состояние?
Так что это сравнение
text is null
... какой эффект имеют подобные сравнения? Вот еще четыре метода, все из которых начинаются с ненулевого строкового параметра:Так что , хотя
x is object
в настоящее время является рекомендуемым альтернативойx != null
, они не имеют такой же эффект: только сравнение с нулем (с любымis
,==
или!=
) изменяет состояние с «не нуль» до « может быть нулем».Почему подъем условия оказывает влияние?
Возвращаясь к нашему первому пункту ранее, почему M5 и M6 не принимают во внимание условие, которое привело к локальной переменной? Это меня не удивляет так сильно, как кажется, удивляет других. Встраивание подобной логики в компилятор и спецификацию - это большая работа и относительно небольшая выгода. Вот еще один пример, не имеющий ничего общего с обнуляемостью, где вставка чего-либо имеет эффект:
Несмотря на то, что мы знаем, что
alwaysTrue
это всегда будет правдой, это не удовлетворяет требованиям спецификации, которые делают код послеif
оператора недоступным, что нам и нужно.Вот еще один пример вокруг определенного присваивания:
Несмотря на то, что мы знаем, что код будет входить именно в один из этих
if
тел операторов, в спецификации нет ничего, что могло бы решить эту проблему. Инструменты статического анализа вполне могут это сделать, но пытаться включить это в спецификацию языка было бы плохой идеей, ИМО - для инструментов статического анализа хорошо иметь все виды эвристики, которые могут развиваться со временем, но не так сильно для спецификации языка.источник
if (x != null) x.foo(); x.bar();
у нас есть два доказательства; этоif
утверждение является доказательством утверждения «автор считает, что x может быть нулевым до вызова foo», а следующее утверждение является доказательством «автор считает, что x не является нулевым до вызова bar», и это противоречие приводит к Вывод, что есть ошибка. Ошибка является либо относительно доброкачественной ошибкой ненужной проверки нуля, либо потенциально сбойной ошибкой. Какая ошибка является настоящей ошибкой, не ясно, но ясно, что она есть.Вы обнаружили свидетельство того, что алгоритм программного потока, который выдает это предупреждение, относительно прост, когда дело доходит до отслеживания значений, закодированных в локальных переменных.
У меня нет конкретных знаний о реализации средства проверки потока, но, поработав над реализациями подобного кода в прошлом, я могу сделать некоторые обоснованные предположения. Средство проверки потока, вероятно, выводит две вещи в ложноположительном случае: (1)
_test
может быть нулем, потому что если бы оно не могло быть, у вас не было бы сравнения в первую очередь, и (2)isNull
могло бы быть истинным или ложным - потому что если бы он не мог, вы бы не получили егоif
. Но соединение, котороеreturn _test;
работает только, если_test
не является нулевым, это соединение не устанавливается .Это удивительно сложная проблема, и вы должны ожидать, что компилятору понадобится некоторое время, чтобы освоить инструменты, которые уже несколько лет работают экспертами. Например, проверка потока Coverity не будет иметь никаких проблем с выводом, что ни один из двух вариантов не имеет нулевого возврата, но проверка потока Coverity стоит серьезных денег для корпоративных клиентов.
Кроме того, контролеры Coverity предназначены для работы на больших кодовых базах в одночасье ; анализ компилятора C # должен выполняться между нажатиями клавиш в редакторе , что значительно меняет виды углубленного анализа, который вы можете выполнять.
источник
bool b = x != null
противbool b = x is { }
(с ни одно присвоение фактически не используется!) показывает, что даже распознанные шаблоны для нулевых проверок сомнительны. Не принижайте несомненно тяжелую работу команды, чтобы сделать эту работу в основном так, как это должно быть для реальных, используемых баз кода - похоже, что анализ прагматичен с большой буквы.Все остальные ответы в значительной степени верны.
На случай, если кому-нибудь интересно, я попытался изложить логику компилятора как можно более подробно в https://github.com/dotnet/roslyn/issues/36927#issuecomment-508595947.
Единственное, что не упомянуто, - это то, как мы решаем, следует ли считать нулевую проверку «чистой», в том смысле, что если вы делаете это, мы должны серьезно рассмотреть вопрос о том, возможна ли нулевая проверка. В C # существует множество «случайных» нулевых проверок, в которых вы проверяете нулевое значение как часть выполнения чего-то другого, поэтому мы решили, что хотим сузить набор проверок до тех, которые, как мы были уверены, люди делали намеренно. Эвристика, которую мы придумали, была «содержит слово null», поэтому
x != null
иx is object
дает разные результаты.источник