新年あけましておめでとうございます。
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 Counter { var count = 0 mutating func countUp() { count += 1 } } var counter = Counter() print(counter.count) counter.countUp() print(counter.count)
これは「インスタンスが代入や関数に渡した際に参照されるのではなくコピーされることが期待されて適当」と言えるだろうか。むしろカウンターの状態が共有されないことが不自然に感じられるかもしれない。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)
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))
AnyObject
に一致させるためのBox
classを作る
UIViewController
のperformSegueWithIdentifier(_: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-C の id
が Any
型としてインポートされるようになった。したがってこの例のような 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年もこういったことをチマチマと考えながら生きていきたい。本年もどうぞよろしくお願い申し上げます。
- 作者: 荻原剛志
- 出版社/メーカー: SBクリエイティブ
- 発売日: 2015/12/25
- メディア: 単行本
- この商品を含むブログ (1件) を見る