Разбор ввода и ошибки
До сих пор чеки попадали в тетрадь прямо из кода. В жизни их вводят текстом: например, записывают траты в заметки на телефоне, по строке на покупку — food 12.5, taxi 7. Программе нужно превратить каждую такую строку в чек.
Но строки бывают с ошибками. В fod 3 опечатка в категории, в taxi abc вместо стоимости буквы, а в food стоимость забыли. Аккуратный бухгалтер не гадает, что имелось в виду. Он откладывает такой чек с запиской о причине и вносит в тетрадь остальные.
Разбор — это превращение текста в значения программы: у нас — строк в чеки. Начнём с одной правильной строки:
enum Category: String, CaseIterable {
case food, taxi
}
struct Expense {
let category: Category
let cost: Double
}
let parts = "taxi 7".split(separator: " ")
let category = Category(rawValue: String(parts[0]))
let cost = Double(parts[1])
if let category, let cost {
let ride = Expense(category: category, cost: cost)
print(ride.category.rawValue, ride.cost) // taxi 7.0
}Давайте разберём программу по шагам:
split(separator: " ")режет строку у пробела на две части:taxiи7.- Части имеют тип
Substring— это куски исходной строки.Category(rawValue:)ждёт именноString, поэтому кусок оборачивают вString(…). Double(parts[1])читает число, и ему кусок подходит как есть.- И категория, и стоимость — опционалы.
if let category, let costоткрывает обе коробки, и программа печатаетtaxi 7.0.
Что может пойти не так
У строки есть три способа оказаться неверной:
fod 3— закладкиfodнет, иCategory(rawValue:)даётnil;taxi abc— вabcнет числа, иDoubleдаётnil; то же будет с12,5: Swift ждёт точку, а не запятую;food— часть всего одна, иparts[1]аварийно завершит программу.
Fatal error: Index out of rangeС if let первые две строки молча не пройдут проверку, и человек не узнает почему. А третья остановит всю программу. Нужна функция, которая проверит строку по порядку и назовёт причину отказа. Для этого в Swift есть ошибки.
Функция parse
Опишем три причины перечислением и будем выбрасывать нужную, когда проверка не прошла:
enum InputError: Error {
case badFormat, badCategory, badCost
}
func parse(_ line: String) throws -> Expense {
let parts = line.split(separator: " ")
guard parts.count == 2 else { throw InputError.badFormat }
guard let category = Category(rawValue: String(parts[0])) else {
throw InputError.badCategory
}
guard let cost = Double(parts[1]) else { throw InputError.badCost }
return Expense(category: category, cost: cost)
}Давайте разберём функцию:
InputErrorназывает причины: не две части, неизвестная категория, стоимость не число.- Первый
guardпроверяет число частей. Он стоит раньше, чемparts[1], поэтому строкаfoodбольше не завершит программу аварийно. - Второй
guardищет закладку по надписи, а третий читает стоимость. - Когда все проверки пройдены,
parseвозвращает готовый чек.
Константы parts, category и cost внутри функции — её собственные. С одноимёнными константами из первого блока они не связаны.
Плохие строки в catch
Теперь прочитаем целый список строк. Каждую строку разбираем в своём do, поэтому ошибка в одной строке не мешает остальным:
let lines = ["food 12.5", "taxi abc", "fod 3", "taxi 7", "food", "food 4.25"]
var expenses: [Expense] = []
for line in lines {
do {
expenses.append(try parse(line))
} catch {
print("skip \"\(line)\":", error)
}
}
let sum = expenses.reduce(0) { $0 + $1.cost }
print(expenses.count, "sum", sum) // 3 sum 23.75Этот блок печатает четыре строки:
skip "taxi abc": badCostskip "fod 3": badCategoryskip "food": badFormat3 sum 23.75
Давайте разберём, что произошло:
do/catchстоит внутри цикла, поэтому ошибка одной строки ловится, и цикл идёт к следующей.- В
catchпойманная ошибка лежит в константеerror, иprintпоказывает её вариант — причину отказа. - Три хорошие строки стали чеками на
12.5,7.0и4.25, и вместе они дают23.75.
На собеседовании часто спрашивают, когда ловить ошибку через do/catch, а когда хватит try?. Ответ: try? превращает ошибку в nil, и запись lines.compactMap { try? parse($0) } даёт те же три чека одной строкой. Но вместе с плохими строками она отбрасывает и причины. Если человеку нужно знать, что исправить, ловите ошибку в catch.
Здесь картинка с бухгалтером неточна. Бухгалтер заметит и странный чек — например, обед за миллион. Программа ловит только то, что проверяет parse, и строку food 1000000 примет без вопросов.
Частая ошибка
Кусок строки передали в Category(rawValue:) без String(…):
let tab = Category(rawValue: parts[0]) // ✗error: cannot convert value of type 'String.SubSequence' (aka 'Substring') to expected argument type 'String'Программа не компилируется. split возвращает куски типа Substring, а Category(rawValue:) принимает только String. Эту ошибку допускают часто, потому что Double(parts[1]) тот же кусок принимает. Оберните кусок: Category(rawValue: String(parts[0])).
Главное
split(separator: " ")режет строку на куски типаSubstring; дляCategory(rawValue:)кусок оборачивают вString(…), аDouble(…)принимает его как есть.parseпроверяет строку по порядку черезguardи на каждую причину выбрасывает свой вариантInputError; число частей проверяют доparts[1].do/catchвнутри цикла ловит ошибку одной строки, называет причину и даёт циклу идти дальше.compactMap { try? parse($0) }короче, но молча отбрасывает плохие строки вместе с причинами.