[Перевод] Обработка ошибок в Swift — меч и магия
Если издали видно общую картину, то вблизи можно понять суть. Концепции, которые казались мне далекими и, прямо скажем, странными во время экспериментов с Haskell и Scala, при программировании на Swift становятся ослепительно очевидными решениями для широкого спектра проблем.Взять вот обработку ошибок. Конкретный пример — деление двух чисел, которое должно вызвать исключение если делитель равен нулю. В Objective C я бы решил проблему так:
NSError *err = nil; CGFloat result = [NMArithmetic divide:2.5 by:3.0 error:&err]; if (err) { NSLog (@»%@», err) } else { [NMArithmetic doSomethingWithResult: result] } Со временем это стало казаться самым привычным способом написания кода. Я не замечаю, какие загогулины приходится писать и как косвенно они связаны с тем, что я на самом деле хочу от программы: Верни мне значение. Если не получится — то дай знать, чтобы ошибку можно было обработать.
Я передаю параметры, разыменовываю указатели, возвращаю значение в любом случае и в некоторых случаях потом игнорирую. Это неорганизованный код по следующим причинам:
Я говорю на машинном языке — указатели, разыменование. Я должен сам предоставить методу способ, которым он уведомит меня об ошибке. Метод возвращает некий результат даже в случае ошибки. Каждый из этих пунктов — источник возможных багов, и все эти проблемы Swift решает по-своему. Первый пункт, например, в Swift вообще не существует, поскольку он прячет под капотом всю работу с указателями. Остальные два пункта решаются с помощью перечислений.Если во время вычисления может возникнуть ошибка, то результата может быть два:
Успешный — с возвращаемым значением
Безуспешный — желательно, с объяснением причины ошибки
Эти варианты взаимоисключающие — в нашем примере, деление на 0 вызывает ошибку, а все остальное — возвращает результат. Swift выражает взаимоисключение с помощью «перечислений». Вот так выглядит описание результата вычисления с возможной ошибкой:
enum Result
switch result { case Success (let quotient): doSomethingWithResult (quotient) case Failure (let errString): println (errString) } Чуть подлиннее, но гораздо лучше! Конструкция switch позволяет связать значения с именами (quotient и errString) и обращаться к ним в коде, и результат можно обрабатывать в зависимости от возникновения ошибки. Все проблемы решены: Указателей нет, а разыменований и подавно Не требуется передавать функции divide лишние параметры Компилятор проверяет, все ли варианты перечисления обрабатываются Поскольку quotient и errString оборачиваются перечислением, они объявлены только в своих ветках и невозможно обратиться к результату в случае ошибки Но самое главное — этот код делает именно то, что я хотел — вычисляет значение и обрабатывает ошибки. Он напрямую соотносится с заданием.Теперь давайте разберем пример посерьезнее. Допустим, я хочу обработать результат — получить из результата магическое число, найдя от него наименьший простой делитель и получив его логарифм. В самом вычислении ничего магического нет — я просто выбрал случайные операции. Код бы выглядел вот так:
func magicNumber (divisionResult: Result
case Failure (let errString):
return Result.Failure (errString)
}
}
Выглядит несложно. Но что если я хочу получить из магического числа… магическое заклинание, которое ему соответствует? Я бы на писал так:
func magicSpell (magicNumResult: Result
case Failure (let errString): return Result.Failure (errString) } } Теперь, правда, у меня в каждой функции есть по выражению switch, и они примерно одинаковые. Более того, обе функции обрабатывают только успешное значение, в то время как обработка ошибок — постоянное отвлечение.Когда вещи начинают повторяться, стоит подумать о способе абстракции. И опять же, в Swift есть нужные инструменты. Перечисления могут иметь методы, и я могу избавиться от необходимости в этих выражениях switch с помощью метода map для перечисления Result:
enum Result
func map
(f: T → P) → Result
{
switch self {
case Success (let value):
return .Success (f (value))
case Failure (let errString):
return .Failure (errString)
}
}
}
Метод map назван так, потому что преобразует Result , и работает очень просто: Если есть результат, к нему применяется функция f
Если результата нет, ошибка возвращается как есть
Несмотря на свою простоту, этот метод позволяет творить настоящие чудеса. Используя обработку ошибок внутри него, можно переписать наши методы с помощью примитивных операций:
func magicNumber (quotient: Float) → Float {
let lpf = leastPrimeFactor (quotient)
return log (lpf)
} func magicSpell (magicNumber: Float) {
var spellID = spellIdentifier (magicNumber)
return incantation (spellID)
}
Теперь заклинание можно получить так:
let theMagicSpell = divide (2.5, by:3).map (magicNumber)
.map (magicSpell)
Хотя от методов можно и вообще избавиться:
let theMagicSpell = divide (2.5, by:3).map (findLeastPrimeFactor)
.map (log)
.map (spellIdentifier)
.map (incantation)
Разве не круто? Вся необходимость в обработке ошибок убрана внутрь абстракции, а мне нужно только указать необходимые вычисления — ошибка будет проброшена автоматически.Это, с другой стороны, не значит, что мне больше никогда не придется использовать выражение switch. В какой-то момент придется либо вывести ошибку, либо передать результат куда-то. Но это будет одно единственное выражение в самом конце цепочки обработки, и промежуточные методы не должны заботиться об обработке ошибок. Магия, скажу я вам! Это все — не просто академические «знания ради знаний». Абстрагирование обработки ошибок очень часто применяется при трансформации данных. Например, частенько бывает нужно получить данные с сервера, которые приходят в виде JSON (строка с ошибкой или результат), преобразовать их в словарь, потом в объект, а потом передать этот объект на уровень UI, где из него будет создано еще несколько отдельных объектов. Наше перечисление позволит писать методы так, будто они всегда работают на валидных данных, а ошибки будут пробрасываться между вызовами map. Если вы никогда до этого не видели подобных приемов, задумайтесь об этом ненадолго и попробуйте повозиться с кодом. (У компилятора какое-то время были проблемы с генерацией кода для обобщенных перечислений, но возможно, все уже компилируется). Думаю, вы оцените то, насколько это мощный подход. Если вы разбираетесь в математике, вы наверняка заметили баг в моем примере. Функция логарифма не объявлена для отрицательных чисел, и значения типа Float могут таковыми быть. В таком случае, log вернет не просто Float, а скорее Result