Menu
Coddy logo textTech

状態を守る

CoddyのSwiftジャーニー「オブジェクト指向プログラミング」セクションの一部。レッスン 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

チャレンジ

簡単

ショーのチケットを販売する capacity 席の Seats 構造体を完成させてください。sold は private(set) です。buy(_:) は、1 未満の件数に対して TicketError.invalidCount を、空席がない場合に TicketError.soldOut を、要求された席数より残りの席数が少ない場合に TicketError.notEnough(left:) をスローします。それ以外の場合は、チケットを sold に追加します。

用意されたコードは、容量と、1 行につき 1 件の要求数を読み取ります。それぞれについて、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プレイグラウンド