今更聞けない?Struct と Class の使い分け方(補足)

  • 9
    Like
  • 0
    Comment

先日の Kyobashi.swift x AKIBA.swift 合同勉強会今更聞けない?Struct と Class の使い分け方の発表をさせていただきましたが、スライドの都合上、とてもソースコードを全部載せることは不可能だっったので、重要なポイントだけ抜粋してスライドに載せましたが、ちょっとわからない人もいるかもしれませんので、せっかくだからここで会場にいなかった方にも解説ついでにフルソースを載せようかと思います。

まず当日の発表スライドはこんな感じでした:
https://speakerdeck.com/lovee/jin-geng-wen-kenai-struct-to-class-falseshi-ifen-kefang

んで、何が言いたかったかというと、structclass の一番大きな違いはやはり値型か参照型かなので、その振る舞いの違いをよく理解して、そのインスタンスは果たしてオブジェクトであるか、それともデータであるかをきちんと考えて、自分の需要に合ったものをチョイスしてほしいということでした。

例えば struct は値型なので、代入する際は常に自分のコピーを代入する振る舞いですので(実際の動きでは Copy-On-Write があるので代入した瞬間にコピーされるわけではないがイメージとしてこういう風だと理解していただければ)、例えば「恋人」というプロパティーが struct 型でしたら、そのプロパティーに値を代入した際は代入したのはその人ではなく、その人のその瞬間の写真的なものになるので、自分の恋人のプロパティーをアクセスする際は常にその瞬間の写真だけしかアクセスできず、自分の恋人の現在の状態が知り得ないため、恋人というプロパティーは class 型の方がふさわしいし、

逆に class は参照型ですので、代入する際は基本自分自身を代入する(NSObject.copy() で代入しなければ)ので、自分の口座情報を class 型で定義したら、他人と共有する際には口座そのものが共有されてしまい、他人も常に自分と全く同じ口座情報を参照しているので悪用されて慌ててパスワードを変更しててもパスワード変更が意味をなさなく、口座情報というプロパティーは struct 型の方がふさわしいです。

発表スライドの中に最も重要な 2 枚を抽出するとこんなものです:

switch instance {
case is Data:        //もちろん英語的な意味での「データ」
    useStruct()

case is Object:      //もちろん英語的な意味での「オブジェクト」
    useClass()

case _:
    fatalError("ちゃんと考えろ!")
}
true false
代入時にデータのコピーとして代入されるのがおかしい オブジェクト データ
同一比較(===)に意味がある オブジェクト データ
「ライフサイクル」が考えられる オブジェクト データ
抽象的な「本体」が考えられる オブジェクト データ

なので、何かを定義する際、struct を使うべきか class を使うべきか悩む際は、上記の表をよく見てみれば多分大体の場合問題が解決されるかと思います。

そして、スライドに全部載せることのできないソースコード(Playground での利用を想定しているもの)は下記となります(コメントアウトした行はエラーケースです)

TanakaLover
import Foundation

protocol BankAccountDelegate: class {
    func account(_ account: BankAccount, changePasswordTo newPassword: String) throws -> Int
    func account(_ account: BankAccount, transfer amount: Int, to receiver: Int) throws
}

//class BankAccount {
struct BankAccount {

    let id: Int
    private(set) var hashedPassword: Int
    private(set) weak var delegate: BankAccountDelegate?

    init(id: Int, hashedPassword: Int, delegate: BankAccountDelegate) {
        self.id = id
        self.hashedPassword = hashedPassword
        self.delegate = delegate
    }

//  func changePassword(to newPassword: String) {
    mutating func changePassword(to newPassword: String) {
        do {
            guard let delegate = self.delegate else { throw NSError() }
            let newHashedPassword = try delegate.account(self, changePasswordTo: newPassword)
            self.hashedPassword = newHashedPassword
            print("パスワード変更成功")
        } catch {
            print("パスワード変更失敗")
        }
    }

    func transfer(_ amount: Int, to receiver: Int) {
        do {
            guard let delegate = self.delegate else { throw NSError() }
            try delegate.account(self, transfer: amount, to: receiver)
            print("送金成功")
        } catch {
            print("送金失敗")
        }
    }

}

//protocol LoverBecomable { }
protocol LoverBecomable: class { }

//struct Human {
class Human {

    var name: String
    var item: String?
    var bankAccounts: [BankAccount] = []

//  var lover: LoverBecomable?
    weak var lover: LoverBecomable?

    init(name: String) {
        self.name = name
    }

}

extension Human: LoverBecomable { }

//var ktanaka = Human(name: "田中賢治")
//var maki = Human(name: "西木野真姫")
let ktanaka = Human(name: "田中賢治")
let maki = Human(name: "西木野真姫")

ktanaka.lover = maki

maki.item = "Bikini"

//(ktanaka.lover as? Human)?.item // nil
//ktanaka.lover === maki // Error: Binary operator '===' cannot be applied
(ktanaka.lover as? Human)?.item // "Bikini"
ktanaka.lover === maki // true


class Bank {

    fileprivate class Account {

        let id: Int
        var hashedPassword: Int
        var balance: Int

        init(id: Int, hashedPassword: Int, balance: Int) {
            self.id = id
            self.hashedPassword = hashedPassword
            self.balance = balance
        }

        func accountInfo(from delegate: BankAccountDelegate) -> BankAccount {
            return BankAccount(id: self.id, hashedPassword: self.hashedPassword, delegate: delegate)
        }

    }

    private let hashSalt = "\(arc4random())"
    fileprivate var accounts: [Int: Account] = [:]

    fileprivate func createHash(for string: String) -> Int {
        return (string + self.hashSalt).hash
    }

}

extension Bank {

    func issueAccount(password: String) -> BankAccount {

        var id = Int(arc4random())
        while self.accounts[id] != nil {
            id = Int(arc4random())
        }

        let hashedPassword = self.createHash(for: password)
        let account = Account(id: id, hashedPassword: hashedPassword, balance: 1_000_000)
        self.accounts[id] = account

        return account.accountInfo(from: self)

    }

}

extension Bank: BankAccountDelegate {

    func account(_ account: BankAccount, transfer amount: Int, to receiverID: Int) throws {

        enum Error: Swift.Error {
            case transfererNotFound(id: Int)
            case invalidPassword
            case balanceNotEnough(balance: Int)
            case receiverNotExist(id: Int)
        }

        guard let transferer = self.accounts[account.id] else {
            throw Error.transfererNotFound(id: account.id)
        }
        guard transferer.hashedPassword == account.hashedPassword else {
            throw Error.invalidPassword
        }
        guard transferer.balance >= amount else {
            throw Error.balanceNotEnough(balance: transferer.balance)
        }
        guard let receiver = self.accounts[receiverID] else {
            throw Error.receiverNotExist(id: receiverID)
        }

        transferer.balance -= amount
        receiver.balance += amount

    }

    func account(_ account: BankAccount, changePasswordTo newPassword: String) throws -> Int {

        enum Error: Swift.Error {
            case accountNotFound(id: Int)
            case invalidPassword
        }

        guard let accountObject = self.accounts[account.id] else {
            throw Error.accountNotFound(id: account.id)
        }
        guard accountObject.hashedPassword == account.hashedPassword else {
            throw Error.invalidPassword
        }

        let newHashedPassword = self.createHash(for: newPassword)
        accountObject.hashedPassword = newHashedPassword

        return newHashedPassword

    }

}


let someBank = Bank()

let tanakaAccount = someBank.issueAccount(password: "1234")
ktanaka.bankAccounts.append(tanakaAccount)
let amazonAccountID = someBank.issueAccount(password: "J7BJuGy8ks2T").id

ktanaka.bankAccounts[0].transfer(1000, to: amazonAccountID) // 送金成功

maki.bankAccounts.append(ktanaka.bankAccounts[0])

maki.bankAccounts[0].transfer(100_000, to: amazonAccountID) // 送金成功

ktanaka.bankAccounts[0].changePassword(to: "5678")

//maki.bankAccounts[0].transfer(500_000, to: amazonAccountID) // 送金成功!?
maki.bankAccounts[0].transfer(500_000, to: amazonAccountID) // 送金失敗!