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

Модель: расходы

Чему вы научитесь

Чему вы научитесь:

  • собирать модель учёта расходов из перечисления, структуры и массива;
  • считать итоги по категориям и печатать их в одном порядке при каждом запуске;
  • разбирать строки вида food 12.5 и называть причину, если строка неверна;
  • видеть, что вы уже умеете на Swift и куда двигаться дальше.
Открытая тетрадь расходов с закладками food и taxi, в которую ложатся чеки, и итоговая строка sum

Представьте тетрадь расходов. На её краю две закладки: food — еда и taxi — такси. Каждый чек вы вкладываете под закладку его категории. В конце месяца вы складываете чеки под каждой закладкой, а внизу страницы подводите черту и пишете общий итог — строку sum.

В этой теме мы соберём такую тетрадь на Swift. Новых конструкций в ней не будет: программа сложится из того, что вы уже знаете. Она растёт от урока к уроку, но первый блок кода в каждом уроке — целая программа, которую можно запустить отдельно.

Начнём с одного чека. Его можно хранить строкой "food 12.5". Но строка примет и "fod 12.5" с опечаткой, и компилятор её не заметит. А чтобы сложить стоимости, каждую строку пришлось бы заново резать на части.

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

SWIFT
enum Category: String, CaseIterable {
    case food, taxi
}

struct Expense: CustomStringConvertible {
    let category: Category
    let cost: Double

    var description: String {
        "\(category.rawValue) \(cost)"
    }
}

let lunch = Expense(category: .food, cost: 12.5)
print(lunch) // food 12.5

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

  1. enum Category — закладки тетради. Список категорий задан заранее, и другой категории в программе не бывает.
  2. struct Expense — чек: категория и стоимость под одним именем.
  3. Свойство description говорит, как чек выглядит при печати.
  4. Expense(category: .food, cost: 12.5) заполняет чек почленным инициализатором: метки совпадают с именами свойств.
  5. print(lunch) берёт текст из description и печатает food 12.5.

Закладки: перечисление

Категория — выбор из короткого списка, поэтому это перечисление. В объявлении enum Category: String, CaseIterable после двоеточия стоят сырой тип и протокол:

  • String — тип сырого значения. Вариант без явной строки получает своё имя, поэтому у food сырое значение "food" — надпись на закладке.
  • CaseIterable — протокол, который даёт массив всех вариантов allCases в порядке объявления.

Сырой тип пишут первым, а протоколы — после него через запятую. Если поменять их местами, enum Category: CaseIterable, String, программа не скомпилируется: raw type 'String' must appear first in the enum inheritance clause.

Посмотрим на сырые значения и allCases в деле:

SWIFT
let labels = Category.allCases.map { $0.rawValue }
print(labels.joined(separator: " ")) // food taxi
if let tab = Category(rawValue: "taxi") {
    print(tab) // taxi
}
print(Category(rawValue: "bus") == nil) // true

Давайте пройдём по программе:

  1. map { $0.rawValue } превращает каждую закладку в её надпись, а joined склеивает надписи через пробел: food taxi.
  2. Category(rawValue: "taxi") находит закладку по надписи. Результат — опционал, и if let его открывает.
  3. Закладки bus в тетради нет, поэтому Category(rawValue: "bus") даёт nil.

Чек: структура

Чек — это данные из двух граф, поэтому это структура. Свойства объявлены через let: записанный чек не исправляют. Стоимость — Double, потому что у неё бывает дробная часть: 12.5.

Но Double хранит не все дроби точно: print(0.1 + 0.2) печатает 0.30000000000000004. Без потерь он хранит целые числа, половины и четверти, поэтому стоимости этой темы — 12.5, 7 и 4.25 — складываются точно. В настоящих приложениях деньги часто хранят целым числом в копейках.

Что ещё важно знать о Expense:

  • Swift сам дал структуре почленный инициализатор, и свой init писать не пришлось;
  • число без точки тоже годится: в Expense(category: .taxi, cost: 7) тип свойства — Double, поэтому 7 хранится как 7.0, так же, как в let price: Double = 5;
  • description задаёт текст при печати; если убрать CustomStringConvertible из объявления, print покажет служебную запись вроде Expense(category: main.Category.food, cost: 12.5). Перед Category стоит имя программы: после swiftc main.swift это main.

Этот вопрос часто задают на собеседовании: почему модель — структура, а не класс? Ответ:

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

Тетрадь: массив чеков

Чеки складываем в массив — стопку в тетради:

SWIFT
let expenses = [
    lunch,
    Expense(category: .taxi, cost: 7),
    Expense(category: .food, cost: 4.25),
]
print(expenses.count) // 3
for expense in expenses {
    print(expense)
}

Swift выводит тип массива из его элементов: [Expense]. Запятая после последнего элемента разрешена, и с ней удобно дописывать новые чеки. Цикл печатает три строки, каждый чек через его description:

  • food 12.5
  • taxi 7.0
  • food 4.25

Здесь картинка с тетрадью неточна. В тетради чек лежит под своей закладкой, а в массиве все чеки идут одной стопкой, в том порядке, в каком их добавили. Закладка — только свойство category каждого чека. Чтобы узнать итог по закладке, стопку придётся перебрать.

Частая ошибка

Категорию передали строкой, как надпись на закладке:

SWIFT
let ride = Expense(category: "taxi", cost: 7) // ✗
TEXT
error: cannot convert value of type 'String' to expected argument type 'Category'

Программа не компилируется. У свойства category тип Category, а "taxi" — строка, хоть она и совпадает с сырым значением. Строка сама в вариант не превращается. Напишите вариант, .taxi, или найдите его по надписи через Category(rawValue: "taxi") и откройте опционал.

Главное

  • Категория — перечисление enum Category: String, CaseIterable: сырой тип пишут первым, протокол — через запятую, а allCases даёт все варианты по порядку.
  • Чек — структура Expense с неизменяемыми свойствами category и cost; description из CustomStringConvertible задаёт вид при печати.
  • Чеки хранятся в массиве [Expense] одной стопкой, в порядке добавления.
  • Строка не превращается в вариант сама: Category(rawValue:) ищет вариант по надписи и возвращает опционал.