Swift6で存在型(Existential type)を書く際にany
キーワードが必須になる。その理由や内容について見ていく。
ある条件を満たす全ての型を表す型。Swiftではプロトコルを型として使用した場合にがこれに該当する。
例: Animal
プロトコルに準拠したDog
構造体とCat
構造体はAnimal
型として扱うことができる。
protocol Animal {}
struct Dog: Animal {}
struct Cat: Animal {}
let animals: [Animal] = [Dog(), Cat()]
存在型には、異なる具体的な型を代入できる。
var dog: Animal = Dog()
dog = Cat() // ok
関連ドキュメント: https://docs.swift.org/swift-book/LanguageGuide/Protocols.html#ID275
存在型を使用する際、下記のような機能面の制限やパフォーマンス面の問題があることを認識する。
- 型として利用できない場合がある。
下記の場合、プロトコルを型として扱うことはできない。
- 関連型(associated type)が要件に含まれている
- メソッド/プロパティ/subscript/イニシャライザの共変ではない位置(引数など)に
Self
への参照が含まれている
※ ただし、この問題は解決される予定。
より詳細はSwift PAT問題を解消して全てのプロトコルを存在型として使えるようにに記載。
- 型情報が消去されているため、使える機能が制限される。
例えば、下記の2つのcallee
の戻り値の型は同じであることがわからないので加算できない。
func callee() -> Numeric {
if Bool.random() {
return 42
} else {
return 42.0
}
}
func caller() {
let x = callee() + callee() // ❌ Binary operator '+' cannot be applied to two 'Numeric' operands
}
- 存在型はそのプロトコル自身に準拠できない。これはプロトコル自身がそのプロトコルの全ての要件を満たすことができない場合があるため(staticプロパティやイニシャライザなど)。全てを満たすことができる場合もあるが、例えば外部から提供されるプロトコルにプロトコル自身が満たせない要件が追加された場合にsource breakが発生してしまうため、これを防ぐ役割もある。
protocol P {
func test()
}
func generic<ConcreteP: P>(p: ConcreteP) {
p.test()
}
func useExistential(p: any P) {
generic(p: p) // ❌ Protocol 'P' as a type cannot conform to the protocol itself
}
関連ドキュメント: https://github.com/apple/swift/blob/main/userdocs/diagnostics/protocol-type-non-conformance.md
- プロトコルは具体的な型を動的に保持するため、動的なメモリが必要になる。たとえ3単語バッファのようなスタックに収まる小さいものでもヒープ領域と参照カウンタを使う。
下記を見てみると、プロトコルは動的な型の情報を保持するためにあらかじめメモリを確保しておく必要がある。(class
は実際のオブジェクトのメモリアドレスを保持するために必要なメモリが確保される)
protocol Animal {}
struct Dog: Animal {}
class Cat: Animal {}
let animal: Animal = Dog()
let dog = Dog()
let cat = Cat()
MemoryLayout.size(ofValue: animal) // 40
MemoryLayout.size(ofValue: dog) // 0
MemoryLayout.size(ofValue: cat) // 8
- メソッドのダイナミックディスパッチやポインタの間接参照があるため、コンパイラによる最適化ができない。
下記の例で見てみると、変数animal
の値は実行時までわからないため、コンパイル時に型を決められない。
protocol Animal {
func bark()
}
struct Dog: Animal {
func bark() { print("bowwow") }
}
class Cat: Animal {
func bark() { print("meow") }
}
var animal: Animal = Dog()
animal.bark() // bowwow
animal = Cat()
animal.bark() // meow
Swiftでは、ジェネリックの制約にも存在型にもプロトコルを使用し、スペリングも似ている。そのため、ジェネリックを扱う際に両方を混同しがちになる。 さらに、存在型はジェネリックの制約を設定するよりもはるかに簡単に記述できることから、上記のようなコストがあることを意識せずに存在型を使ってしまっている場合がある。
関連ドキュメント:
そこで、存在型が使用されている場面では、any
キーワードを付けることを必須にすることで、存在型の誤用を防ぎたい。
エラーにはならないが、Swift5.6から導入予定。
※ Swift5とSwift6以降で挙動が異なる。
- Swift5 モード
protocol P {}
protocol Q {}
struct S: P, Q {}
let p1: P = S() // ⭕️ この P は存在型
let p2: any P = S() // ⭕️ any P は明示的に存在型
let pq1: P & Q = S() // ⭕️ この P & Q は存在型
let pq2: any P & Q = S() // ⭕️ any P & Q は明示的に存在型
- Swift6 モード
protocol P {}
protocol Q {}
struct S: P, Q {}
let p1: P = S() // ❌ エラー
let p2: any P = S() // ⭕️
let pq1: P & Q = S() // ❌ エラー
let pq2: any P & Q = S() // ⭕️
逆にstruct
やenum
、tuple
などの名前的型(nominal type)、tupleやOptional、関数型などの構造的型(structural type)、関数、ジェネリックの型パラメータなどには使えない。
struct S {}
let s: any S = S() // ❌ error: 'any' has no effect on concrete type 'S'
func generic<T>(t: T) {
let x: any T = t // ❌ error: 'any' has no effect on type parameter 'T'
}
let f: any ((Int) -> Void) = generic // ❌ error: 'any' has no effect on concrete type '(Int) -> Void'
Any
とAnyObject
はすでに存在型だと明白なので2度手間になる。ただし、付けてもワーニングなどは出ない。(Accepted時に変更された)
struct S {}
class C {}
let value: any Any = S()
let values: [any Any] = []
let object: any AnyObject = C()
protocol P {}
extension C: P {}
let pObject: any AnyObject & P = C() // ⭕️
存在型のメタタイプにはany
を付ける。例えば、P
に準拠した型自体のメタタイプ(P.Type
)はany P.Type
になる。P
プロコトルのメタタイプ(P.Protocol
)は、any P
をかっこで囲んで(any P).Type
になる。P
プロコトルのメタタイプの値(P.self
)はany P
をかっこで囲んで.self
を付ける。(any P).self
存在型のメタタイプは複数存在するが、プロトコルのメタタイプは一つしか存在しない。
protocol P {}
struct S: P {}
let existentialMetatype: any P.Type = S.self
protocol Q {}
extension S: Q {}
let compositionMetatype: any (P & Q).Type = S.self
let protocolMetatype: (any P).Type = (any P).self
プレーンなプロトコルの名前と同様に、プロトコルのタイプエイリアスもジェネリック制約と存在型の両方で使える。any
が付いていると明らかに存在型なので、any P
のタイプエイリアスはジェネリック制約としては使えず、存在型のみに利用できる。
protocol P {}
typealias AnotherP = P
typealias AnyP = any P
struct S: P {}
let p2: any AnotherP = S()
let p1: AnyP = S()
func generic<T: AnotherP>(value: T) { ... }
func generic<T: AnyP>(value: T) { ... } // ❌
Swift6になると、プレーンなプロトコルのタイプエイリアスは関連型として妥当ではなくなり、タイプエイリアスの中でany
を指定する必要がある。
- Swift6 モード
protocol P {}
protocol Requirements {
associatedtype A
}
struct S1: Requirements {
typealias A = P // ❌ error: associated type requirement cannot be satisfied with a protocol
}
struct S2: Requirements {
typealias A = any P // ⭕️ okay
}
方法はまだ未定だが、migratorで自動で変換されるという記載はある。
段階的な導入として
- Swift5.6から利用可能だが、必要な箇所で
any
がなくてもワーニングは出ない - 1つ以上先のメジャーリリース※でワーニングを導入して新しい構文の導入を促す
- 最終的に新しいメジャーな言語バージョン(Swift6を想定)では、古い構文(
any
なしの存在型)はエラーになる
※ 5.6や5.7などをメジャーリリースと呼ぶ(5.5.1などをポイントリリースと呼ぶ)
Unlock existential for all protocols によってExistentialをより多くのコードで使えるようになった。そのためSwift6で不正になるコードを減らすためにもSwift5.6から導入するのが良いと判断。
Existentialをそのプロトコル自身に適合できるようにする。こうすることで、型消去の型をわざわざ作成しなくてもextensionで対応できるようになる場合がある。
extension any Equatable: Equatable { ... }
存在型のシンタックスを変えることで、プレーンなプロトコル名の利用方法を変えることができるかもしれない。
例えば、プレーンなプロトコル名は、プロトコルの要件を満たす囲まれたコンテキスト(extensionなど)内で、常にジェネリックの型パラメータのシンタックスシュガーとみなすことができる。
extension Collection { ... }
// 上記と同じと見なせる
extension <Self> Self where Self: Collection { ... }
これによって今提案されている他の機能と組み合わせることで、よりコードを簡潔に書けるかもしれない。
例えば、現在のArrayのappend(contentsOf:)
は
extension Array {
mutating func append<S: Sequence>(contentsOf newElements: S) where S.Element == Element
}
という形だが、associated typeをカッコ内に書けるようになると、もっと簡単に定義できる。
extension Array {
mutating func append(contentsOf newElements: Sequence<Element>)
}
- [Pitch] Introduce existential any
- SE-0335: Introduce existential any
- [Accepted with modifications] SE-0335: Introduce existential any