Swift のオブジェクトが共有されていないか判定する

Swift プログラミング

Swift でキャッシュの役割をするオブジェクトを作っていたら、そのオブジェクトが共有されていないかを知る必要に迫られました。

Swift では isUniquelyReferencedNonObjC 関数を使って唯一の存在であるかを調べられるようです。


Swift にて、キャッシュデータを扱うようなオブジェクトを作っていたところ、そのオブジェクトが複数の変数で共有されているかを判定する必要に迫られました。

Swift のオブジェクトは、複数のインスタンスで状態を共有する仕組みになっています。構造体で作ればキャッシュの保存場所も複製されるので良かったのでしょうけど、今回は nonmutating のときにもキャッシュを更新したくてクラスで扱うことにしたため、今回のような問題に行き着くことになりました。

共有判定の必要性

構造体の中に、クラス型のキャッシュデータを含めると、構造体が別の変数に格納されたときに、構造体の値は独立するのにキャッシュだけが元の変数と共有されることが問題になりました。

値が独立するため、複製後にどちらかの構造体が変更され、そしてキャッシュの更新が必要になったタイミングで、値が変わっているのは片方だけなのに、実質的に両方のキャッシュが更新されてしまうのが問題です。

このような矛盾の発生をなくすためにも、複数で共有されていた場合は『キャッシュを分断する』などの対応が必要になります。


Objective-C では、工夫すれば retainCount などを使って参照カウントから共有状態を判定することができましたけど、Swift ではそういったことができない様子で、代わりに別の方法で調べられるようになっていました。

共有されているか判定する

Swift では、オブジェクトが共有されているかを判定する関数として isUniquelyReferencedNonObjC というものが用意されています。

この関数は inout 指定で引数を取るようになっていて、ここに渡したオブジェクトが唯一の強参照であれば true を、共有されていれば false を返す作りになっているようです。


たとえば、ある Object クラスがあったときに、そのインスタンスが共有されているかは次のようにして判定できます。

// 判定関数に inout で渡せるように、可変値変数で定義する必要があります。
class Object {

}

var obj1 = Object()

// 共有されていないため、ここでは true が得られます。
if isUniquelyReferencedNonObjC(&obj1) {

}

// 複数の変数でインスタンスを共有します。
var obj2 = obj1

// 共有されるようになったため false が得られます。
if isUniquelyReferencedNonObjC(&obj1) {

}

// 代入先の変数も共有されているので false を返します。
if isUniquelyReferencedNonObjC(&obj2) {

}

オプショナル内も判定可能

この isUniquelyReferencedNonObjC関数 には Optional<T> を想定したものも用意されていて、オプショナルな変数をそのまま inout で渡すことで、中身の参照状態を判定できます。

var obj1:Object? = Object()

// 共有されていないため、ここでは true が得られます。
if isUniquelyReferencedNonObjC(&obj1) {

}

// 複数の変数でインスタンスを共有します。
var obj2 = obj1

// 共有されるようになったため false が得られます。
if isUniquelyReferencedNonObjC(&obj1) {

}

このように、オプショナルではなかった場合と全く同じに記載できます。

ただし、値が nil の場合には、共有状態を判定できずに false を返す様子でした。false を返すということは、意味的には『共有されている』とも取れてしまうため、オプショナルを扱うときには注意が必要かもしれません。

var obj:Object? = nil

// オプショナルに nil が入っている場合は false が得られます。
if isUniquelyReferencedNonObjC(&obj) {

}

Weak 参照を扱う場合

たとえば、クラスや構造体で設計された Box 型が、内部に weak 参照でオブジェクトをもつ場合があったとします。

このような weak に対してオブジェクトをもたせても、そのオブジェクト自体は『共有されていないもの』として判定できます。

class Box {

	weak var object: Object?
	
	init(_ object: Object?) {
	
		self.object = object
	}
}

var obj:Object? = Object()

// 共有されていないため、ここでは true が得られます。
if isUniquelyReferencedNonObjC(&obj) {

}

// このオブジェクトを weak で保持する Box に渡したとします。
var box = Box(obj)

// オブジェクト自体が Box 内の weak で参照されるため false を返します。
if isUniquelyReferencedNonObjC(&obj) {

}

ただし、このとき Box の weak 参照している変数そのもの を調べようとすると、必ず false を返してくる様子なので注意が必要です。

// 共有されていないはずが、ここで false が得られます。
if isUniquelyReferencedNonObjC(&box.object) {

}

// もちろん、代入前の obj を調べれば true になります。
if isUniquelyReferencedNonObjC(&obj) {

}

Box インスタンスから object にアクセスした際に強参照されているのかもと思って、試しに Box 内で次のように判定用のプロパティを用意してみましたが、このようにしても意味的には『共有されている』ことを示す false が得られました。

// 型の内部から直接判定するようにしてみます。
extension Box {

	var isObjectUnique:Bool {
	
		mutating get {
	
			return isUniquelyReferencedNonObjC(&self.object)
		}	
	}
}

// そして外からそのメソッドを介して調べても、共有を意味する false が得られました。
if box.isObjectUnique {

}

// もちろん、代入前の obj を調べれば true になります。
if isUniquelyReferencedNonObjC(&obj) {

}

このように weak でオブジェクトを扱っているときは、それのユニーク性の判定で false が返ってくることに注意する必要がありそうです。

もっとも weak の場合、保持させた値が他で参照されていないと nil になるので weak を使う立場でいえば 必ず共有されている ともいえて、わざわざユニーク性を判定する必要はないかもしれません。

同様に unowned(safe) や unowned(unsafe) でも weak と同じユニーク性の判定結果になる様子でした。

Objective-C クラスを判定する場合

この isUniquelyReferencedNonObjC関数 を使って NSObject を継承した Objective-C クラスは判定できない様子です。

そのようなオブジェクトを判定しようとすると、共有されていなくても必ず false が返されるようでした。

まとめ

以上のように、若干の癖はあるものの、ずいぶん簡単に共有されているかどうかを判定できるようになっていました。

Objective-C の参照カウントを直接見る方法と違って、いくつの変数で共有されているかまでは分かりませんけど、通常はそこまで精密に判定する必要はないでしょうから、このように簡単な方法で調べる機能が備わっているのは嬉しいところです。


たとえば Swift の Array 型では、書き換えが必要になるまでは内部バッファーを共有して、いざ書き換えが行われるタイミングで、Array が共有されていればバッファーを複製して書き換え、Array が共有されていなければ既存のバッファーを直接書き換え、といった最適化が図られているそうです。

こういった最適化も、この機能を使えば実現できたりするので、使いどころはもしかするといろいろあるかもしれません。

他にも isUniquelyReferenced という関数が用意されていたりしますけど、こちらは NonObjectiveCBase型 を想定した関数のようです。これに準拠した型としては ManagedBuffer があるようですが、今回はそこまでの調査はしないでおくことにします。