インスタンスが Equatable に準拠しているかを実行時には判定できない様子

Swift コラム

Swift で、実行時に渡されてきたインスタンスが Equatable に準拠しているかを判定したかったのですけど、どうやらそれはできない様子でした。


実行時に渡されてきたインスタンスが Equatableプロトコル に準拠しているかを判定したくなって試してみたのですけど、どうやらできない様子でした。

具体的には、AnyObject型 を扱うジェネリック関数で、もし渡されたインスタンスが Equatableプロトコル に準拠していれば ==演算子 で判定を行い、準拠していないときは ===演算子 で判定したくなったのが発端です。

Equatable は実行時に判定できない様子

Swift で実行時に型判定をする演算子として isas? などがありますが、これを Equatable に対して使おうとするとエラーになってしまいます。

if instance is Equatable {

}

Protocol 'Equatable' can only be used as a generic constraint because it has Self or associated type requirements.

どうやら Equatableプロトコル のように、内部で Self などを使ったジェネリックなプロトコルだと、このようにして動的に準拠性を判定することはできない様子です。

ジェネリックによる判定は静的

ジェネリック関数を使えば Equatable に準拠しているかどうかで処理を分岐することができますが、この場合はあくまでもビルド時の静的な判定に限られるようです。

原則的には判定可能

通常であれば、次のように 2 つのジェネリック関数を用意ることで、渡されたインスタンスの型に応じて処理を分岐できます。

func isSame<T:AnyObject where T:Equatable>(lhs:T, rhs:T) -> Bool {

	return lhs == rhs	
}

func isSame<T:AnyObject>(lhs:T, rhs:T) -> Bool {

	return lhs === rhs	
}

いったん縛ると判定できない

ただ、いったんジェネリック関数で受けたインスタンスを、上の関数に通そうとすると、受けたジェネリック関数で指定された制約に則って、次に呼び出すジェネリック関数がビルド時に決定されてしまう様子です。

たとえば、次のように AnyObject型 の引数を受け取るジェネリック関数 hasObject を作ったときには、その中で呼び出す isSame関数 は、実際の型に関わらず AnyObject型 を想定した方が呼び出されます。

func hasObject<T:AnyObject>(object:T) -> Bool {
	
	return isSame(object, _object)
}

もしこの hasObject関数Equatableプロトコル に準拠した型のインスタンスを渡したとしても、ジェネリックの型パラメーターは AnyObject のため、内部で呼び出す関数は Equatableプロトコル を想定しない方が選ばれます。

判定関数をインスタンスに持たせて対応する

インスタンスの型に応じて呼び出す機能を切り替えたいとはいっても、ジェネリックで扱う型自体は最終的にはビルドの時点で定まっています。

そこで、型の種類に応じて適用する関数を変えたい場合は、そこで使う関数自体を指定できるようにしておくことで対応できます。

func hasObject<T:AnyObject>(object:T, equal:(T,T)->Bool) -> Bool {
	
	return equal(object, _object)
}

このようにすることで、たとえば Equatable に準拠したインスタンスを扱う場合は、第二引数の equal==演算子 を使った比較を行う関数を渡せます。

しかもこの時点ではまだ TAnyObject型 に縛られきってはいないため、上で定義した isSame関数 は、実際に T に渡された型によって使う方を自動的に判断されます。

既定値で比較で使う関数を渡す

ただ、関数を呼び出すときに、比較で使う関数を毎回渡すのは格好が良くない気がするので、既定値をあらかじめ指定しておくことで省略できるようにしてみます。

func hasObject<T:AnyObject>(object:T, equal:(T,T)->Bool = isSame) -> Bool {
	
	return equal(object, _object)
}

このようにすることで、比較関数の指定を省略すると isSame関数 が適用されるようにできます。

ただし、ここでは T型AnyObject型 と指定されているため、省略時に自動で適用される関数は AnyObject型 だけを想定した方になります。


Equatableプロトコル に準拠した型のときにはそれようの isSame関数 を適用したい場合は、今回の関数も isSame関数 と同じようにジェネリックで定義する必要があります。

func hasObject<T:AnyObject where T:Equatable>(object:T, equal:(T,T)->Bool = isSame) -> Bool {
	
	return equal(object, _object)
}

func hasObject<T:AnyObject>(object:T, equal:(T,T)->Bool = isSame) -> Bool {
	
	return equal(object, _object)
}

このようにすることで、渡された型の種類に応じて実行する関数そのものが切り替わり、そこで想定されている型に応じた isSame関数 が既定値として採用されます。

ジェネリックメソッドの場合

これまでの応用みたいになりますが、ジェネリックメソッドで同じことをしたい場合は、インスタンス生成時に比較関数を渡しておいて、必要なときに内部でそれを使うという方法が取れます。

class Container<T:AnyObject> {
	
	var object:T
	var equal:(T,T)->Bool
	
	init(_ object:T, equal: (T,T)->Bool) {
		
		self.object = object
		self.equal = equal
	}
	
	func hasObject(object:T) -> Bool {
	
		return equal(object, self.object)
	}
}

クラスや構造体のインスタンス変数場合、扱う型に応じて比較関数を自動で切り替えるようなことはできないようなので、equal の指定を省略したい場合は、既定値では AnyObject型 を想定した関数を指定しておいて、必要に応じてプログラマーが別の関数に差し替えられるようにする方法になりそうです。

イニシャライザで適用する関数を切り替えるのは難しそう

ちなみに、もしかしたら init をジェネリックにして Equatableプロトコル の場合の初期化を別に用意すれば大丈夫かもと考えたんですけど、Equatable として受け取った型 U と、実際に扱う型 T とを一致させようとしたところでエラーになりました。

init<U:AnyObject where U:Equatable, U==T>(_ object:U, equal: (U,U)->Bool) {

}

Same-type requirement makes generic parameters 'T' and 'U' equivalent.

ここで突然登場した U と本来の T とを同じものとして扱えないことには先が成り立たないため、やはりなかなか Equatable に準拠していた場合の特例を途中で作るのは難しそうです。