Перейти к содержимому
Теория · Итоговый проект · урок 3 из 5

Разбор ввода и ошибки

До сих пор чеки попадали в тетрадь прямо из кода. В жизни их вводят текстом: например, записывают траты в заметки на телефоне, по строке на покупку — food 12.5, taxi 7. Программе нужно превратить каждую такую строку в чек.

Но строки бывают с ошибками. В fod 3 опечатка в категории, в taxi abc вместо стоимости буквы, а в food стоимость забыли. Аккуратный бухгалтер не гадает, что имелось в виду. Он откладывает такой чек с запиской о причине и вносит в тетрадь остальные.

Разбор — это превращение текста в значения программы: у нас — строк в чеки. Начнём с одной правильной строки:

SWIFT
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
}

Давайте разберём программу по шагам:

  1. split(separator: " ") режет строку у пробела на две части: taxi и 7.
  2. Части имеют тип Substring — это куски исходной строки. Category(rawValue:) ждёт именно String, поэтому кусок оборачивают в String(…).
  3. Double(parts[1]) читает число, и ему кусок подходит как есть.
  4. И категория, и стоимость — опционалы. 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] аварийно завершит программу.
TEXT
Fatal error: Index out of range

С if let первые две строки молча не пройдут проверку, и человек не узнает почему. А третья остановит всю программу. Нужна функция, которая проверит строку по порядку и назовёт причину отказа. Для этого в Swift есть ошибки.

Функция parse

Опишем три причины перечислением и будем выбрасывать нужную, когда проверка не прошла:

SWIFT
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)
}

Давайте разберём функцию:

  1. InputError называет причины: не две части, неизвестная категория, стоимость не число.
  2. Первый guard проверяет число частей. Он стоит раньше, чем parts[1], поэтому строка food больше не завершит программу аварийно.
  3. Второй guard ищет закладку по надписи, а третий читает стоимость.
  4. Когда все проверки пройдены, parse возвращает готовый чек.

Константы parts, category и cost внутри функции — её собственные. С одноимёнными константами из первого блока они не связаны.

Плохие строки в catch

Теперь прочитаем целый список строк. Каждую строку разбираем в своём do, поэтому ошибка в одной строке не мешает остальным:

SWIFT
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": badCost
  • skip "fod 3": badCategory
  • skip "food": badFormat
  • 3 sum 23.75

Давайте разберём, что произошло:

  1. do/catch стоит внутри цикла, поэтому ошибка одной строки ловится, и цикл идёт к следующей.
  2. В catch пойманная ошибка лежит в константе error, и print показывает её вариант — причину отказа.
  3. Три хорошие строки стали чеками на 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(…):

SWIFT
let tab = Category(rawValue: parts[0]) // ✗
TEXT
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) } короче, но молча отбрасывает плохие строки вместе с причинами.