Remove NonObjectiveCBase and isUniquelyReferenced
01 何が問題だったのか
値型のcopy-on-write(CoW)を実装する際には、内部のストレージ(参照型のバッファ)が自分だけの参照かどうかを判定して、ユニークなら破壊的変更、共有中なら複製してから変更、という切り分けを行います。Swift 3.0 以前の標準ライブラリには、この判定のための API が複数並立していました。
// 対象は NonObjectiveCBase の派生クラスに限定される
public func isUniquelyReferenced<T : NonObjectiveCBase>(_ object: inout T) -> Bool
// @objc クラスも含めて受け取れるが、@objc インスタンスに対しては常に false を返す
public func isUniquelyReferencedNonObjC<T : AnyObject>(_ object: inout T) -> Bool
public func isUniquelyReferencedNonObjC<T : AnyObject>(_ object: inout T?) -> Bool
// 上記の関数で受け取るための専用ベースクラス
public class NonObjectiveCBase {
public init() {}
}
使う側は、CoW 用のストレージクラスをわざわざ NonObjectiveCBase から継承させ、isUniquelyReferenced を呼ぶ、という形になっていました。
class SwiftKlazz : NonObjectiveCBase {}
class ObjcKlazz : NSObject {}
expectTrue(isUniquelyReferenced(SwiftKlazz()))
expectFalse(isUniquelyReferencedNonObjC(ObjcKlazz()))
// コンパイルエラー: ObjcKlazz は NonObjectiveCBase の派生ではない
expectFalse(isUniquelyReferenced(ObjcKlazz()))
この構成にはいくつか問題がありました。
- 同じ目的の API が
isUniquelyReferencedとisUniquelyReferencedNonObjCの2種類あり、標準ライブラリの API 表面が無駄に広くなっています。 NonObjectiveCBaseは、型システム上で「non-@objcなクラスであること」を表すためだけに存在するベースクラスで、ユーザーに不要な継承を強制します。isUniquelyReferencedNonObjCの “NonObjC” という名前は、Objective-C 相互運用のないプラットフォームでは意味を持ちません。isUniquelyReferencedNonObjCは@objcクラスのインスタンスに対して常にfalseを返すと約束していますが、CoW 実装のためのプリミティブとしては、この挙動は過剰な保証でした。ManagedBufferPointer側にも同じ意味のholdsUniqueReference()があり、こちらは名前からisUniquelyReferenced系と同じ意味であることが読み取りにくくなっていました。また、holdsUniqueOrPinnedReference()も公開されていましたが、対になる pinning API は非公開で、外部から利用する手段がありませんでした。
これらを整理し、CoW 実装に必要な一意参照チェックのための API を一本化する必要がありました。
02 どのように解決されるのか
一意参照チェックの API を isKnownUniquelyReferenced に一本化し、周辺 API を整理します。
NonObjectiveCBase と isUniquelyReferenced の削除
NonObjectiveCBase クラスと、それを制約にした isUniquelyReferenced<T : NonObjectiveCBase> を標準ライブラリから削除します。従来この API を使っていたコードは、isKnownUniquelyReferenced(後述)に置き換えます。対象の型がコンパイル時に non-@objc なネイティブ Swift クラスであるとわかっている場合、生成されるコードは従来と同じビルトイン呼び出しに落ちるため、パフォーマンス特性は変わりません。
移行前後の対応関係は次のとおりです。
// Before
class ClientClass : NonObjectiveCBase { }
class ClientClass2 : NonObjectiveCBase { }
var x: NonObjectiveCBase = pred ? ClientClass() : ClientClass2()
if isUniquelyReferenced(&x) { /* ... */ }
// After
class CommonNonObjectiveCBase {}
class ClientClass : CommonNonObjectiveCBase { }
class ClientClass2 : CommonNonObjectiveCBase { }
var x: CommonNonObjectiveCBase = pred ? ClientClass() : ClientClass2()
if isKnownUniquelyReferenced(&x) { /* ... */ }
NonObjectiveCBase を継承していたクラスは、任意のベースクラス(あるいはベースクラスなし)に置き換えられます。
isUniquelyReferencedNonObjC の isKnownUniquelyReferenced への改名
isUniquelyReferencedNonObjC を isKnownUniquelyReferenced に改名します。同時に、@objc クラスのインスタンスに対して必ず false を返すという保証は取り下げます。この API の目的はあくまで CoW 実装のサポートであり、@objc であるかどうかを判別するためのものではないためです。
新しいシグネチャは次のとおりです。
/// Returns `true` iff `object` is class instance with a single strong
/// reference.
///
/// * Does *not* modify `object`; the use of `inout` is an
/// implementation artifact.
/// * Weak references do not affect the result of this function.
public func isKnownUniquelyReferenced<T : AnyObject>(_ object: inout T) -> Bool
public func isKnownUniquelyReferenced<T : AnyObject>(_ object: inout T?) -> Bool
典型的な CoW 実装は次のように書けます。
class SwiftKlazz {}
mutating func modifyMe(_ arg: X) {
if isKnownUniquelyReferenced(&myStorage) {
myStorage.modifyInPlace(arg)
} else {
myStorage = self.createModified(myStorage, arg)
}
}
ベースクラスを指定する必要がなくなり、CoW 用のストレージクラスは任意のクラス定義で構いません。
ManagedBufferPointer の整理
ManagedBufferPointer のメソッドも合わせて整理します。
holdsUniqueReference()をisUniqueReference()に改名し、isKnownUniquelyReferencedと同じ意味合いであることを名前から読み取れるようにします。holdsUniqueOrPinnedReference()を削除します。対になる pinning API が非公開である以上、この API が公開されている意味はありませんでした。
public struct ManagedBufferPointer<Header, Element> : Equatable {
// ...
/// Returns `true` iff `self` holds the only strong reference to its buffer.
public mutating func isUniqueReference() -> Bool
}
既存コードへの影響
旧来の isUniquelyReferenced / isUniquelyReferencedNonObjC / holdsUniqueReference は unavailable としてマークされ、コンパイラが新 API への置き換えを提示します。NonObjectiveCBase を継承していたクラスは継承を外すだけで済み、呼び出し側は isKnownUniquelyReferenced / isUniqueReference に書き換えれば従来と同じ挙動が得られます。