Подтвердить что ты не робот

Почему "Свитки" не безопасны в Swift?

Самое большое недоразумение для меня в Swift - ключевое слово throws. Рассмотрим следующий фрагмент кода:

func myUsefulFunction() throws

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

Но разве это не против природы Свифта? У Swift есть мощные дженерики и система типов, чтобы сделать код выразительным, но кажется, что throws противоположно, потому что вы не можете получить что-либо об ошибке от поиска сигнатуры функции.

Почему это так? Или я пропустил что-то важное и принял концепцию?

4b9b3361

Ответ 1

Выбор - это продуманное дизайнерское решение.

Им не нужна ситуация, когда вам не нужно объявлять бросание исключений, как в Objective-C, С++ и С#, поскольку это заставляет вызывающих абонентов либо предполагать, что все функции выбрасывают исключения, и включают шаблонный шаблон для обработки исключений, которые могут не произойти, или просто игнорировать возможность исключений. Ни один из них не идеален, а второй делает исключения непригодными для использования, за исключением случая, когда вы хотите завершить программу, потому что вы не можете гарантировать, что каждая функция в стеке вызовов правильно освободила ресурсы при разматывании стека.

Другая крайность - это идея, которую вы пропагандировали, и что каждый тип исключенного броска может быть объявлен. К сожалению, люди, похоже, возражают против этого, что у вас есть большое количество блоков catch, поэтому вы можете обрабатывать каждый тип исключения. Так, например, в Java они будут бросать Exception, уменьшая ситуацию до того же уровня, что и у Swift или хуже, они используют исключенные исключения, поэтому вы можете вообще игнорировать проблему. Библиотека GSON является примером последнего подхода.

Мы решили использовать исключенные исключения для указания сбоя синтаксического анализа. Это прежде всего делается потому, что обычно клиент не может восстановиться из-за плохого ввода и, следовательно, вынуждает их поймать проверенные результаты исключения в неаккуратном коде в блоке catch().

https://github.com/google/gson/blob/master/GsonDesignDocument.md

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

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

Полное обоснование проектного решения - https://github.com/apple/swift/blob/master/docs/ErrorHandlingRationale.rst

ИЗМЕНИТЬ

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

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

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

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

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

Причина, по которой отношение gson настолько ужасающей, состоит в том, что они говорят, что вы не можете оправиться от ошибки синтаксического анализа (на самом деле, хуже того, они говорят вам, что вам не хватает навыков для восстановления после ошибки синтаксического анализа). Это нелепо, что утверждать, люди пытаются разобрать недопустимые файлы JSON все время. Хорошо ли, что моя программа выйдет из строя, если кто-то выбирает файл XML по ошибке? Нет, нет. Он должен сообщить о проблеме и попросить их выбрать другой файл.

И дело gson было, кстати, просто примером того, почему использование исключенных исключений для ошибок, которые вы можете восстановить, плохо. Если я действительно хочу восстановить кого-то, выбирающего XML файл, мне нужно уловить исключения для выполнения Java, но какие? Ну, я мог бы посмотреть в документах Gson, чтобы узнать, считая, что они правильные и современные. Если бы они ушли с проверенными исключениями, API скажет мне, какие исключения ожидать, и компилятор скажет мне, не обрабатываю ли я их.

Ответ 2

Я был ранним сторонником типизированных ошибок в Swift. Вот как команда Свифта убедила меня, что я ошибаюсь.

Сильно типизированные ошибки являются хрупкими способами, которые могут привести к плохой эволюции API. Если API promises выкинет только одну из 3-х ошибок, тогда, когда возникает четвертое условие ошибки в более позднем выпуске, у меня есть выбор: я как-то похоронил его в существующих 3, или я заставляю каждого вызывающего пользователя переписать их ошибку обрабатывая код, чтобы справиться с ним. Поскольку это было не в оригинале 3, это, вероятно, не очень распространенное условие, и это оказывает сильное давление на API, чтобы не расширять их список ошибок, особенно после того, как структура широко используется в течение длительного времени (подумайте: Foundation).

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

Вы все еще можете сказать "по крайней мере, я знаю, откуда возникает ошибка с открытым перечислением", но это имеет тенденцию ухудшать ситуацию. Скажем, у меня есть система ведения журнала, и она пытается писать и получает ошибку ввода-вывода. Что он должен вернуть? У Swift нет алгебраических типов данных (я не могу сказать () -> IOError | LoggingError), поэтому мне, вероятно, придется обернуть IOError в LoggingError.IO(IOError) (что заставляет каждый слой явно переписывать, вы не можете иметь rethrows очень часто). Даже если у него были ADT, действительно ли вы хотите IOError | MemoryError | LoggingError | UnexpectedError | ...? После того, как у вас есть несколько слоев, я наматываю слой на слой обертывания некоторой основной "основной причины", которую нужно мучительно разворачивать, чтобы иметь дело.

И как вы собираетесь с этим справиться? В подавляющем большинстве случаев, как выглядят блоки catch?

} catch {
    logError(error)
    return
}

Чрезвычайно необычно для программ Cocoa (т.е. "приложений" ) глубоко вникать в точную основную причину ошибки и выполнять разные операции на основе каждого конкретного случая. Там может быть один или два случая, у которых есть восстановление, а остальное - вещи, в которых вы ничего не могли бы сделать. (Это обычная проблема в Java с проверенным исключением, которое не просто Exception; оно не похоже на то, что раньше никто этого не делал. Мне нравится Егор Бугаенко аргументы за проверенные исключения в Java, который в основном утверждает, что его предпочтительная практика Java - это решение Swift.)

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

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

func something() throws MyError { }

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

В конце концов, для сильно типизированных ошибок, которые могут быть очень полезны, Foundation должен будет их выбросить, поскольку он является крупнейшим производителем ошибок в системе. (Как часто вы действительно создаете NSError с нуля, по сравнению с тем, который был создан Фондом?) Это будет серьезный капитальный ремонт Фонда и очень сложно поддерживать совместимость с существующим кодом и ObjC. Таким образом, типизированные ошибки должны быть абсолютно фантастическими при решении очень распространенных проблем Cocoa, которые стоит рассматривать как поведение по умолчанию. Это не могло быть немного лучше (не говоря уже о проблемах, описанных выше).

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