Warning for Retroactive Conformances of External Types
01 何が問題だったのか
Swift のランタイムでは、プロトコルへの適合はプロセス内でグローバルに一意である必要があります。同じ型が同じプロトコルに2回適合すると挙動が未定義になり、どちらの実装が「勝つ」かを保証できません。
このため、自分のモジュールが所有していない型を、自分のモジュールが所有していないプロトコルに適合させる「retroactive conformance(遡及的な適合)」は本質的に危険です。例えば、あるライブラリが Date に対して Identifiable を独自に適合させたとします。
// Foundation も Swift 標準ライブラリも所有していないコード
extension Date: Identifiable {
public var id: TimeInterval { timeIntervalSince1970 }
}
この状態で、のちに Foundation 側が Date: Identifiable の適合を追加すると、まずビルドが壊れます。ユーザーがローカルの適合を削除して再ビルドするまでの間、アプリケーションは未定義動作になり、どちらの id 実装が使われるかは不定です。Foundation 版が timeIntervalSinceReferenceDate を用いていた場合、もしアプリが ID を永続化していれば、同じ Date のレコードに対して異なる ID が返るといった深刻な破損につながります。
さらに、この適合がライブラリターゲットで宣言されていると、そのライブラリを import するすべてのクライアントに適合が伝搬します。library evolution 有効のバイナリフレームワークとして配布されている場合、クライアントは適合が本来の所有モジュールから来ていないことに気づきにくく、問題の発見と修正がますます困難になります。
ローカルでの利便性のために書かれたこうした適合が、エコシステム全体では大きな不安定要因になっていたため、コンパイラが明示的に危険性を指摘し、どうしても必要な場合にだけ意図を宣言して使える仕組みが必要でした。
02 どのように解決されるのか
自分のモジュールが所有していない型を、自分のモジュールが所有していないプロトコルに適合させる extension に対して、コンパイラが警告を出すようにします。どうしても必要な場合にだけ、新しい属性 @retroactive を付けて意図的な retroactive conformance であることを宣言し、警告を抑えられます。
// 警告: extension declares a conformance of imported type 'Date'
// to imported protocol 'Identifiable'; this will not behave correctly
// if the owners of 'Foundation' introduce this conformance in the future
extension Date: Identifiable {
public var id: TimeInterval { timeIntervalSince1970 }
}
警告を抑えるには、適合するプロトコル名に @retroactive を付けます。
extension Date: @retroactive Identifiable {
public var id: TimeInterval { timeIntervalSince1970 }
}
継承階層のある複数のプロトコルを同時に適合させる場合、階層上のそれぞれの retroactive な適合に対して明示的に @retroactive を付ける必要があります。コンパイラは必要に応じて、不足している適合用の extension を生成する fix-it を提示します。
@retroactive は extension で、かつ retroactive な適合を導入する場合にのみ使用できます。それ以外の場所で書くとエラーになります。
警告の対象条件
警告は、extension が次の両方を満たすときに出ます。
- 拡張対象の型が、extension とは別のモジュールで宣言されている。
- 追加する適合先のプロトコルが、extension とは別のモジュールで宣言されている。
次のケースは、上記を満たしていても retroactive とはみなされず、警告は出ません。
- 型またはプロトコルが Clang モジュールで宣言され、当該 extension がその Swift オーバーレイにある場合。
- 型が bridging header または
-import-objc-headerで(推移的に)import されており、他のモジュールに属していない場合。これらは暗黙の__ObjCモジュールに属し、クライアントが責任を負う前提になります。 - 型が
@_originallyDefined(in:)で別モジュールからの移動を示している場合。 - 型またはプロトコルの所有モジュールが、extension と同じパッケージに属している場合。同一パッケージ内での重複適合はリンク時または実行時に検出されます。
また、以下は従来どおり安全で警告の対象外です。
- 自分のモジュールで定義したプロトコルに対して、外部の型を適合させる extension。
- 適合を追加しない、外部の型への単なる extension(シンボルにモジュール名がマングリングされるため、ランタイム衝突は起きません)。
古いコンパイラとの互換性
@retroactive は純粋に追加的な属性で、すべての言語バージョンで受け入れられますが、古い Swift コンパイラでは構文として認識されません。複数の Swift バージョンでビルドする必要があるプロジェクトで警告を抑えたい場合は、extension 内の型を完全修飾することで同じ効果が得られます。
extension Foundation.Date: Swift.Identifiable {
// ...
}
こうすることで、@retroactive を使えない古いツールチェーンでもビルドが通り、かつ警告も抑えられます。