Что такое нулевой код возврата

Обновлено: 26.04.2024

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

Хотя эта программа достаточно тривиальна, и нам не нужно разбивать ее на несколько функций, но что, если бы мы захотели это сделать? Получение целочисленного значения от пользователя – это четко определенная задача, которую мы хотим, чтобы наша программа выполняла, поэтому она могла бы стать хорошим кандидатом на функцию.

Итак, давайте напишем программу для этого:

Хотя эта программа – хорошая попытка решения, она не совсем работает.

Когда вызывается функция getValueFromUser , пользователя просят ввести целое число, как и ожидалось. Но введенное им значение теряется, когда getValueFromUser завершает работу и управление возвращается к main . Переменная num никогда не инициализируется значением, введенным пользователем, поэтому программа всегда печатает ответ 0.

Чего нам не хватает, так это того, чтобы getValueFromUser могла вернуть значение, введенное пользователем, обратно в main , чтобы main могла использовать эти данные.

Возвращаемые значения

Когда вы пишете пользовательскую функцию, вы можете определить, будет ли ваша функция возвращать значение вызывающей стороне или нет. Чтобы вернуть значение вызывающей стороне, необходимы две вещи.

Во-первых, ваша функция должна указать, значение какого типа будет возвращено. Это делается путем установки типа возвращаемого значения функции, который является типом, определенным перед именем функции. В приведенном выше примере функция getValueFromUser имеет тип возвращаемого значения void , а функция main имеет тип возвращаемого значения int . Обратите внимание, что это не определяет, какое конкретное значение будет возвращено – только тип значения.

Во-вторых, внутри функции, которая будет возвращать значение, мы используем инструкцию return , чтобы указать конкретное значение, возвращаемое вызывающей стороне. Конкретное значение, возвращаемое функцией, называется возвращаемым значением. Когда инструкция return выполняется, возвращаемое значение копируется из функции обратно в вызывающую функцию. Этот процесс называется возвратом по значению.

Давайте рассмотрим простую функцию, которая возвращает целочисленное значение, и пример программы, которая ее вызывает:

При запуске эта программа печатает:

Выполнение начинается с верхней части main . В первой инструкции вычисляется вызов функции returnFive , в результате чего вызывается функция returnFive . Функция returnFive возвращает конкретное значение 5 обратно вызывающей стороне, которое затем выводится в консоль через std::cout .

Во второй инструкции вычисляется вызов функции returnFive , что приводит к повторному вызову функции returnFive . Функция returnFive возвращает значение 5 обратно вызывающей стороне. Выражение 5 + 2 вычисляется для получения результата 7, который затем выводится в консоль через std::cout .

В третьей инструкции функция returnFive вызывается снова, в результате чего значение 5 возвращается обратно вызывающей стороне. Однако функция main ничего не делает с возвращаемым значением, поэтому больше ничего не происходит (возвращаемое значение игнорируется).

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

Исправляем нашу тестовую программу

Когда эта программа выполняется, первая инструкция в main создаст переменную типа int с именем num . Когда программа перейдет к инициализации num , она увидит, что есть вызов функции getValueFromUser , поэтому она выполнит эту функцию. Функция getValueFromUser просит пользователя ввести значение, а затем возвращает это значение вызывающей функции ( main ). Это возвращенное значение используется как значение для инициализации переменной num .

Скомпилируйте эту программу и запустите ее несколько раз, чтобы убедиться, что она работает.

Отсутствие возвращаемого значения

Функции не обязаны возвращать значение. Чтобы сообщить компилятору, что функция не возвращает значение, используется тип возвращаемого значения void . Давайте посмотрим на функцию doPrint() из предыдущего урока:

Эта функция имеет тип возвращаемого значения void , что указывает на то, что она не возвращает значение вызывающей стороне. Поскольку она не возвращает значение, инструкция return не требуется (попытка вернуть конкретное значение из функции с типом возврата void приведет к ошибке компиляции).

Вот еще один пример функции, ничего не возвращающей, и пример программы, которая ее вызывает:

Возвращаясь к main

Теперь у вас есть концептуальные инструменты, чтобы понять, как на самом деле работает функция main . Когда программа выполняется, операционная система вызывает функцию main . Затем выполнение переходит в начало main . Инструкции в main выполняются последовательно. Наконец, main возвращает целочисленное значение (обычно 0), и ваша программа завершается. Значение, возвращаемое из main , иногда называют кодом состояния (также иногда называемым кодом выхода или, реже, кодом возврата), поскольку оно используется, чтобы указать, успешно ли была выполнена программа.

По определению, код состояния 0 означает, что программа выполнена успешно.

Лучшая практика

Ваша функция main должна возвращать 0, если программа работает нормально.

Код состояния, отличный от нуля, часто используется для обозначения сбоя (и хотя он отлично работает в большинстве операционных систем, строго говоря, его портируемость не гарантируется).

Для продвинутых читателей

Стандарт C++ определяет значение только трех кодов состояния: 0, EXIT_SUCCESS и EXIT_FAILURE . 0 и EXIT_SUCCESS означают, что программа выполнена успешно. EXIT_FAILURE означает, что программа не была успешно выполнена.

EXIT_SUCCESS и EXIT_FAILURE определены в заголовочном файле :

Если вы хотите максимизировать портируемость, вы должны использовать только 0 или EXIT_SUCCESS , чтобы указать на успешное завершение, или EXIT_FAILURE , чтобы указать на неудачное завершение.

C++ запрещает явный вызов функции main .

На данный момент вы также должны определять свою функцию main внизу исходного файла кода, под другими функциями.

Несколько дополнительных замечаний о возвращаемых значениях

Во-первых, если функция имеет тип возврата, являющийся не- void , она должна возвращать значение этого типа (используя инструкцию return ). Несоблюдение этого правила приведет к неопределенному поведению. Единственное исключение из этого правила – функция main() , которая, если возвращаемое значение не указано явно, примет, что оно равно 0. Тем не менее, рекомендуется явно возвращать значение из main , как для демонстрации вашего намерения, так и для согласованности с другими функциями (что не позволит вам опустить возвращаемое значение).

Лучшая практика

Всегда явно указывайте возвращаемое значение для любой функции, которая имеет тип возврата не- void .

Предупреждение

Неспособность вернуть значение из функции с типом возврата не- void (кроме main ) приведет к неопределенному поведению.

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

В-третьих, функция может возвращать вызывающей функции при каждом вызове только одно значение. Однако это значение не обязательно должно быть литералом, оно может быть результатом любого допустимого выражения, включая переменную или даже вызов другой функции, которая возвращает значение. В приведенном выше примере getValueFromUser() мы вернули переменную, содержащую число, введенное пользователем.

Наконец, обратите внимание, что функция может определять, что означает ее возвращаемое значение. Некоторые функции используют возвращаемые значения как коды состояния, чтобы указать, были ли они успешными или неудачными. Другие функции возвращают вычисленное или выбранное значение. Остальные функции ничего не возвращают. Что возвращает конкретная функция, и смысл этого значения определяется автором функции. Из-за большого разнообразия возможностей рекомендуется задокументировать функцию комментарием, указывающим, что означают возвращаемые значения.

Повторное использование функций

Теперь мы можем проиллюстрировать хороший случай повторного использования функции. Рассмотрим следующую программу:

В этой программе мы дважды вызываем getValueFromUser : один раз для инициализации переменной x и один раз для инициализации переменной y . Это избавляет нас от дублирования кода для ввода пользовательских данных и снижает вероятность ошибки. Как только мы узнаем, что getValueFromUser работает для одной переменной, она будет работать для любого их количества, сколько нам будет нужно.

В этом суть модульного программирования: возможность написать функцию, протестировать ее, убедиться, что она работает, а затем знать, что мы можем повторно использовать ее столько раз, сколько захотим, и она будет продолжать работать (пока мы не изменим эту функцию – после чего нам придется ее повторно протестировать).

Лучшая практика

Заключение

Возвращаемые значения позволяют функциям возвращать одно значение обратно вызывающей функции.

Функции позволяют минимизировать избыточность наших программ.

Небольшой тест

Вопрос 1

Проверьте следующие программы и укажите, что они выводят, или будут ли они не компилироваться.

Эта программа печатает число 16.

Эта программа не компилируется. Вложенные функции не допускаются.

Эта программа компилируется, но ничего не выводит. Значения, возвращаемые функциями, ни для чего не используются (и, таким образом, отбрасываются).

Эта программа печатает буквы A и B в отдельных строках.

Эта программа напечатает 5 дважды (в отдельных строках). Оба раза, когда вызывается функция getNumbers() , возвращается значение 5. Когда выполняется инструкция return 5; , функция немедленно завершается, поэтому инструкция return 7; никогда не выполняется.

1h) Чуть сложнее.

Эта программа будет скомпилирована, но функция не будет вызвана, потому что при вызове функции отсутствуют круглые скобки. То, что на самом деле будет выведено, зависит от компилятора.

Вопрос 2

По умолчанию, когда выполнение командной строки завершено, оно должно либо возвращать ноль, если выполнение завершается успешно, либо ненулевое, если выполнение не выполняется. Когда пакетный скрипт возвращает ненулевое значение после сбоя выполнения, ненулевое значение будет указывать, что такое номер ошибки. Затем мы будем использовать номер ошибки, чтобы определить ее причину и соответствующим образом ее устранить.

Ниже приведены общий код выхода и их описание.

Недостаточно виртуальной памяти.

Это указывает на то, что в Windows не хватает памяти.

Недостаточно виртуальной памяти.

Это указывает на то, что в Windows не хватает памяти.

Уровень ошибки

Переменная среды% ERRORLEVEL% содержит код возврата последней выполненной программы или сценария.

По умолчанию способ проверки на наличие ОШИБКИ находится через следующий код.

Синтаксис

Обычно используется команда EXIT / B% ERRORLEVEL% в конце пакетного файла, чтобы вернуть коды ошибок из пакетного файла.

EXIT / B в конце командного файла остановит выполнение командного файла.

Используйте EXIT / B в конце пакетного файла для возврата пользовательских кодов возврата.

Переменная среды% ERRORLEVEL% содержит последний уровень ошибки в пакетном файле, который является последними кодами ошибок из последней выполненной команды. В пакетном файле всегда рекомендуется использовать переменные среды вместо постоянных значений, поскольку одна и та же переменная расширяется до разных значений на разных компьютерах.

Давайте посмотрим на быстрый пример того, как проверить коды ошибок из командного файла.

пример

Давайте предположим, что у нас есть пакетный файл с именем Find.cmd, который имеет следующий код. В коде мы четко упомянули, что если мы не найдем файл lists.txt, то должны установить уровень ошибки равным 7. Точно так же, если мы видим, что переменная userprofile не определена, мы должны установить код уровня ошибки на 9.

Давайте предположим, что у нас есть еще один файл с именем App.cmd, который сначала вызывает Find.cmd. Теперь, если Find.cmd возвращает ошибку, в которой он устанавливает уровень ошибки больше 0, он завершает работу программы. В следующем пакетном файле после вызова Find.cnd он на самом деле проверяет, не превышает ли уровень ошибки больше 0.

Выход

В приведенной выше программе мы можем использовать следующие сценарии:

Если файл c: \ lists.txt не существует, в выводе консоли ничего не будет отображаться.

Если переменная userprofile не существует, в выводе консоли ничего не будет отображаться.

Если файл c: \ lists.txt не существует, в выводе консоли ничего не будет отображаться.

Если переменная userprofile не существует, в выводе консоли ничего не будет отображаться.

Loops

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

В Batch Script нет прямого оператора while, но мы можем очень легко реализовать этот цикл, используя оператор if и метки.

Оператор for также может перемещаться по диапазону значений. Ниже приводится общая форма заявления.

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

В Batch Script нет прямого оператора while, но мы можем очень легко реализовать этот цикл, используя оператор if и метки.

Оператор for также может перемещаться по диапазону значений. Ниже приводится общая форма заявления.

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

Цикл по аргументам командной строки

Оператор ‘for’ также можно использовать для проверки аргументов командной строки. В следующем примере показано, как можно использовать оператор for для циклического перебора аргументов командной строки.

пример

Выход

Давайте предположим, что приведенный выше код хранится в файле с именем Test.bat. Приведенная выше команда выдаст следующий вывод, если командный файл передает аргументы командной строки 1,2 и 3 как Test.bat 1 2 3.

Оператор break используется для изменения потока управления внутри циклов в любом языке программирования. Оператор break обычно используется в конструкциях цикла и используется для немедленного завершения самого внутреннего замкнутого цикла.

В решениях запрещается:

Ввод и вывод данных

Во всех задачах необходимо считывать входные данные из текстового файла и выводить результат в текстовый файл. Имена файлов указаны в условии задачи. Предполагается, что файлы располагаются в текущем каталоге программы, поэтому в решениях можно и нужно использовать только их имена без абсолютных путей в файловой системе тестирующего сервера.

Можно считать, что изначально при запуске решения выходной файл будет отсутствовать, и решение должно его создать и записать туда ответ.

Внимательно проверяйте имена файлов в решениях на соответствие условию задачи.

Тестирование решений

Каждое отправленное решение проходит на сервере проверку на нескольких тестах. Задача считается решённой только в случае прохождения всех тестов. Решение запускается на всех тестах, которые есть по задаче, и процесс тестирования не прерывается на первом непройденном тесте, как это делается в соревнованиях типа ACM.

Тестирование осуществляется автоматически, поэтому решения должны строго следовать формату входных и выходных данных, который описан в условии. В случае неясности можно задавать вопросы преподавателям. Если не сказано явно, все входные данные можно считать корректными и удовлетворяющими ограничениям из условия. Например, если сказано, что на входе целое число от 1 до 100 включительно, то можно считать, что это так и есть, и проверять неравенства и выводить ошибку в случае, если это не так, в коде решения нет необходимости.

Тесты по каждой задаче не упорядочены по сложности, по размеру входных данных, по какому-то иному критерию, а следуют в исторически сложившемся порядке их добавления в систему.

Не гарантируется, что первый тест в системе будет совпадать с тестом из условия (зачастую это не так).

CE — Ошибка компиляции (Compilation Error)

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

Посмотреть вывод компилятора и понять, почему код не удаётся скомпилировать, можно путём нажатия на иконку в таблице с вашими решениями. Наиболее частые причины ошибки компиляции: выбран неверный компилятор (для другого языка программирования или же несовместимая версия, например Java v7 вместо Java v8), отправляется не тот файл (файл проекта IDE вместо файла с исходным кодом).

TLE — Нарушен предел времени (Time Limit Exceeded)

Для каждого теста установлено своё ограничение по времени (Time Limit) в секундах. Для разных тестов по одной задаче ограничение по времени может быть разным.

Тестирующая система учитывает так называемое процессорное время (CPU Time) выполнения процесса в операционной системе. Нет смысла делать решение задачи многопоточным, потому что распараллеливание хоть и позволяет сократить реальное время работы (Wall Time), но не уменьшает процессорное время.

  • неэффективный алгоритм (например, в решении реализован алгоритм с временной сложностью Ω(n 2 ), хотя задача предполагает решение за O(n log n));
  • недостаточно эффективная программная реализация (идея и алгоритм правильные, но код написан не очень хорошо: например, ввод данных из файла осуществляется медленно, чрезмерно часто выделяется и освобождается память);
  • попытка чтения данных с консоли ( std::cin , scanf() , getchar() в C++, System.in в Java), тогда как нужно читать входные данные из файла (в этом случае программа блокируется в ожидании ввода и зависает, не расходуя при этом CPU Time, поэтому такой случай тестирующая система обрабатывает отдельно);
  • ошибка в программе (например, программа входит в бесконечный цикл).

ILE — Нарушен предел ожидания (Idleness Limit Exceeded)

Программа зависла в ожидании, не потребляя при этом ресурсы процессора.

Такое может быть, например, если согласно условию чтение входных данных осуществляется из файла, а решение выполняет ввод с консоли. В этом случае процесс решения заблокируется в ожидании нажатия клавиш на клавиатуре. Через некоторое время система тестирования принудительно завершит этот процесс и выставит вердикт ILE.

MLE — Нарушен предел памяти (Memory Limit Exceeded)

Программа использует слишком много оперативной памяти, стоит проанализировать использование памяти и оптимизировать его. Также причиной чрезмерного использования памяти может быть ошибка в программе, например, вечный цикл в теле которого на каждой или некоторых итерациях выделяется дополнительная память. К используемой памяти относится не только память с данными, но также память с кодом и программным стеком.

Ограничение по памяти есть не для всех задач. Гарантируется, что для всех тестов в рамках одной задачи ограничение по памяти одинаково.

Как и в случае нарушения ограничения по времени, программа при нарушении ограничения по памяти аварийно завершается тестирующей системой, её вывод не проверяется на правильность. Точно так же не следует воспринимать размер памяти, использованной до момента аварийного завершения, как объём, которого решению хватило бы для успешной работы. Более точно, вердикт MLE, полученный с использованием 257 МБ памяти, говорит о том, что приложение успело использовать 257 МБ памяти и было принудительно остановлено, но ничего не говорит о том, сколько памяти использовало бы приложение, не будучи принудительно остановленным.

В некоторых случаях при разовом выделении чрезмерно большого блока в памяти, этот запрос может быть не выполнен операционной системой, что в результате может привести к ошибке во время выполнения или (значительно реже) другому результату неопределённого поведения в случае с языком C++.

RTE — Ошибка во время выполнения (Run-time Error)

Укажем типичные причины ошибок во время выполнения.

PE — Ошибка представления (Presentation Error)

Наиболее частая причина возникновения этой ошибки — не найден выходной файл. Возможно, вы забыли создать выходной файл и выводите ответ в консоль (он в таком случае игнорируется). Проверьте имена входного и выходного файла в вашей программе на соответствие условию задачи. Исторически сложилось, что в разных задачах входной и выходной файл именуются по разным правилам: input.txt и output.txt, in.txt и out.txt, input.in и output.out (обратите внимание, что нет расширения txt), [задача].in и [задача].out.

Для некоторых задач программа проверки (checker) дополнительно удостоверяется, что ваш вывод соответствует определённому формату, и выдаёт ошибку представления в случае, если это не так. Например, если в задаче нужно вывести число, а вы выводите строку. Или если в задаче нужно вывести сначала число k, затем k чисел, а ваше решение выводит число k и далее (k + 1) чисел (то есть решение выводит в файл лишние данные).

WA — Неправильный ответ (Wrong Answer)

Для многих задач ответ однозначен, и проверяется просто побайтовое совпадение вашего выходного файла и сохранённого правильного ответа. Такая проверка требует строгого соблюдения формата файла, не допускает незначащих пробелов и пустых строк. Например, если правильный ответ имеет вид (пробелы обозначены символом ␣)

и решение вывело

После последней строки файла можно выводить или не выводить перевод строки — не важно. Есть две точки зрения в зависимости от того, с какой стороны смотреть на символ перевода строки:

  1. каждая строка завершается переводом строки, поэтому \n в конце файла нужен;
  2. перевод строки является разделителем между соседними строками, поэтому \n в конце файла не нужен.

Поэтому рекомендуется придерживаться первого подхода и завершать все строки переводами строк.

Другие очевидные причины получения неправильного ответа:

  • неверный алгоритм;
  • ошибка в программе.

Бывает такое, что решение от запуска к запуску даёт разные ответы, или же правильно работает на одном компьютере и неправильно на другом. Такие случаи, как правило, связаны с ошибками в решениях.

OK — Принято (Accepted)

Программа работает правильно и прошла все тесты с соблюдением всех ограничений.

CF — Ошибка тестирования (Check Failed)

Если указан номер теста, то программа успешно завершается на предложенном тесте (укладывается в отведённые время и память и не совершает ошибок во время выполнения), но результат не удаётся проверить из-за ошибок в программе проверки. Вашей ошибки в этом случае, возможно, никакой нет и после исправления программы проверки будет получен вердикт OK. Не исключены ещё два варианта: WA, PE.

Имейте в виду, что если ошибка тестирования возникает на первом же тесте, то на остальных Ваше решение не запускается вовсе. Соответственно, в этом случае после устранения ошибок программы проверки вердикты TLE, MLE, RTE также могут возникнуть в любом тесте, кроме первого.

Если же номер теста, на котором возникает ошибка тестирования, не указан, значит, программа проверки не была скомпилирована, а Ваше решение не запускалось вовсе. В этом случае правильным может быть любой вердикт, отличный от CF.

SV — Нарушение безопасности (Security Violation)

Ошибка означает, что программа попыталась выполнить запрещённые действия.

К их числу относится попытка создания новых процессов. Вашим решениям запрещено запускать на выполнение другие программы. Например, в коде

порождается новый процесс командной оболочки cmd.exe и в нём выполняется команда pause. Пожалуйста, не пишите так, для достижения аналогичного эффекта можно использовать другие приёмы.

Особенности языков программирования

У каждого конкретного языка программирования есть свои особенности, о которых полезно знать. Далее рассмотрены отдельные особенности написания решений на разных языках программирования:

Выбор языка программирования

Конфигурация тестирующего сервера

Сервер, на котором осуществляется запуск решений, является виртуальной машиной, выполняющейся внутри Microsoft Hyper-V Server 2012 R2. Виртуальный компьютер работает под управлением Windows 7 Professional x64, оснащён процессором Intel® Core™ i3-4130 (Haswell, кэш 3 МБ, 3,40 ГГц, доступно только одно ядро) и 4 ГБ оперативной памяти. Для хранения входных и выходных файлов используется RAM-диск, чтобы обеспечить максимальную производительность ввода-вывода.

Языки программирования

Размер системного стека явно не задаётся (используется размер по умолчанию). При компиляции кода на C++ включен режим оптимизации O2.

Когда я строй это линия , отмеченная . выдает предупреждение компилятора: warning CS8603: Possible null reference return. .

Я нахожу это немного запутанным, учитывая, что _test оно доступно только для чтения и инициализировано как ненулевое.

Если я изменю код на следующее, предупреждение исчезнет:

Кто-нибудь может объяснить это поведение?

Debug.Assert не имеет значения, потому что это проверка во время выполнения, тогда как предупреждение компилятора - проверка во время компиляции. Компилятор не имеет доступа к поведению во время выполнения.

The Debug.Assert is irrelevant because that is a runtime check - Это является уместным , потому что если вы прокомментируете эту линию, предупреждение уходит.

@Polyfun: компилятор может знать (через атрибуты), что он Debug.Assert выдаст исключение, если тест не пройден.

Я добавил много разных случаев здесь, и есть некоторые действительно интересные результаты. Позже напишу ответ - работа на данный момент.

@EricLippert: Debug.Assert теперь есть аннотация ( src ) DoesNotReturnIf(false) для параметра условия.

Анализ обнуляемого потока отслеживает нулевое состояние переменных, но не отслеживает другое состояние, например, значение bool переменной (как isNull указано выше), и не отслеживает взаимосвязь между состоянием отдельных переменных (например, isNull и _test ).

Вы знаете, что хотите поместить теорию решетки в спецификацию; теория решеток удивительна и совсем не смущает! Сделай это! :)

@EricLippert Awesome не начинает описывать вас. Эта ссылка сразу же входит в мой список для прочтения.

Я собираюсь немного упростить пример, избавившись от полей, и рассмотрим метод с одной из этих двух подписей:

В приведенных ниже реализациях я дал каждому методу различное число, чтобы я мог однозначно сослаться на конкретные примеры. Это также позволяет всем реализациям присутствовать в одной и той же программе.

В каждом из случаев, описанных ниже, мы будем делать разные вещи, но в конечном итоге попытаемся вернуться text - так text что важно нулевое состояние .

Безусловный возврат

Во-первых, давайте просто попробуем вернуть его напрямую:

Простой условный возврат

Теперь давайте проверим нулевое значение внутри самого if условия оператора. (Я бы использовал условный оператор, который, как я полагаю, будет иметь тот же эффект, но я хотел бы остаться честнее с вопросом.)

Условный возврат с локальной переменной

Теперь давайте попробуем перенести это условие в локальную переменную:

Похоже, мы узнали, что:

Безусловный возврат после игнорируемого сравнения

Давайте посмотрим на второй из этих пунктов, введя сравнение перед безусловным возвратом. (Таким образом, мы полностью игнорируем результат сравнения.):

Какие сравнения влияют на состояние?

Так что это сравнение text is null . какой эффект имеют подобные сравнения? Вот еще четыре метода, все из которых начинаются с ненулевого строкового параметра:

Почему подъем условия оказывает влияние?

Возвращаясь к нашему первому пункту ранее, почему M5 и M6 не принимают во внимание условие, которое привело к локальной переменной? Это меня не удивляет так сильно, как кажется, удивляет других. Встраивание подобной логики в компилятор и спецификацию - это большая работа и относительно небольшая выгода. Вот еще один пример, не имеющий ничего общего с обнуляемостью, где вставка чего-либо имеет эффект:

Несмотря на то, что мы знаем, что alwaysTrue это всегда будет правдой, это не удовлетворяет требованиям спецификации, которые делают код после if оператора недоступным, что нам и нужно.

Вот еще один пример вокруг определенного присваивания:

Несмотря на то, что мы знаем, что код будет входить именно в один из этих if тел операторов, в спецификации нет ничего, что могло бы решить эту проблему. Инструменты статического анализа вполне могут это сделать, но пытаться включить это в спецификацию языка было бы плохой идеей, ИМО - для инструментов статического анализа хорошо иметь все виды эвристики, которые могут развиваться со временем, но не так сильно для спецификации языка.

Отличный анализ, Джон. Ключевым моментом, который я узнал при изучении средства проверки Coverity, является то, что код является свидетельством убеждений его авторов . Когда мы видим нулевую проверку, которая должна сообщить нам, что авторы кода считают эту проверку необходимой. На самом деле проверяющий ищет доказательства того, что убеждения авторов были непоследовательными, потому что именно там мы видим противоречивые убеждения, скажем, об аннулировании, в которых возникают ошибки.

Несоответствие между "is object", "is <>" и "! = Null" - это вопрос, который мы внутренне обсуждали последние несколько недель. В ближайшем будущем я расскажу об этом в LDM, чтобы решить, нужно ли нам рассматривать их как чистые нулевые проверки или нет (что делает поведение непротиворечивым).

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

У меня нет конкретных знаний о реализации средства проверки потока, но, поработав над реализациями подобного кода в прошлом, я могу сделать некоторые обоснованные предположения. Средство проверки потока, вероятно, выводит две вещи в ложноположительном случае: (1) _test может быть нулем, потому что если бы оно не могло быть, у вас не было бы сравнения в первую очередь, и (2) isNull могло бы быть истинным или ложным - потому что если бы он не мог, вы бы не получили его if . Но соединение, которое return _test; работает только, если _test не является нулевым, это соединение не устанавливается .

Это удивительно сложная проблема, и вы должны ожидать, что компилятору понадобится некоторое время, чтобы освоить инструменты, которые уже несколько лет работают экспертами. Например, проверка потока Coverity не будет иметь никаких проблем с выводом, что ни один из двух вариантов не имеет нулевого возврата, но проверка потока Coverity стоит серьезных денег для корпоративных клиентов.

@JeroenMostert: Джаред Пар упоминает в комментарии к ответу Джона Скита, что Microsoft обсуждает эту проблему внутри компании.

Читайте также: