Conditional ConformanceはSwift4.1で追加された言語機能です。
型パラメータに条件をつけて(Conditional)他のProtocolに適合する(Conformance)ことができる便利な機能です。
class Box<T> {
var value: T
init(_ value: T) { self.value = value }
}
解説用の箱です。これをConditional Conformanceで拡張して遊んでみましょう。最近私や身の回りの人が踏んだものを一通り紹介します。
前提となるProtocolは明示的に宣言する必要がある
// Conditional conformance of type 'Box<T>' to protocol 'Hashable' does not imply conformance to inherited protocol 'Equatable'
extension Box: Hashable where T: Hashable {
func hash(into hasher: inout Hasher) {
value.hash(into: &hasher)
}
}
HashableはEquatableを前提に持つProtocolです。ConditionalConformanceでHashableを使う場合は、「明示的にEquatableのConditionalConformanceを宣言する」必要があります。
複数のConditionから同一のConformanceは作れない
// Conflicting conformance of 'Box<T>' to protocol 'Hashable'; there cannot be more than one conformance, even with different conditional bounds
extension Box: Hashable where T: AnyObject {
func hash(into hasher: inout Hasher) {
return ObjectIdentifier(self).hash(into: &hasher)
}
}
// Conflicting conformance of 'Box<T>' to protocol 'Hashable'; there cannot be more than one conformance, even with different conditional bounds
extension Box: Hashable where T == Any.Type {
func hash(into hasher: inout Hasher) {
return ObjectIdentifier(self).hash(into: &hasher)
}
}
一度HashableにConformしたBoxは他の方法でHashableにConformできなくなります。
滅多にこの要求は発生しないものですが、ObjectIdentifierを使って良しなに動かす夢は潰えました。
Existentialが作れる
extension Box: CustomStringConvertible where T: CustomStringConvertible {
var description: String { return value.description }
}
func check(_ value: Any) {
guard let value = value as? CustomStringConvertible else { return }
print(value)
}
check(Box(1))
check(Box(Box(1)))
Swift4.2からExistentialが使えるようになりました。Swift4.1ではキャストの実行時に警告(warning: Swift runtime does not yet support dynamically querying conditional conformance
)が出ていたものです。
Existentialを作るとクラッシュする
作れると言ったな?あれは嘘だ
extension Box: CustomStringConvertible where T: Sequence, T.Element :CustomStringConvertible {
var description: String {
return "[" + value.map { $0.description }.joined(separator: ", ") + "]"
}
}
check(Box([1, 2, 3])) // EXC_BAD_ACCESS (code=EXC_I386_GPFLT
Conditionの複雑さが以下の条件を超えると実行時クラッシュです。
- associatedtypeを持つprotocolに条件付をする
- associatedtypeにも条件付をする
Bugsに報告されています https://bugs.swift.org/browse/SR-8666
typealiasはconditionalではなく、全体を汚染する。
数日前にDiscordで盛り上がっていたネタです。
extension Box: Sequence where T: Sequence {
typealias Element = T.Element
typealias Iterator = T.Iterator
func makeIterator() -> T.Iterator {
return value.makeIterator()
}
}
例えばSequenceのConditional Conformanceを書いてみましょう。
一見すると問題なさそうですが、ElementはT: Sequenceでない場合にも存在してしまいます。いろいろ困ったことが発生します。
// error: Segmentation fault: 11
print(Box<Int>.Element.self)
Box.Elementは存在しているのですが、Int.Elementは存在しない、これはコンパイル時にセグフォが発生します。
extension Box: IteratorProtocol where T: IteratorProtocol {
// Invalid redeclaration of 'Element'
typealias Element = T.Element
func next() -> Element? {
return value.next()
}
}
加えてIteratorProtocolのConditional Conformanceを追加しました。Elementは汚染されているので宣言することが出来ません。ところでこの場合は汚染されたElementはT.Elementなので、typealiasを消してそのまま利用すれば、「たまたま正しく」動きます。
極めて稀に発生する、ElementをT.Element以外で指定したいという要求が発生すると詰みます。他の方法を探しましょう。
Bugsに報告されています https://bugs.swift.org/browse/SR-9533
おわり
Conditional Conformanceで遊んでみました。残念ながら少し壊れてしまいました。
とはいえConditional Conformanceの用途の80%は「ElementがPならWrapperもPにしたい」というもので今回紹介した悪いコードを使う必要は殆どありません。でも20%ぐらいは壊しそうになること、ありますよね。
20%の具体例ですが @taketo1024 先生が書いているSwiftyMathが、限界を超えそうなConditional Conformanceを要所要所で利用されていて面白いです。
https://github.com/taketo1024/SwiftyMath