Swiftにおける構造体(struct)vsクラス(class)問題に向き合ってみた


項目 構造体 クラス
タイプ 値型 参照型
格納型プロパティ
計算型プロパティ
スタティックプロパティ
クラスプロパティ ×
メソッド
スタティックメソッド
クラスメソッド ×
継承 ×
プロトコル準拠
拡張
メモリリーク回避
スレッドセーフ

Swiftにおける構造体とクラスの性質の違い、できる・できないことをまとめるとこんな感じになると思います。これを踏まえつつ、この記事ではアップルさんの見解をもとに構造体とクラスの使い分けの方針についてつづります。(結論を先に知りたい方はここ)

構造体とクラスの選択についてのアップルさんの見解

原文はこちらのリンクにあります。
https://developer.apple.com/documentation/swift/choosing_between_structures_and_classes

原文のみorその日本語直訳とかは眠すぎる内容になると思ったので、原文と筆者による日本語要約(要約でも眠さは残る)を載せます。大筋of大筋としてはSwiftでは構造体推し!ってことを言いたいのだと思います。

Overview

Structures and classes are good choices for storing data and modeling behavior in your apps, but their similarities can make it difficult to choose one over the other.

Consider the following recommendations to help choose which option makes sense when adding a new data type to your app.

  • Use structures by default.
  • Use classes when you need Objective-C interoperability.
  • Use classes when you need to control the identity of the data you're modeling.
  • Use structures along with protocols to adopt behavior by sharing implementations.

概要

アプリのデータや処理をまとめておくのに構造体やクラスは便利ですが、両者は似ているためどちらを使えばいいのか迷います。そんな時は判断基準として以下がおすすめです。

  • まずは構造体を使ってみる
  • Objective-Cやモデル化したデータの操作が必要ならクラスを使う
  • 実装を共有したいなら構造体はプロトコルと一緒に使う

Choose Structures by Default

Use structures to represent common kinds of data. Structures in Swift include many features that are limited to classes in other languages: They can include stored properties, computed properties, and methods. Moreover, Swift structures can adopt protocols to gain behavior through default implementations. The Swift standard library and Foundation use structures for types you use frequently, such as numbers, strings, arrays, and dictionaries.

Using structures makes it easier to reason about a portion of your code without needing to consider the whole state of your app. Because structures are value types—unlike classes—local changes to a structure aren't visible to the rest of your app unless you intentionally communicate those changes as part of the flow of your app. As a result, you can look at a section of code and be more confident that changes to instances in that section will be made explicitly, rather than being made invisibly from a tangentially related function call.

まずは構造体を使ってみる

Swiftの構造体は、他の言語ではクラスにしかできないような性質(プロパティとかメソッドを持ったりなど)を持っており、プロトコルを使って既定の実装に応じた振る舞いも可能です。

また、構造体は値型なので、ローカルで変更を加えても変更した部分以外には影響しません。アプリ全体の状態を気にすることなく、一部のコードを扱うことができるわけです。

Use Classes When You Need Objective-C Interoperability

If you use an Objective-C API that needs to process your data, or you need to fit your data model into an existing class hierarchy defined in an Objective-C framework, you might need to use classes and class inheritance to model your data. For example, many Objective-C frameworks expose classes that you are expected to subclass.

Objective-Cが必要ならクラスを使う

データ処理にObjective-CのAPIが必要だったり、Objective-Cのフレームワークで書かれたクラスにモデルを当てはめたいのであれば、クラスやクラス継承を使う必要があるかもしれません。

Use Classes When You Need to Control Identity

Classes in Swift come with a built-in notion of identity because they're reference types. This means that when two different class instances have the same value for each of their stored properties, they're still considered to be different by the identity operator (===). It also means that when you share a class instance across your app, changes you make to that instance are visible to every part of your code that holds a reference to that instance. Use classes when you need your instances to have this kind of identity. Common use cases are file handles, network connections, and shared hardware intermediaries like CBCentralManager.

For example, if you have a type that represents a local database connection, the code that manages access to that database needs full control over the state of the database as viewed from your app. It's appropriate to use a class in this case, but be sure to limit which parts of your app get access to the shared database object.

Important

Treat identity with care. Sharing class instances pervasively throughout an app makes logic errors more likely. You might not anticipate the consequences of changing a heavily shared instance, so it's more work to write such code correctly.

モデル化したデータを操作したいならクラスを使う

Swiftのクラスは参照型なので、クラスインスタンスに変更を加えれば、アプリ内のそのインスタンスの参照部分すべてに影響します。このような性質を求めるならクラスを使いましょう。

ただ、クラスインスタンスを共有することで、ロジックエラーなどの予期せぬ結果を招くことがあるのでクラスは慎重に扱いましょう。

Use Structures When You Don't Control Identity

Use structures when you're modeling data that contains information about an entity with an identity that you don't control.

In an app that consults a remote database, for example, an instance's identity may be fully owned by an external entity and communicated by an identifier. If the consistency of an app's models is stored on a server, you can model records as structures with identifiers. In the example below, jsonResponse contains an encoded PenPalRecord instance from a server:

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

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

Local changes to model types like PenPalRecord are useful. For example, an app might recommend multiple different penpals in response to user feedback. Because the PenPalRecord structure doesn't control the identity of the underlying database records, there's no risk that the changes made to local PenPalRecord instances accidentally change values in the database.

If another part of the app changes myNickname and submits a change request back to the server, the most recently rejected penpal recommendation won't be mistakenly picked up by the change. Because the myID property is declared as a constant, it can't change locally. As a result, requests to the database won't accidentally change the wrong record.

モデル化したデータを操作したくないなら構造体を使う

変更したくないデータをモデル化するなら構造体を使いましょう。

リモートDBと接続するアプリでは、サーバー上でモデルの一貫性を保つなら、識別情報を持たせた構造体としてレコードをモデル化することができます。

コード例にあるPenPalRecordは構造体なので、ローカルで変更を加えてもDB内のレコードを操作することがなく、不意にDB内の値が変わってしまうといったリスクがありません。

Use Structures and Protocols to Model Inheritance and Share Behavior

Structures and classes both support a form of inheritance. Structures and protocols can only adopt protocols; they can't inherit from classes. However, the kinds of inheritance hierarchies you can build with class inheritance can be also modeled using protocol inheritance and structures.

If you're building an inheritance relationship from scratch, prefer protocol inheritance. Protocols permit classes, structures, and enumerations to participate in inheritance, while class inheritance is only compatible with other classes. When you're choosing how to model your data, try building the hierarchy of data types using protocol inheritance first, then adopt those protocols in your structures.

モデルを継承したり振る舞いを共有したいなら構造体とプロトコルを使う

構造体はクラスを継承することができませんが、プロトコル継承と構造体を使えばクラス継承のようなことが可能です。

データをモデル化する際は、プロトコル継承を使って型の階層を作り、それからそのプロトコルを構造体に準拠させるようにしましょう。

つまり、

筆者の知識不足でたどたどしい要約もありますが、以上がアップルさんの見解です。これを踏まえてまとめると、以下の2つの場合はクラスを使ってそれ以外は構造体を使いましょう、ということだと思います。

  • Objective-CのAPIを使ったり、そのAPIを含む標準フレームワークのクラス(UIViewControllerなど)を継承したりしたい
  • モデル化したデータをオブジェクト間で統一的に変更(参照渡しが適切)しやすくしたい

思えばSwiftを勉強したての頃もこの構造体vsクラス問題に悩み、実務経験を5ヶ月ほど積んだ今も悩んでます。多分これからも悩むんでしょうね。ただ、Swiftには構造体を扱いやすくするプロトコルなどの仕組みが多数用意されているので、構造体を使える場面がないか積極的に探していきたいなあと思った次第です。