読者です 読者をやめる 読者になる 読者になる

cockscomblog?

cockscomb on hatena blog

Swiftにおけるclassとstructの使い分け

新年あけましておめでとうございます。

class, struct

Swiftにはclass (class)の他にstructure (struct)があり、どちらもよく似た機能を提供する。しかしそれぞれ参照型 (reference type)と値型 (value type)という違いがあり、このことはパラダイムの違いをもたらす。そこで多くのSwiftプログラマーは、classとstructのどちらを採用するべきか迷いがちである。本記事ではこの問題について議論を深めたい。

structはカッコいい

classについてはなじみ深いと思うので、structの特徴を整理する。はじめに述べたようにstructは値型である。値型であることがstructを大きく特徴付けている。

structはデフォルトで不変である。var, mutating, inoutのキーワードを用いることで、この不変であるという挙動を変えられる。varで宣言された変数に保持されるstructは、stored propertyをletではなくvarで宣言することで、可変の状態を持つことができる。mutatingとして宣言されたメソッドは自身のstored propertyを変更できる。関数のinoutで宣言された引数は、&で目印された変数を受け取って、そのstored propertyを変更して返すことができる。このようにデフォルトで不変であることが、意図せずに内部状態を変更することを防ぐ。

structは値であるから、何かするたびに毎回コピーされる。別な変数に代入されるときや、関数の引数として渡されるときなどに、いちいち新しくコピーされる。つまり可変のstored propertyがあったとしても、変数間で状態が共有されることは原則としてない。唯一の例外はinoutで、関数の引数がinout宣言されている場合には、関数の内外で共有されているように見える。

これらを大きな特徴とするstructは、この他にもclassと違って継承関係を持つことはなく、またメモリ管理上もclassとは異なる。継承関係はないがprotocolに準拠することはでき、protocol extensionによってmix-inのように何らかの実装を他の型と共有できる。classのメモリ管理が、自動的に参照カウントを増減させるARCであるのに対して、structは値であるからより単純に破棄される。加えてclassには存在するdeinitializerがない。

これらがstructの特徴である。一言でまとめると、カッコいいのである。なるべくならカッコいいstructの方を使いたいのである。

classとstructureのどちらを選ぶか

structが使いたいわけではあるが、ほんとうにいつでもstructを使えるのであろうか。structを使っていいかどうか、経験によって学ぶしかないのだろうか。その答えは用意されている。“Choosing Between Classes and Structures” と題された節が、Appleによるドキュメント “The Swift Programming Language” にある。そこには以下のように条件が載せられている。

As a general guideline, consider creating a structure when one or more of these conditions apply:

  • The structure’s primary purpose is to encapsulate a few relatively simple data values.
  • It is reasonable to expect that the encapsulated values will be copied rather than referenced when you assign or pass around an instance of that structure.
  • Any properties stored by the structure are themselves value types, which would also be expected to be copied rather than referenced.
  • The structure does not need to inherit properties or behavior from another existing type.

(中略、いくつかの具体例)

In all other cases, define a class, and create instances of that class to be managed and passed by reference. In practice, this means that most custom data constructs should be classes, not structures.

ここで挙げられているような条件に当てはまる場合にはstructを用い、そうでないほとんどの場合はclassを用いるべきであると書かれている。ひどく保守的な条件と感じた読者も多いだろう。

実際にこの条件を意識して例を書いてみよう。

struct ScreenSize {
    let width: Int
    let height: Int
}

let fullHD = ScreenSize(width: 1920, height: 1080)

画面の解像度を表現したstruct

この例は「比較的単純ないくつかのデータをまとめた」ものであり、明確に先の条件に当てはまる。何の文句もなくstructでよいだろう。

struct Counter {
    var count = 0

    mutating func countUp() {
        count += 1
    }
}

var counter = Counter()
print(counter.count)
counter.countUp()
print(counter.count)

カウンターをstructにしたもの

これは「インスタンスが代入や関数に渡した際に参照されるのではなくコピーされることが期待されて適当」と言えるだろうか。むしろカウンターの状態が共有されないことが不自然に感じられるかもしれない。classの方がよいのではないだろうか。

struct Observing {
    var observer: AnyObject? = nil
    var noticed = false

    init() {
        observer = NSNotificationCenter.defaultCenter().addObserverForName("MyNotification", object: nil, queue: nil) { (notification) -> Void in
            print("Noticed!")
            self.noticed = true
        }
    }
}

var observing = Observing()
NSNotificationCenter.defaultCenter().postNotification(NSNotification(name: "MyNotification", object: nil))
print(observing.noticed)

structがNSNotificationCenterに登録する例

この例は信じられないほど全くあり得ない例である。structのイニシャライザで作られたクロージャが自身を参照し、破壊的な変更をしている。このコードは期待される動作にはならない。クロージャは参照型であり、クロージャによって参照されたstructはコピーされ、このようにクロージャでstructの状態を変更することには何の意味もない。

struct ScreenSize {
    let width: Int
    let height: Int
}

class Box<Type> {
    let value: Type

    init(_ value: Type) {
        self.value = value
    }
}

let fullHD = ScreenSize(width: 1920, height: 1080)
performSegueWithIdentifier("MySegue", sender: Box(fullHD))

structをAnyObjectに一致させるためのBox classを作る

UIViewControllerperformSegueWithIdentifier(_:sender:)は、ドキュメントによると第二引数senderに対して

The object that you want to use to initiate the segue. This object is made available for informational purposes during the actual segue.

とあり、遷移を初期化するために何らかのオブジェクトを渡してよいことになっている。これはAnyObject?型にstructはそのまま渡せない (structはAny型ではあるが、AnyObject型ではない) ので、Box classを作ってラップしている例である。ScreenSizeはstructにするのに適していたが、Objective-Cで書かれたフレームワークとの兼ね合いでは余計な手間がかかるようになってしまった。この箇所だけであればこういったワークアラウンドも悪くないが、もしプログラムのあちこちにこういったワークアラウンドが出現することになったら、そもそも不変なstored propertyを持つclassで定義しておけばよかっただけかもしれない。

2016/09/12追記

Swift 3 での改善

SE-0116 Import Objective-C id as Swift Any type により、Swift 3 では Objective-CidAny 型としてインポートされるようになった。したがってこの例のような API では sender に struct を利用できるようになり、ワークアラウンドしなくてもよい。Swift 3 以降においてはこのようなケースで struct が使いやすくなっている。

Swiftにおけるclassとstructの使い分け

こうして見ると、カッコいいstructはただカッコいいというだけでは使いにくいことがわかる。ドキュメントの通り、多くの場合はclassを使うことが適切かもしれない。これを読んでいる諸氏は当然読んでいるはずのAppleの“The Swift Programming Language”は、Swiftを学ぶ者のために書かれたものであって、Swiftグルの読者諸氏には関係ないとも言える。

実際問題としては、現実的ないくつかの指針を挙げることができるだろう。いまclassかstructかで作ろうとしているものが、共有されるべき状態を持つならclassを選ぶべきだし、そうでなければstructが好ましいかもしれない。Objective-Cのコードとやりとりすることが想定されるなら最初からclassにしてしまってもよいだろう。classやクロージャのような参照型の要素をstored propertyなどに保持し、これと密にやりとりするならclassが好ましい。何らかの副作用を持つようなものを抽象化する場合には、コピーされるより参照される方が適切であることが多い。

classとstructの使い分けは難しい問題である。カッコいいstructを使うためには、それに合わせたカッコいい設計も必要だろう。そもそも可変なインスタンスを減らす方がよいだろうし、状態が共有されずに済むように作れるならそのほうがよい。


本記事は、関西モバイルアプリ研究会 #9での発表を元に書かれている。

2016年もこういったことをチマチマと考えながら生きていきたい。本年もどうぞよろしくお願い申し上げます。

詳解 Swift 改訂版

詳解 Swift 改訂版