Убедитесь, что ваши базы кода iOS не работают правильно

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

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

  1. Пользователь
  2. Сервис
  3. Сама программа

Неожиданное действие пользователя

Если вы просите пользователя ввести свое имя, но он просто нажимает кнопку «ОК», то это не обычное ожидаемое поведение, а такое, которое (надеюсь) было определено заранее.

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

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

Подходящие инструменты для этого типа ошибки:

  • if или switch ветки
  • guard заявления

Неожиданное поведение службы

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

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

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

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

Подходящие инструменты:

  • Result.error или nil в качестве необязательного возвращаемого значения
  • Проверьте возвращаемое значение с помощью оператора if, switch или guard.
  • do try catch блокирует или try?, если ошибку можно игнорировать

Неожиданное поведение программы

Мы дошли до интересных и «проблемных» задач. Что делать с ошибками в самой программе?

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

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

Программные (mer) ошибки обычно приводят к неопределенным состояниям в приложении. Некоторые могут быть не очень серьезными, но другие - таковыми. А неопределенные состояния также могут влиять на другие состояния приложения, которые, таким образом, также не определены.

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

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

Однако есть еще более тонкий способ - тестирование непосредственно в коде с помощью проверок работоспособности. Проверки на вменяемость похожи на assert и precondition. Они помогают проверять предположения и внутренние состояния в самом коде и, таким образом, распознавать неопределенные состояния приложения.

Если проверка работоспособности не удалась, это связано с тем, что приложение уже находится в неопределенном состоянии - состоянии, которое должно / не может произойти на самом деле. Если ситуация действительно возникает, на самом деле есть только одно верное решение: сбой!

Здесь сбой - это хорошо, потому что использование отчетов о сбоях:

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

Это дает вам возможность исправить эту ошибку как можно быстрее. Любая попытка восстановления неизбежно приведет к переносу ошибки и последующим ошибкам, которые никогда не будут исправлены.

Поэтому неопределенные состояния являются неисправимыми ошибками и должны приводить к сбою.

Подходящие инструменты:

  • assert() or assertionFailure()
  • precondition() or preconditionFailure()
  • fatalError()

Давайте подробнее рассмотрим эти инструменты и способы их использования.

Проверки работоспособности в коде

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

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

Итак, когда вы используете какую функцию?

фатальная ошибка

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

предварительное условие

С помощью precondition() вы можете проверить правильность входных параметров.

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

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

preconditionFailure

Семантика preconditionFailure() фактически идентична семантике precondition(). Если это срабатывает, значит, параметр метода неверен. Разница, однако, в том, что preconditionFailure() сам по себе не выполняет проверку (т. Е. Вы должны выполнить такую ​​проверку в коде и, в отрицательном случае, вызвать preconditionFailure() вручную).

Вместо preconditionFailure() можно также использовать fatalError(). Однако preconditionFailure() явно указывает, что это связано с параметром, а fatalError() является более общим. Так что единственная разница - это используемый контекст.

утверждать

Как и precondition(), функция assert() проверяет значение и аварийно завершает работу приложения в отрицательном случае. Однако это происходит только в отладочной сборке, а не в сборке выпуска. Таким образом, если вы передадите такую ​​функцию, как assert(isStateSet()), то эта функция будет выполняться только во время разработки, а не в продуктивном приложении.

Если isStateSet() выполняет трудоемкие вычисления, возможно, имеет смысл, что они не выполняются в продуктивном приложении. Однако, если isStateSet() имеет побочные эффекты, то это происходит только во время разработки, а не в производственном приложении, и тогда возникает вопрос, почему готовое приложение ведет себя иначе, чем во время разработки. Поэтому вы должны убедиться, что assert() не вызывает побочных эффектов.

Если вы используете precondition() для параметров функции, когда вы используете assert()? Фактически, только для проверок в пределах вашего собственного блока кода (например, если вы в настоящее время что-то вычисляете самостоятельно и хотите убедиться, что вы что-то вычислили правильно, и это все еще находится в диапазоне значений). Если кто-то изменит код позже, assert() гарантирует, что это изменение по-прежнему зависит от исходных условий (т. Е. Не позволяет будущему «я» забыть исходные предположения).

Но что произойдет, если aPositiveNumber станет отрицательным в сборке релиза? Это могло произойти, если расчет работает со свойствами класса. Обычно они всегда должны содержать положительные значения, но если закралась ошибка, они внезапно становятся отрицательными. Поскольку assert() не запускается в сборке выпуска, работа продолжается с неверным значением. Тогда результат не определен, и ошибка будет продолжительной.

Вот почему я бы рекомендовал не использовать assert() и всегда использовать guard с fatalError():

Однако, если вы абсолютно хотите использовать assert, вам следует обратить внимание на следующие моменты:

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

Значимым вариантом использования assert, например, может быть то, что вы хотите убедиться, что все ресурсы объединены в приложение. Ресурсы - это константы, которые поступают в приложение во время компиляции и не изменяются во время выполнения. Их загрузка и проверка могут занять довольно много времени. Если вы сделаете это один раз при запуске приложения отладочной версии, вы можете быть уверены, что не забыли добавить ресурс. Однако в сборке выпуска вы можете сэкономить время и пропустить эту проверку.

assertionFailure

Так же, как preconditionFailure() относится к precondition(), assertionFailure() относится к assert(). assertionFailure() ничего не проверяет, а только приводит к сбою приложения в отладочной сборке. Как и в случае с assert(), может возникнуть проблема неопределенного состояния, которая затягивается в сборке выпуска.

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

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

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

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

Таким образом, вполне законно использовать вызов assertionFailure(), например, после guard, потому что он немедленно сообщит вам, что сервер и его модульные тесты неисправны. Если бы не ваш собственный сервер, вы, вероятно, не использовали бы assertionFailure(), но здесь это имеет смысл.

Заключение

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

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