先日の Kyobashi.swift x AKIBA.swift 合同勉強会で今更聞けない?Struct と Class の使い分け方の発表をさせていただきましたが、スライドの都合上、とてもソースコードを全部載せることは不可能だっったので、重要なポイントだけ抜粋してスライドに載せましたが、ちょっとわからない人もいるかもしれませんので、せっかくだからここで会場にいなかった方にも解説ついでにフルソースを載せようかと思います。
まず当日の発表スライドはこんな感じでした:
https://speakerdeck.com/lovee/jin-geng-wen-kenai-struct-to-class-falseshi-ifen-kefang
んで、何が言いたかったかというと、struct
と class
の一番大きな違いはやはり値型か参照型かなので、その振る舞いの違いをよく理解して、そのインスタンスは果たしてオブジェクトであるか、それともデータであるかをきちんと考えて、自分の需要に合ったものをチョイスしてほしいということでした。
例えば 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 での利用を想定しているもの)は下記となります(コメントアウトした行はエラーケースです)
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) // 送金失敗!