[Swift] struct, class どちらを使うべき?

Swift

     

TAGS:

⌛️ 2 min.
struct と class の使うべきシーンを考えていて、良い資料を見つけたので 和訳しつつ説明してみます。

元ネタ

Apple のドキュメント こちら がネタ元です。

MEMO

自分の理解を深めるために行った和訳なので、厳密に確認したいときは、上記のドキュメントを参照ください。

struct と class の選択基準

overview

struct と class は、データを保存することができ、振る舞いをモデリングするために使用されます。しかし、struct と class は類似点もあるため、どちらを選択すべきか悩む時があります。

次のような推奨基準を考慮して選択するとより良い選択ができます。

  • struct を使うことを最初に検討しましょう
  • Objective-C との相互接続性が必要であれば、class を使いましょう
  • オブジェクトの同一性を制御する必要があるならば、class を使いましょう
  • 実装を共有することによる振る舞いの共有は、protocol と struct を組み合わせて使いましょう

struct を使うことを最初に検討しましょう

ある共通の構造を持つデータを表現するのであれば、struct を使いましょう。
Swift での struct は、他言語では class でないと持てないような機能も持っています。具体的には、次のような機能です。”stored properties”, “computed properties”, “methods”。
それ以上に、Swift での struct は、protorocl を使うことで、デフォルトの実装を提供することができます。
Swift standard library と (Apple の提供する)Foundation は、Number, String, Array 等 多くの部分で struct を使用しています。

struct を使うことで、アプリ全体のコードを確認することなく 局所的なコードのみでコードの振る舞いを簡単に把握できるようになります。
なぜかというと、struct は value-type であり、その点で class と異なるからです。
struct に行われた変更は局所的であり、(意図的に外部から見えるようにしない限り)アプリの他の箇所からはみることができません。
その結果として、見える範囲の局所的なコードのみでどのような変更が行われているかを理解することが可能となります。
見えない箇所からの変更を考慮する必要があるコードでは簡単に理解することはできません。

<訳註>
Apple のビデオで言うところの Local reasoning です。

Objective-C との相互接続性が必要であれば、class を使いましょう

Objective-C の API でデータを処理しなければいけない場合や、Objective-C で作成されたフレームワークで定義したクラス階層にデータを定義しなければいけない時には、class を使う必要があります。
例えば、多くの Objective-C フレームワークは、継承することを想定して多くの class を提供しています。

オブジェクトの同一性を制御する必要があるならば、class を使いましょう

Swift の class は、それが reference-type であるため、同一性についてデフォルトでの実装が用意されています。
このことは、ある class の2つのインスタンスが 同一の値を持っていたとしても、== (同一性判断) は、偽になることを意味しています。
言い換えると、アプリ全体で 1つのインスタンスを共有した時には、アプリの一箇所で行ったインスタンスへの変更は、アプリの別の箇所からも見えてしまうことを意味します。
このような要求があるときには、class を使いましょう。
一般的なユースケースとしては、ファイルハンドル・ネットワーク接続・CBCentralManager のような 共有ハードウェアメディアを扱うのに適しています。

例えば、ローカルの DB への接続を表すタイプを考える時には、データベースへのアクセスをコントロールするためには その状態がアプリ全体から見えるようになっていないといけません。
このケースでは、class を使用することが適切です。しかし、class を使用して表現されたデータベースオブジェクトへのアクセスは、アプリの適切な部分からのみ行われるように制限しないといけません。

注意

同一性の扱いは気をつけて扱うべきトピックです。
class インスタンスをアプリ全体で共有することは、ロジックエラーを誘発します。
共有されたインスタンスへの変更がどのような範囲にまで及ぶかを予想・制御することは、一般に非常に困難です。
つまり、インスタンスを共有するコードを書く際には、いつも以上に気をつけて記述する必要があります。

オブジェクトの同一性を制御しないのであれば、struct を使いましょう

同一性の制御を必要としないのであれば、struct を使いましょう。

例えば、リモートDBへアクセスするアプリでは、要素の同一性は 外部(リモートDB)が管理していて、ID 等でやりとりされるでしょう。
サーバー上でモデルの一貫性が確保されているのであれば、sturct と ID でモデルを作ることが可能です。
以下の例では、jsonRespnse が サーバーから取得した PenPalRecord という encode されたインスタンスを保持しています。


struct PenPalRecord {
    let myID: Int
    var myNickname: String
    var recommendedPenPalID: Int
}

var myRecord = try JSONDecoder().decode(PenPalRecord.self, from: jsonResponse)

PenPalRecord のようなモデルへの局所的な変更は非常に便利に使えます。
例えば、アプリは複数の異なる属性値を持つ PenpalRecord をユーザーへのフィードバック「おすすめ」として提示するかもしれません。
そのような時、PenPalRecord は、struct であり同一性を制御していないので、ローカルの PenPalRecord インスタンスに変更を行なっても、データベース中の要素が意図せず変更されてしまうことはありません。

アプリの別の箇所で PenPalRecord のプロパティの1つである myNickName が変更され その変更がデータベースに反映されたとしても、先に行なったようなおすすめを提示するためにおこなった変更が誤って変更として採用されることはありません。なぜならば、myID プロパティは、constant として定義されているので、ローカルに変更することはできないからです。結果として、変更対象が誤って変更されることは起こり得ません。

実装を共有することによる振る舞いの共有は、protocol と struct を組み合わせて使いましょう

struct も class も継承の”ような”ものをサポートします。
struct と protocol は、protocol にのみ adopt できます。struct や enum は、class から継承することはできません。
しかし class による継承で実現できることは、protocol の継承と struct によって実装することができます。

もし、継承関係をスクラッチから設計できるケースであるならば、protocol による継承を検討しましょう。
Protocol は、struct,class,enum が継承関係に参加することができるものです。class による継承は、class にのみ許されます。
データモデリングを検討している時には、protocol (とその継承)を最初に検討してください。その後、struct に adopt させます。

Apple のドキュメントはここまでですが、自アプリ向けに検討したことをメモしておきます。

モデルレイヤーにつかうべきは、struct/class のどちら?

上記の文章を見つけるまでは、以下の問題を考え続けていました。

・Model レイヤーは、struct, class のどちらを使ってモデリングするべきか

struct/class の使い分け 選択基準(追加):関係性

Model レイヤーで使うことを考えると 上記で説明された選択基準の他に もう1つの基準が出てきます。

  • 要素間の関係性をモデリングすることが必要か?

struct でデータをモデリングすると、データ間の関係性の属性を作ることができません。

Book と Author という struct を作って見てみます。

Author は、著書として 複数の Book を持つことができ、Book は、著者として 1つの author を持ちます。


struct Book {
    var title: String
    var author: Author
}
struct Author {
    var name: String
    var books: [Book]
}

上記のように定義はできるのですが、実際に設定していくと破綻していきます。

やってみましょう。


var author = Author(name: "me", books: [])      // (1)
var book = Book(title: "book1", author: author) // (2)
print(book)               // (3)
print(author)             // (4)
author.books.append(book) // (5)
print(author)             // (6)
print(book)               // (7)

// printout
// from (3)
Book(title: "book1", author: __lldb_expr_8.Author(name: "me", books: []))
// from (4)
Author(name: "me", books: [])
// from (6)
Author(name: "me", books: [__lldb_expr_8.Book(title: "book1", author: __lldb_expr_8.Author(name: "me", books: []))])
// from (7)
Book(title: "book1", author: __lldb_expr_8.Author(name: "me", books: []))
  1. Author を “me” という name で作ります。(このとき Book は存在しないので books は空です)
  2. Book を “book1” という title で作ります。先に作った “me” という name を持つ author を設定しました
  3. print で確認すると、book は author 情報を持っています。
  4. author を print してみると、book が空であることがわかります。
  5. book はもう作られているので、author に追加することにします。
  6. author を print してみます。author の books に “book1” という book を含めることができました。しかし よくみると books の中の book が持つ author の持つ books は空です。
  7. book を再度 print してみると、”book1″ の author は “me” になっていますが、”me” という author の books は空のままです。

struct は value-type であるために、あるプロパティに設定するとコピーが作成されてセットされます。この動作により、同じ name を持つが books が異なる Author や同じ title を持つが author の異なる Book が複数作られていくことになります。
具体的には、Book を作成する時に使用した author と その後 books に要素を追加した author は、プログラムのコード上は、同じ変数で表されていますが、別の struct になっています。

# Apple の説明にあった 「struct への変更は局所的である」という説明は、この現象を別方向から説明していることになります。

実は、関係性を定義することは 本質的には Apple のドキュメントで触れられている 「同一性を制御したいか」ということと同一であることに行き着くことにもなります。

まとめ:struct と class の選択基準

struct と class の選択基準
  • struct を使うことを最初に検討しましょう
  • Objective-C との相互接続性が必要であれば、class を使いましょう
  • オブジェクトの同一性を制御する必要があるならば、class を使いましょう
  • 実装を共有することによる振る舞いの共有は、protocol と struct を組み合わせて使いましょう
  • オブジェクト間の関係性を扱う必要があるならば、class を使いましょう (NEW !)

説明は以上です。
不明な点やおかしな点ありましたら、こちらまで。

Swift おすすめ本

struct, class の使い分け以外にも Swift は非常に深く考えて設計された言語だと感じる仕様が多くあります。

Swift を深く理解するには、以下の本がおすすめです。

2 COMMENTS

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です