この記事について
この記事は、開発未経験の人間がインプットした内容が書かれています。
実際に開発経験を積まないと身につかない、かといって疎かにしたら後々痛い目にあう、といったジレンマを少しでも解消するために、「頭の中を言語化し、解釈違いを指摘してもらう」という手段を取ることとしました。
気になる点がありましたらツッコミをいただけると嬉しいです。
記事の内容
Swiftで開発する際に、クラスを採用したほうが良いケースのまとめ。
インプット教材
書籍「Swift実践入門」第12章
開発環境
% swift -version
Apple Swift version 5.3.2 (swiftlang-1200.0.45 clang-1200.0.32.28)
% xcodebuild -version
Xcode 12.4
Swiftの開発は構造体(struct)が推奨されている
Swiftでの開発は、想定しない参照によるバグがないことや安全性の高さから構造体での開発を推奨されている。
とはいえ、依然としてクラスのほうが良いケースも存在するらしい。そのケースを理解していれば、それ以外は構造体で組めば良いことになるので、クラスの採用基準を注目し、記事にしてみた。
クラスを採用する基準
1.変更した値などをインスタンスに保持したいとき
例) タイマーアプリを実装する。ターゲットを構造体とクラスとで挙動を比較する。
- 参照元のプロトコルでターゲット先の型名"identifier"とカウント数"count"を定義。
- 拡張機能を使ってカウントを進める関数を実装。
- プロトコルに準拠した2つのターゲットを用意し、プロパティに値を設定。
- タイマーを実行する"Timer"を定義。今回は5秒。
- それぞれをインスタンス化しスタートを実行。終了後の"count"を確認。
protocol Target {
var identifier: String { get set }
var count: Int { get set }
mutating func action()
}
extension Target {
mutating func action() {
count += 1
print("id: \(identifier), count: \(count)")
}
}
struct ValueTypeTarget : Target {
var identifier = "Value Type"
var count = 0
init() {}
}
class ReferenceTypeTarget : Target {
var identifier = "Reference Type"
var count = 0
init() {}
}
struct Timer {
var target: Target
mutating func start() {
for _ in 0..<5 {
target.action()
}
}
}
//構造体のターゲットを登録してタイマーを実行
let valueTypeTarget: Target = ValueTypeTarget()
var timer1 = Timer(target: valueTypeTarget)
timer1.start()
valueTypeTarget.count
//クラスのターゲットを登録してタイマーを実行
let referenceTypeTarget = ReferenceTypeTarget()
var timer2 = Timer(target: referenceTypeTarget)
timer2.start()
referenceTypeTarget.count
出力結果
id: Value Type, count: 1
id: Value Type, count: 2
id: Value Type, count: 3
id: Value Type, count: 4
id: Value Type, count: 5
0 // コピーを作っただけなので値は共有されていない
id: Reference Type, count: 1
id: Reference Type, count: 2
id: Reference Type, count: 3
id: Reference Type, count: 4
id: Reference Type, count: 5
5 // 値を共有している
構造体は値型であるため、ターゲットのコピーがインスタンスになる。つまり、ターゲットのプロパティの値が変更されても(countの値が5に更新されても)、インスタンスの値はコピーした時点での値のままである。
対して、クラスは参照型なのでインスタンスの値はターゲットと共有している。スタート実行後に値を確認すると反映されているのがわかる。
タイマーアプリのような、実行後の値をインスタンスにも共有したい場合などにはクラスが適切である。
2.デイニシャライザを実装したいとき
クラスにしかない機能にデイニシャライザがある。デイニシャライザは型がnilになった時に実行される機能である。
例) 一時ファイルを作成し、削除する。
- ファイルの状態を表現する変数を定義
- インスタンス生成時にファイル作成、インスタンス削除時にファイル削除を定義したクラスを定義
- ファイルを生成、削除を実行し挙動の確認
var temporaryData: String?
class SomeClass {
init() {
print("Create a temporary data")
temporaryData = "a temporary data"
}
deinit {
print("Clean up the temporary")
temporaryData = nil
}
}
//一時ファイル作成
var someClass: SomeClass? = SomeClass()
temporaryData // "a temporary data"
// 削除
someClass = nil
temporaryData // nil
出力結果
Create a temporary data
Clean up the temporary
インスタンスの値がnilになるとデイニシャライザが実行されデータが削除される。
メモリに確保されたインスタンスやプロパティはARC(Automatic reference counting)によって自動的に破棄されるので通常はデイニシャライザを記述する必要はないが、ファイルの操作をする場合、自動でファイルを閉じたりしてくれないので、確実に閉じるためにデイニシャライザを利用するケースがある。
3.複数の型でプロパティの中身を共有できる
例) Animalクラスがあり、それを継承したクラスが複数ある。
Animalクラスにはownerプロパティがあり、プロパティオブザーバ(値がセットされた時に実行される機能)が定義されている。継承したクラスのインスタンスのプロパティに値をセットしたい。
class Animal {
var owner: String? {
didSet {
guard let owner = owner else { return }
print("\(owner) さんが飼い主になりました。")
}
}
}
class Bear : Animal {}
class Tiger : Animal {}
class WildEagle : Animal {}
// 値をセットするとプロパティオブザーバが実行
let bear = Bear()
bear.owner = "吉田沙保里"
出力結果
吉田沙保里 さんが飼い主になりました。
クラスの参照機能を使えば親クラスのプロパティを子クラスにも引き継ぐことができる。
これを構造型で実装しようとすると、少々面倒な処理になる。
// プロトコルではプロパティの宣言しかできない。
protocol Ownable {
var owner : String { get set }
}
// 継承した各クラスにプロパティの中身を書かなければならない。
struct Dog : Ownable {
var owner: String {
didSet {
print("\(owner)さんが飼い主になりました。")
}
}
}
struct Shark : Ownable {
var owner: String {
didSet {
print("\(owner)さんが飼い主になりました。")
}
}
}
struct Buffalo : Ownable {
var owner: String {
didSet {
print("\(owner)さんが飼い主になりました。")
}
}
}
// インスタンス化時に別の値を入れてから値を更新する
var buffulo = Buffalo(owner: "")
buffulo.owner = "吉田沙保里"
出力結果
吉田沙保里さんが飼い主になりました。
プロトコルでストアドプロパティやプロパティオブザーバは定義できない仕様になっているので、参照先の構造体で個別に定義してあげないといけない。また、プロパティオブザーバを実行させるためにインスタンス化時にいったん別の値を入れないといけない。
全然スマートじゃないので、参照元でストアドプロパティやプロパティオブザーバを定義したい場合はクラス型が良い。
まとめ
クラスを採用したほうが良いと判断する基準は、
- 参照先で更新されたプロパティをインスタンスにも共有させたいか
- デイニシャライザを使いたいか
- 参照元でプロパティを定義したいか。それを複数の参照先で共有したいか
になるのかなぁ、と書籍を読んで思いました。