Menu
Coddy logo textTech

Защита состояния

Часть раздела Объектно-ориентированное программирование путешествия по Swift на Coddy. Урок 19 из 57.

Инвариант — это правило, которое всегда должно выполняться для значения, например «баланс никогда не бывает отрицательным». Тип защищает свои инварианты, проверяя каждое изменение перед его применением:

struct BankAccount {
    private(set) var balance = 0

    mutating func withdraw(_ amount: Int) -> Bool {
        guard amount > 0, amount <= balance else { return false }
        balance -= amount
        return true
    }

    mutating func deposit(_ amount: Int) {
        guard amount > 0 else { return }
        balance += amount
    }
}

var acc = BankAccount()
acc.deposit(100)
print(acc.withdraw(150))
print(acc.withdraw(40))
print(acc.balance)

Вывод:

false
true
60

Когда вызывающему коду нужно знать, почему изменение было отклонено, метод может выбросить ошибку вместо возврата false. Тип ошибки — перечисление, соответствующее Error:

enum TransferError: Error {
    case invalidAmount
    case insufficientFunds(missing: Int)
}

struct Account {
    private(set) var balance: Int

    mutating func withdraw(_ amount: Int) throws {
        guard amount > 0 else { throw TransferError.invalidAmount }
        guard amount <= balance else { throw TransferError.insufficientFunds(missing: amount - balance) }
        balance -= amount
    }
}

var acc = Account(balance: 30)
do {
    try acc.withdraw(50)
} catch TransferError.insufficientFunds(let missing) {
    print("missing \(missing)")
} catch {
    print("other error")
}

Вывод:

missing 20

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

struct Temperature {
    private(set) var kelvin: Double   // должно оставаться >= 0
    mutating func cool(by amount: Double) {
        kelvin = max(0, kelvin - amount)   // единственный способ понизить его
    }
}

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

struct Inventory {
    private(set) var stock: [String: Int] = ["tea": 2, "jam": 1]

    mutating func sell(_ items: [String]) -> Bool {
        var needed: [String: Int] = [:]
        for i in items { needed[i, default: 0] += 1 }
        for (item, n) in needed where (stock[item] ?? 0) < n { return false }
        for (item, n) in needed { stock[item]! -= n }
        return true
    }
}

var inv = Inventory()
print(inv.sell(["tea", "jam", "jam"]))
print(inv.stock["tea"]!)
print(inv.sell(["tea", "jam"]))
print(inv.stock["tea"]!)

Вывод:

false
2
true
1
challenge icon

Задание

Легко

Дополните структуру Seats, которая продаёт билеты на представление с количеством мест, равным capacity. Свойство sold имеет модификатор private(set). Метод buy(_:) выбрасывает ошибку TicketError.invalidCount, если количество меньше 1, TicketError.soldOut, если не осталось свободных мест, и TicketError.notEnough(left:), если свободных мест осталось меньше, чем запрошено; в противном случае он добавляет билеты к значению sold.

Предоставленный код считывает вместимость и по одному запрошенному количеству в каждой строке. Для каждого значения он выводит sold 3, left 7, invalid count, sold out или only 2 left.

Ваш код должен находиться в TicketError.swift и Seats.swift. В файле main.swift содержится предоставленный код ввода/вывода, и его нельзя изменять.

Попробуйте сами

// Предоставленный код ввода/вывода: оставьте его как есть
var input: [String] = []
while let line = readLine() { input.append(line) }
var seats = Seats(capacity: Int(input[0])!)
for line in input.dropFirst() {
    let n = Int(line)!
    do {
        try seats.buy(n)
        print("sold \(n), left \(seats.capacity - seats.sold)")
    } catch TicketError.invalidCount {
        print("invalid count")
    } catch TicketError.soldOut {
        print("sold out")
    } catch TicketError.notEnough(let left) {
        print("only \(left) left")
    } catch {
        print("unexpected")
    }
}
quiz iconПроверьте себя

В этом уроке есть небольшой тест. Начните урок, чтобы ответить на вопросы и сохранить прогресс.

Все уроки раздела Объектно-ориентированное программирование

Потренируйтесь самостоятельно: Песочница Swift