この記事は何?
Appleの開発者向けドキュメントのChoosing Between Structures and Classesについて、独自に解説する。
データの保存方法とモデルの動作を決定するために、「構造体とクラスのどちらを利用すべきか」を判断する。
Swiftを基礎から学ぶには
自著、工学社より発売中の「まるごと分かるSwiftプログラミング」をお勧めします。変数、関数、フロー制御構文、データ構造はもちろん、構造体からクロージャ、エクステンション、プロトコル、クロージャまでを基礎からわかりやすく解説しています。
概要
構造体とクラスはアプリにデータを保存し、動作をモデル化する手段。
どちらも良い方法だが、それらは似通っている部分があるので、どちらか一方を選択する判断が難しいことがある。
アプリに新しいデータ型を追加する際は、以下にあげる項目を考慮して、構造体とクラスのどちらが合理的かを判断する。
- 基本的には構造体
- Objective-Cの相互運用性が必要な場合はクラス
- モデリングするデータのアイデンティティを制御する必要がある場合はクラス
- 実装を共有するプロトコルと共に利用する場合は構造体
基本は構造体
構造体を使用して、一般的な種類のデータを表す。
Swiftの構造体には、他の言語ではクラスに限定された機能もたくさんある。
格納プロパティ、計算プロパティ、およびメソッドを利用できる。
さらに、Swiftの構造体は、デフォルト実装を通じて動作を得るためにプロトコルを採用できる。
Swift標準ライブラリとFoundation
フレームワークは数値、文字列、配列、辞書など頻繁に使用するほとんどの型に構造体を使用している。
構造体を使用すると、アプリ全体を把握しなくても、コードを部分的に推論しやすくなる。
クラスとは異なり、構造体は値型データである。
そのため、構造体でのローカルな変更は、アプリのフローの一部として意図的にそれらの変更を伝えない限り、他の部分には見えない。
そのおかげで、ある部分のコードを見るだけで、そこで行われるインスタンスへの変更が、関連する関数の呼び出しから意図せず行われることなく、明示的に行われていると確信できる。
Objective-C互換が必要ならクラス
データの処理が必要なObjective-C APIを使用している場合、またはデータモデルを「Objective-Cのフレームワークで定義された既存のクラス階層」に適合させる必要がある場合は、データをモデル化するためにクラスと継承を使用する。
たとえば、多くのObjective-Cフレームワークが公開するクラスの多くは、サブクラス化できる。
識別制御が必要ならクラス
Swiftのクラスは参照型データなので、アイデンティティの概念がある。
つまり、2つの異なるクラスインスタンスが互いに同じ値のプロパティを持つ場合でも、「アイデンティティ演算子===
はそれらを異なると見なす」ことを意味する。
また、アプリ全体でクラスインスタンスを共有すると、そのインスタンスに加えた変更は、そのインスタンスへの参照を保持するコードのすべての部分に表示される。
この種のアイデンティティが必要なインスタンスを作成するには、クラスを使ってモデル化する。
一般的なユースケースは、ファイルハンドル、ネットワーク接続、およびCBCentralManagerのような共有ハードウェアの中継。
たとえば、ローカルデータベース接続をモデル化するデータ型がある場合、そのデータベースへのアクセスを管理するコードは、アプリから見たデータベースの状態を完全に制御する必要がある。
この場合、クラスを使用するのが適切だが、アプリのどの部分が共有データベースオブジェクトにアクセスするかを制限する。
重要
アイデンティティの扱いは慎重すべき。
アプリ全体でクラスインスタンスを広範囲に共有すると、ロジックエラーの可能性が高くなる。
過度に共有されたインスタンスの変更は、その結果を予想しえなくなる。
それを適切にコーディングすることは、負担の大きな作業になる。
識別不要なら構造体
アイデンティティの制御がないエンティティのデータをモデル化する場合は、構造体を使用する。
たとえば、リモートデータベースを参照するアプリでは、インスタンスのアイデンティティは外部エンティティによって完全に所有され、識別子によって通信できるかもしれない。
アプリのモデルの一貫性がサーバーに保存されている場合は、識別子を持つ構造体としてレコードをモデル化できる。
以下の例では、jsonResponse
にはサーバーからエンコードされたPenPalRecord
型インスタンスが含まれている。
struct PenPalRecord {
let myID: Int
var myNickname: String
var recommendedPenPalID: Int
}
var myRecord = try JSONDecoder().decode(PenPalRecord.self, from: jsonResponse)
PenPalRecord
のようなモデルデータへの変更は簡単。
たとえば、ユーザーのフィードバックに応答して、アプリは複数のペンパルをお勧めするかもしれない。
PenPalRecord
型は構造体なので、データベースのレコードごとにアイデンティティを制御しない。
そのため、PenPalRecord
型インスタンスに加えられた変更が、データベースを意図せずに変更するリスクはない。
アプリの別の部分がmyNickname
を変更し、変更リクエストをサーバーに送信した場合、最近拒否されたペンパルの推奨事項が変更によって誤って取り上げられることはない。
myID
プロパティは定数として宣言されているため、ローカルでは変更できない。
そのおかげで、データベースへのリクエストが意図せずに、不適切なレコードを変更することはない。
動作の共有とモデルを継承するために、構造体とプロトコルを使用する
構造体とクラスは、どちらも継承の類をサポートしている。
構造体とプロトコルが採用できるは、プロトコルだけ。
クラスを継承することはできない。
ただし、クラス継承で構築できる継承階層は、プロトコル継承と構造体を使用してモデル化できる。
継承関係をゼロから構築する場合は、プロトコル継承が好ましい。
プロトコルはクラス、構造体、および列挙型が継承に加わることを許可する。
ただし、クラス継承は他のクラスとのみ互換性がある。
データをモデル化する方法を選択するときは、まずプロトコル継承を使用してデータ型の階層を構築してみる。
それから、それらのプロトコルを構造体に採用してみる。