С момента выпуска Kotlin 1.3 мы увидели введение типа Result, который представляет собой объединение типа Success и типа Failure. Его цель - инкапсулировать результат действия, успешного или нет, позволяя обработать его в более позднее время. Обоснование этого добавления API, а также конкретные варианты использования обсуждаются в KEEP-127.

Вместе с Result в стандартной библиотеке появилось множество новых вспомогательных функций для работы с новым типом, одна из которых runCatching:

Как мы видим, основная цель runCatching - конкретно создать Результат: он выполнит блок кода и вернет его успех или свой отказ, инкапсулированный в тип объединения.

Второй и, вероятно, более непосредственный вариант использования этой простой функции - это функциональная обработка исключений. Kotlin всегда поощрял написание кода в функциональном стиле, и runCatching является прекрасным дополнением, заменяющим «старую» императивную конструкцию try / catch.

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

Разве не нужно их всех ловить

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

Методы должны генерировать исключения, объясняющие, что пошло не так: был ли передан неверный аргумент? Вот исключение IllegalArgumentException. Требуется некоторая переменная, допускающая значение NULL, но это не так? У вас есть исключение IllegalStateException и так далее. Они также обычно должны нести сообщение, описывающее конкретный сбой, чтобы лучше понять (и отладить) сбой.

Фундаментальная концепция, которую следует понять, состоит в том, что исключения не являются злом; да, они могут вызвать сбой вашей программы и рассердить некоторых пользователей, но на самом деле это не ошибка исключения - скорее всего, это наша вина за неправильное обращение с ней!

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

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

Теперь давайте еще раз посмотрим на реализацию runCatching в приведенной выше сути. Что оно делает? Улавливает все.

В этом случае он идет еще дальше: он ловит все бросаемые предметы. Для тех, кто не знает, Throwable - это все, что может идти после ключевого слова throw; у него есть два потомка: исключения и ошибки. Мы пока не упоминали об ошибках; Ошибки обычно представляют собой что-то неправильное, что произошло на более низком уровне, чем ваша бизнес-логика, и что обычно не может быть исправлено с помощью простого улова. Например: OutOfMemoryError, StackOverflowError, NuSuchMethodError: если что-то из этого происходит, вы понимаете, что пора остановиться и внимательно подумать о том, что происходит в вашем коде, вы не можете просто поймать их и показать вежливое сообщение об ошибке. На самом деле общепринятой практикой является никогда перехват ошибок, поэтому мы обычно перехватываем только подклассы Exception.

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

Очевидно, что если мы ответственные программисты, мы все равно можем использовать функциональный стиль runCatching, а также правильно обрабатывать ошибки, поскольку, в конце концов, функция расширения onFailure дает нам возможность обрабатывать Throwable:

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

Прерыватель сопрограмм

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

Функция приостановки гарантирует, что перед возобновлением работы с точки приостановки она проверит, была ли отменена сама сопрограмма. Если он обнаружит, что это так, он подтвердит отмену, выдав исключение CancellationException, которое является не чем иным, как обычным исключением с той лишь разницей, что оно не приводит к сбою нашей программы, потому что CoroutineExceptionHandler перехватывает его для нас. Конечным результатом этого является то, что остальная часть сопрограммы после точки приостановки больше не выполняется.

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

Представьте приложение для Android: что произойдет, если пользователь нажмет кнопку «Назад», закрывающую текущий экран, в то время как данные все еще загружаются? Обратное нажатие вызывает onUserCanceled() ведущего, область сопрограммы отменяет свои дочерние задания Jobs, и мы ожидаем, что загрузка данных будет прервана.
Вместо этого мы, вероятно, увидим некоторый сбой из-за того, что мы каким-то образом пытаемся получить доступ к пользовательскому интерфейсу, который больше не отображается на экране из объекта View. Но ... методы View вызываются после точки приостановки, и моя сопрограмма наверняка проверила отмену перед возобновлением оттуда.
Вы можете быть уверены, что это так, и она действительно выдала свое CancellationException как всегда… только для runCatching, чтобы проглотить его. Поскольку это исключение не было распространено, машина сопрограмм не останавливается: код после приостановки может вызвать некоторую фатальную ошибку. runCatching мешает внутренней механике сопрограммы и просто нарушает ее.

В этом важность явного отлова исключений.

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