SwiftUI をしっかり理解しようとすると、これまで意識しなくて良かった多くの言語機能を知る必要があり、なかなか苦労します。 その 1 つとして、型消去(Type Erasure)があります。 ずっとわかったようなわからないような感じだったので、腰を据えて調べてみました。 まだこれについては自信がないので、ご指摘などあれば GitHub から頂けるとうれしいです。
サンプルは GitHub に置きました。
Swift では、associatedtype
を持つプロトコルは、型宣言に利用できません。
以下のコードでは、最下部で Store
型の変数を宣言しようとしていますが、コンパイルが通りません。
これは、Store
の持つ associatedtype
の型が確定しないためです。
// お店で扱う商品の種類
struct Drug {
var item: String
}
/**
何らかのお店を表すプロトコル.
商品の種類は associatedtype により柔軟に指定できるようにしている.
*/
protocol Store {
associatedtype T
var kind: T { get }
func kindsOfStore() -> T
}
/**
具体的なお店(マツモトキヨシ).
associatedtype は Drug として確定.
*/
class MatsumotoKiyoshi: Store {
var kind: Drug
init(kind: Drug) {
self.kind = kind
}
func kindsOfStore() -> Drug {
return kind
}
}
/**
具体的なお店(赤ひげ)
associatedtype は Drug として確定.
*/
class AkaHige: Store {
var kind: Drug
init(kind: Drug) {
self.kind = kind
}
func kindsOfStore() -> Drug {
return kind
}
}
/**
MatsumotoKiyoshi は Store プロトコルに準拠しているので問題なさそうだが、
associatedtype を持つのでこの宣言はできない.
*/
var myStore1: Store = MatsumotoKiyoshi(kind: Drug(item: "絆創膏"))
associatedtype
を持ったオブジェクトを別の型にラップして、associatedtype
をジェネリクスで表現できるようにしてみます。こうすることで、Store
プロトコル型ではないものの、型宣言からは MatsumotoKiyoshi
という具体的な型の情報を消去し、抽象的な AnyStore
型として表現できるようになります。
/**
AnyStore は、Store に準拠したクラスの associatedtype をジェネリクスとして持つ.
*/
class AnyStore<T>: Store {
// kind の型は、イニシャライザで決まる.
var kind: T
/**
引数として、Store に準拠したオブジェクトを受け取る.
where 句で、AnyStore の associatedtype T を引数で受け取ったオブジェクトの
T と同じ型としている.
*/
init<S: Store>(store: S) where T == S.T {
self.kind = store.kind
}
func kindsOfStore() -> T {
return kind
}
}
/**
具体型である MatsumotoKiyoshi ではなく、抽象型である AnyStore 型として宣言できている.
このように、型消去により具体型情報を消去できていることがわかる.
*/
var myStore2: AnyStore<Drug>
myStore2 = AnyStore(store: MatsumotoKiyoshi(kind: Drug(item: "絆創膏")))
print(myStore2.kindsOfStore())
以下のように、配列の宣言としてももちろん利用可能です。
/**
Store に準拠し、associatedtype として Drug を持つオブジェクトを格納できる配列.
型消去により、`MatsumotoKiyoshi` だけでなく `AkaHige` も格納可能.
*/
var stores: [AnyStore<Drug>]
stores = [
AnyStore(store: MatsumotoKiyoshi(kind: Drug(item: "歯ブラシ"))),
AnyStore(store: AkaHige(kind: Drug(item: "毒マムシドリンク"))),
]
for store in stores {
print(store.kindsOfStore())
}
実際には、Swift 5.1 で導入された Opaque Types により変数の宣言時に some Store
のように表現できるようになりました。
しかし、Opaque Types のままでは抽象型なので利用できない場面(たとえば、配列の宣言に [some Store]
とは書けない)があります。この場合には、[AnyStore<Drug>]
と記述する必要があります。
SwiftUI における View
型は、associatedtype
を持っているため Opaque Types により some View
のように記述します。この View
の型消去を行うためのラッパーとして、SwiftUI には AnyView
型が定義されています。
型消去の仕組み自体は気にしなくても SwiftUI で画面は記述できますが、仕組みを知っておく必要はあるでしょう。 それにより、Opaque Types が何のために存在しているのかも、理解が深まると思います。
Swift 5.6 では、some
に続いて any
キーワードが導入されました。
some Store
は「 何らかの Store
準拠型」ですが、any Store
は「 任意の Store
準拠型」を表します。
そのため、以下のような宣言も可能です。
//var someStores: [some Store] // NG
var anyStores: [any Store] // OK