swiftのany/some
![Shun Uematsu](https://res.cloudinary.com/zenn/image/fetch/s--JxuT-W2t--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_70/https://storage.googleapis.com/zenn-user-upload/avatar/6c7bb774e8.jpeg)
Existensial Containerとany
protocolとは
Swiftのprotocolの主な役割はジェネリクスの型パラメーターに制約を設けるものです。
ただ副次的に型としても扱える便利機能も備えている。
型としてprotocolを扱う
protocolを型として扱うと以下のような感じになります。
protocol Animal {
func foo() -> Int
}
struct Dog: Animal {
func foo() -> Int { 1 }
}
struct Cat: Animal {
func foo() -> Int { 2 }
}
let animal: Animal = Cat()
print(animal.foo())
ここで注目して欲しいのはこの一文です。
let animal: Animal = Cat()
前述したようにprotocolの主な役割はジェネリクスに制約を設けることです。
それにも関わらず、ジェネリクスの制約として扱うほうが書きづらいのは微妙です。
そこでSwift 5.6からany
キーワードをつける事ができるようになりました。そしてSwift6からany
をつけていないと警告が出るようになるみたいです。
- let animal: Animal = Cat()
+ let animal: any Animal = Cat()
ではなぜprotocolを型として扱うことがメインではなくサブなのでしょうか。
これにはExistensial Containerというものが関わってきます。
例えば以下のようにDog型に1byte、Cat型に4bytesのstored propertyを持たせてみます。
そしてそれらをそれぞれインスタンス化すると、1 byteと4 bytesのインスタンスが生成されます。
ではそれらのインスタンスをAnimal型で抽象化された定数に格納した際、その定数は何byteになるのでしょうか。
struct Dog: Animal {
var a: UInt8 = 0 // 1 byte
func foo() -> Int { 1 }
}
struct Cat: Animal {
var b: UInt32 = 0 // 4 bytes
func foo() -> Int { 2 }
}
let dog: Dog = .init() // 1 byte
let cat: Cat = .init() // 4 bytes
let animal: any Animal = Bool.random() ? dog : cat // ? byte
print(animal.foo())
MemoryLayoutを使って確認してみます。
print(MemoryLayout.size(ofValue: dog)) // 1 byte
print(MemoryLayout.size(ofValue: cat)) // 4 bytes
print(MemoryLayout.size(ofValue: animal)) // 40 bytes
dogとcatは想定どおりになりましたが、animalに関しては40 bytesになっています。
animal定数には、dogかcatしか入る可能性がないから大きい方の4 bytesにしておけば良いのではないかと思うかもしれません。
ではAnimal型に適合した以下のようなCow型が追加されたとします。
struct Cow: Animal {
var c1: Double = 0 // 8 bytes
var c2: Double = 0 // 8 bytes
var c3: Double = 0 // 8 bytes
var c4: Double = 0 // 8 bytes
var c5: Double = 0 // 8 bytes
var c6: Double = 0 // 8 bytes
var c7: Double = 0 // 8 bytes
var c8: Double = 0 // 8 bytes
var c9: Double = 0 // 8 bytes
var c10: Double = 0 // 8 bytes
func foo() -> Int { 0 }
}
Cow型は合計80 bytesのstored propertyを持っており、当然それをインスタンス化すると80 bytesになります。
let cow: Cow = .init() // 80 bytes
print(MemoryLayout.size(ofValue: cow)) // 80 bytes
ではAnimal型で定義された定数にCowインスタンスを格納すると、その定数は何bytesになるでしょうか。
答えは40 bytesです。
let animal: any Animal = cow // ? byte
print(MemoryLayout.size(ofValue: animal)) // 40 bytes
Animal型で定義された定数には、Animal型に適合したインスタンスは何でも入れられないといけません。
しかし今回の様な80 bytesだったり、さらに大きいインスタンスを格納しようとした時にそれに合わせたメモリ領域を確保しようとすると上限がなくなってしまいます。
そこでどんなサイズの物が来ても入れられるように特殊な入れ物に入れています。
その入れ物のことをExistential Containerと言います。
Existential Containerが先程でた40 bytesと大きさになっています。
Existential Containerの24bytes分は値を格納するために使われます。
- catやdogなど24 bytes以下のものは直接値が格納される
- cowのような24 bytesを超えるようなものは、そのインスタンスをヒープ領域に格納し、そのアドレスが格納される
残りの16bytesは型情報などのメタデータやprotocol fitness tableが入っています。
Exitential Containerにはオーバーヘッドを生んでしまうという問題があります。
- 1 byteしか必要としないdogをExistential Containerで包んでしまうと、40 bytesとなり無駄にメモリを消費してしまう。
- Existential Containerで包んだり、それを剥がしたりするのでパフォーマンスが悪くなる。
-
animal.foo()
としたときに、animalはcatかdogかcowかコンパイル時には分からず、実行時に解決しないと行けないので、パフォーマンスが悪くなる。
ただ以下のようにAnimal型のような抽象化されたものを使ってコーディングすることは多々あると思います。
しかし関数を引数に渡すときにexistential containerに包まれてしまうので、オーバーヘッドが発生してしまいます。
func use(animal: Animal) {
print(animal.foo()) // dynamic dispatch
}
use(animal: dog)
use(animal: cat)
上記のような抽象化はポリモーフィズムの一種である、Subtyping(サブタイピング)と呼ばれる抽象化の方法を使っています。これをParametric polymorphism(パラメーター多相)という別の抽象化方法をジェネリック関数を用いて書いてみます。
func use<A: Animal>(animal: A) {
print(animal.foo())
}
use(animal: dog)
use(animal: cat)
上記の様に書くことで、
任意の型がAnimalプロトコルに準拠していないといけない、という制約を持ったジェネリック関数を定義することができます。
そして上記関数はコンパイル時、**特殊化(Specialization)**という最適化が行われることがあり、これにより先程のジェネリック関数は以下のコード書いたときと同じバイナリが出力されます。
func use(animal: Dog) {
print(animal.foo())
}
func use(animal: Cat) {
print(animal.foo())
}
上記のanimal引数の型は、DogやCatとなっているのでExistential Containerに包む必要はありません。
そのためProtocolを型として扱った書き方より、型パラメーターの制約として扱ったときのほうがオーバーヘッドが少なくなります。
これが最初に記述した、
Swiftのprotocolの主な役割はジェネリクスの型パラメーターに制約を設けるものです。
ただ副次的に型としても扱える便利機能も備えている。
の理由です。
![Shun Uematsu](https://res.cloudinary.com/zenn/image/fetch/s--JxuT-W2t--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_70/https://storage.googleapis.com/zenn-user-upload/avatar/6c7bb774e8.jpeg)
Opaque Parameter Declarations
Protocolの主な役割は、型パラメーターの制約を設けるものでした。
にもかかわらずSwift5.5までは、Protocolを型として扱うほうが書きやすく、型パラメーターの制約として扱うほうが書きにくい状態でした。
そこでこの現状を改善するために、
Swift5.6からはany
キーワード、Swift5.7からはsome
キーワードを使うことができるようになりました。
- func use(animal: Animal) {}
+ func use(animal: any Animal) {}
- func use<A: Animal>(animal: A) {}
+ func use(animal: some Animal) {}
![Shun Uematsu](https://res.cloudinary.com/zenn/image/fetch/s--JxuT-W2t--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_70/https://storage.googleapis.com/zenn-user-upload/avatar/6c7bb774e8.jpeg)
参照型中心の言語と値型中心の言語の違い
前述したように、Swiftでサブタイピングを実現しようとすると、Existential Containerに包む必要がありオーバーヘッドが発生すると説明しました。
しかしkotlinなどの参照型中心の言語では、サブタイピングでも問題ありません。
それは参照型と値型の特徴の違いにあります。
参照型は変数に代入する際、値そのものを格納しているのではなくメモリ上の別の領域(ヒープ領域のどこか)に保存され、その領域を表すアドレスが格納されています。
そのため参照型中心の言語ではExistential Containerに包む必要は無いので、値型中心の言語余地もオーバーヘッドも少ないのです。
![Shun Uematsu](https://res.cloudinary.com/zenn/image/fetch/s--JxuT-W2t--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_70/https://storage.googleapis.com/zenn-user-upload/avatar/6c7bb774e8.jpeg)
Opaque Result Type
ここまでは引数の型を抽象化する話をしてきました。では戻り値を抽象化するにはどうすればよいでしょうか。
サブタイピングを用いた場合は以下のように書くことができます。
func makeAnimal() -> Animal { Dog() }
let animal = makeAnimal()
print(animal.foo())
上記をパラメーター多相を用いて書く場合は、どのように書けば良いでしょうか。
例えば以下のようにジェネリクスを使って書いてみます。