Swift言語で struct と class の使い分け方を理解するために自分なりに理解した内容を記事にしてみました。
重要: この記事を投稿したのが2021年12月03日(金)で自身の法人サイトの方でしたけれども、公式サイトの更新などでブログ系のメンテナンスが大変だったので、ここに過去の投稿の移動とこれからの記事を投稿をする予定です。
概要
struct(構造体)と class は、アプリ内でデータを保存したり、動作をモデル化したりするのに適した選択肢ですが、その類似性から、どちらかを選択するのが難しい場合があります。
アプリに新しいデータタイプを追加する際には、以下の推奨事項を参考にして、どのオプションを選択するのが妥当かを検討してください。
- デフォルトでは struct を使用します。
- Objective-Cの相互運用性が必要な場合は、 class を使用してください。
- モデル化するデータのアイデンティティを管理する必要がある場合は、 class を使用します。
- プロトコルとともに struct を使用し、実装を共有することで動作を採用する。
デフォルトで構造を選択
struct を使って一般的な種類のデータを表現します。Swiftの構造体は、他の言語では class に限定される多くの機能を含んでいます。それらは、保存されたプロパティ、計算されたプロパティ、およびメソッドを含むことができます。さらに、Swiftの構造体は、デフォルトの実装によって動作を得るためにプロトコルを採用することができます。Swiftの標準ライブラリとFoundationでは、数値、文字列、配列、辞書など、頻繁に使用するタイプに構造体を使用します。
struct を使用すると、アプリ全体の状態を考慮することなく、コードの一部を簡単に推論することができます。struct は class とは異なり値型であるため、アプリのフローの一部として意図的に変更を伝えない限り、struct に対するローカルな変更はアプリの他の部分からは見えません。その結果、コードのあるセクションを見て、そのセクションのインスタンスに対する変更が、関連する関数の呼び出しから目に見えない形で行われるのではなく、明示的に行われることに自信を持つことができます。
Objective-Cの相互運用性が必要な場合は、クラスを使用する。
データを処理する必要のある Objective-C API を使用する場合や、Objective-C フレームワークで定義された既存のクラス階層にデータ・モデルを適合させる必要がある場合は、クラスとクラス継承を使用してデータをモデル化する必要があるかもしれません。例えば、多くのObjective-Cフレームワークでは、サブクラス化することが求められるクラスが公開されています。
アイデンティティを管理するためにクラスを使用する
Swiftのクラスは、参照型であるため、アイデンティティのビルトインの概念を持っています。これは、2つの異なるクラスインスタンスがそれらの保存されたプロパティのそれぞれに同じ値を持っているとき、それらはまだアイデンティティ演算子(===)によって異なるものとみなされることを意味します。また、アプリ全体でクラスのインスタンスを共有している場合、そのインスタンスに加えた変更は、そのインスタンスへの参照を保持しているコードのすべての部分で見ることができます。クラスは、インスタンスがこのような識別性を持つ必要がある場合に使用します。一般的な使用例としては、ファイルハンドル、ネットワーク接続、CBCentralManagerのような共有ハードウェアの仲介などがあります。
例えば、ローカルのデータベース接続を表す型がある場合、そのデータベースへのアクセスを管理するコードは、アプリから見たデータベースの状態を完全に制御する必要があります。この場合、クラスを使用することは適切ですが、アプリのどの部分が共有データベースオブジェクトにアクセスできるかを必ず制限してください。
重要: アイデンティティの取り扱いには注意が必要です。アプリ全体でクラスインスタンスを広く共有すると、ロジックエラーが発生しやすくなります。頻繁に共有されているインスタンスを変更した場合の結果は予想できないので、そのようなコードを正しく書くのはより大変です。
アイデンティティをコントロールできない場合の struct の使用
struct は、自分ではコントロールできないアイデンティティを持つエンティティに関する情報を含むデータをモデル化する場合に使用します。
例えば、リモートのデータベースを参照するアプリでは、インスタンスのアイデンティティは外部のエンティティが完全に所有し、識別子によって伝達される場合があります。アプリのモデルの一貫性がサーバーに保存されている場合、識別子を持つ構造体としてレコードをモデル化できます。以下の例では、jsonResponse には、サーバーからのエンコードされた PenPalRecord インスタンスが含まれています。
struct PenPalRecord {
let myID: Int
var myNickname: String
var recommendedPenPalID: Int
}
PenPalRecord のようなモデル タイプのローカルな変更は便利です。たとえば、アプリがユーザーのフィードバックに応じて複数の異なるペンパルを推奨する場合があります。PenPalRecord 構造は、基礎となるデータベース レコードの ID を制御しないため、ローカルの PenPalRecord インスタンスに加えられた変更が、データベース内の値を誤って変更するリスクはありません。
アプリの他の部分がmyNicknameを変更してサーバに変更要求を送り返しても、直近に拒否されたペンパルの推薦が誤って変更されて拾われることはありません。myIDプロパティは定数として宣言されているため、ローカルでは変更できません。そのため、データベースへのリクエストで間違ったレコードを変更してしまうことはありません。
構造体とプロトコルを使って、継承と動作の共有をモデル化する
struct と class は、どちらも一種の継承をサポートしています。 struct とプロトコルはプロトコルのみを採用することができ、 class を継承することはできません。しかし、 class の継承で構築できる継承階層は、プロトコルの継承や struct でもモデル化することができます。
継承関係をゼロから構築する場合は、プロトコル継承をお勧めします。プロトコル継承では、クラス、構造体、列挙体が継承に参加できますが、クラス継承では、他のクラスとの互換性しかありません。データをどのようにモデル化するかを決める際には、まずプロトコル継承でデータ型の階層を構築し、次に struct にプロトコルを採用してみてください。