Swift Digest
SE-0125 | Swift Evolution

Remove NonObjectiveCBase and isUniquelyReferenced

Proposal
SE-0125
Authors
Arnold Schwaighofer
Review Manager
Chris Lattner
Status
Implemented (Swift 3.0)

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 が isUniquelyReferencedisUniquelyReferencedNonObjC の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 を整理します。

NonObjectiveCBaseisUniquelyReferenced の削除

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 を継承していたクラスは、任意のベースクラス(あるいはベースクラスなし)に置き換えられます。

isUniquelyReferencedNonObjCisKnownUniquelyReferenced への改名

isUniquelyReferencedNonObjCisKnownUniquelyReferenced に改名します。同時に、@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 に書き換えれば従来と同じ挙動が得られます。