はじめに
Swiftという言語にはProtocolというとても便利なシステムがあって、特にその中でもEquatable
は最も使われているProtocolだと思うのです1。
Equatable
をオリジナルのクラスに適用するには、単に2つのインスタンスが同じ値(とみなせる)かどうかを判断する==
というstatic funcさえ実装すればいいのです2が、openなクラスに実装する場合は多少工夫が必要です。この記事では、工夫が必要な場面とその工夫を紹介します。
実例
MyClass
にEquatable
を適用する
次のようなMyClass
を考えてみましょう
open class MyClass {
public var a:Int
public init(a:Int) { self.a = a }
}
Equatable
をMyClass
に適用するにあたって、MyClass
のインスタンスは.a
の値が同じなら同じであるとみなしましょう。
extension MyClass: Equatable {
public static func ==(lhs:MyClass, rhs:MyClass) -> Bool {
return lhs.a == rhs.a
}
}
ね?簡単でしょ。
let object1 = MyClass(a:0)
let object2 = MyClass(a:1)
let object3 = MyClass(a:0)
print(object1 == object2) // -> false
print(object1 == object3) // -> true
MyClass
を継承したMySubclass
を作ってみよう
MyClass
を継承したMySubclass
を定義します。新たなるインスタンス変数として.b
を追加します。
open class MySubclass: MyClass {
public var b: Int
public init(a:Int, b:Int) { self.b = b; super.init(a:a) }
}
MyClass
がEquatable
を適用しているので、それを継承したMySubclass
も==
演算子を使うことができます。が、しかし…
let object4 = MySubclass(a:0, b:1)
let object5 = MySubclass(a:0, b:2)
let object6 = MySubclass(a:0, b:1)
print(object4 == object5) // -> true!!
print(object4 == object6) // -> true
当たり前と言えば当たり前なのですが、MyClass
で定義した==
演算子は.a
というインスタンス変数しか比較をしていないので、.b
の値が違ってもtrue
を返します。サブクラスの役割としてそれでいい場合もあるかもしれませんが、ここでは.b
という値も同一でないといけないということにしましょう。
extension MySubclass {
public static func ==(lhs:MySubclass, rhs:MySubclass) -> Bool {
return lhs as MyClass == rhs as MyClass && lhs.b == rhs.b
}
}
こうすることで、先ほどのprint(object4 == object5)
は期待通りfalse
と表示されるはずです。なお、今回の例では、lhs as MyClass == rhs as MyClass
の部分はlhs.a == rhs.a
としても構いませんが、親クラスであるMyClass
に比較すべきインスタンス変数がたくさんある場合や後から比較すべきインスタンス変数を追加した場合などを考えると、前者の方が読みやすさと保守性の点で優れているでしょう。
だが、これで終わりではない
ここまでのソースを振り返って見ましょう。
open class MyClass {
public var a:Int
public init(a:Int) { self.a = a }
}
extension MyClass: Equatable {
public static func ==(lhs:MyClass, rhs:MyClass) -> Bool {
return lhs.a == rhs.a
}
}
open class MySubclass: MyClass {
public var b: Int
public init(a:Int, b:Int) { self.b = b; super.init(a:a) }
}
extension MySubclass {
public static func ==(lhs:MySubclass, rhs:MySubclass) -> Bool {
return lhs as MyClass == rhs as MyClass && lhs.b == rhs.b
}
}
MyClass
でもMySubclass
でもEquatable
を適用できていて完璧にみえます。ですが、まだ落とし穴があります。
ここで、MyClass
またはその子孫クラスのインスタンスを保持するラッパーMyWrapper
を登場させてみましょう。
public struct MyWrapper {
public var object: MyClass
public init(object:MyClass) { self.object = object }
}
こうなるとMyWrapper
もEquatable
にしたいですよね?さぁ、してみましょう。
extension MyWrapper: Equatable {
public static func ==(lhs:MyWrapper, rhs:MyWrapper) -> Bool {
return lhs.object == rhs.object
}
}
これでOKでしょうか?先ほどのobject1, 4, 5
に再度登場してもらいましょう。
let object1 = MyClass(a:0)
let object4 = MySubclass(a:0, b:1)
let object5 = MySubclass(a:0, b:2)
let wrapper1 = MyWrapper(object:object1)
let wrapper4 = MyWrapper(object:object4)
let wrapper5 = MyWrapper(object:object5)
print(wrapper1 == wrapper4) // -> true!!
print(wrapper4 == wrapper5) // -> true!!
はて?
object1
,object4
,object5
はそれぞれ全てが違うと判断されるべきインスタンスですが、ラッパーで包んだ途端、同一とみなされてしまいました。
これも当たり前と言えば当たり前なのですが、インスタンス変数がpublic var object: MyClass
と定義されているので、MyWrapper
のstatic func ==
内にあるlhs.object == rhs.object
は、MyClass
の==
が呼ばれてしまうのです。.object
がMySubclass
のインスタンスの場合はそれとして比較するためにはどうすればいいのでしょうか?
案①
MyWrapper
の実装を工夫してみる。
extension MyWrapper: Equatable {
public static func ==(lhs:MyWrapper, rhs:MyWrapper) -> Bool {
let (lobj, robj) = (lhs.object, rhs.object)
switch (type(of:lobj), type(of:robj)) {
// lobjとrobjのクラスが一致する場合のみ
// そのクラスのインスタンスとして比較
case let (ltype, rtype) where ltype == rtype:
if ltype == MySubclass.self {
return lobj as! MySubclass == robj as! MySubclass
} else {
return lobj == robj
}
// クラスが一致しない場合はfalse
default:
return false
}
}
}
これでMyWrapper
の==
も期待通りに動作します。ただし、この場合、MyClass
を継承する別のクラスやMySubclass
をさらに継承するクラスを作ると、その都度この関数を修正する必要が生じます。即ち「MyWrapper
の.object
はMyClass
とMySubclass
のインスタンス以外ありえない」という状況でなければめんどくさいことになります。open
なクラスでそれは…。
案②
MyClass
やMySubclass
を継承する別のクラスを作る予定(作られる可能性)があるのであれば、作る人間にその責任を負わせることにしましょう。つまりMyWrapper
の==
は最初のmywrapper-eq.swift
のままうまくいくように、今度はMyClass
やMySubclass
のほうを弄ります。大改修(おおげさ)になるので、いっぺんにソース全体をお見せします。
open class MyClass: Equatable {
public var a:Int
public init(a:Int) { self.a = a }
public func isEqual(to another:MyClass) -> Bool {
return self.a == another.a
}
public static func ==(lhs:MyClass, rhs:MyClass) -> Bool {
return lhs.isEqual(to:rhs) && rhs.isEqual(to:lhs)
}
}
open class MySubclass: MyClass {
public var b: Int
public init(a:Int, b:Int) { self.b = b; super.init(a:a) }
public override func isEqual(to another:MyClass) -> Bool {
guard case let anotherInstance as MySubclass = another else {
return false
}
return super.isEqual(to:another) && self.b == anotherInstance.b
}
}
public struct MyWrapper: Equatable {
public var object: MyClass
public init(object:MyClass) { self.object = object }
public static func ==(lhs:MyWrapper, rhs:MyWrapper) -> Bool {
return lhs.object == rhs.object
}
}
いろいろ変わっていますが、変更点のキモとしては、MyClass
にisEqual(to:)
というインスタンスメソッドを追加し、==
からそのメソッドを呼ぶようにしたことです。そして、MySubclass
には最早==
というstatic funcはありません。その代わりに、isEqual(to:)
をオーバライドしています。そしてMyWrapper
の==
内では予定通りreturn lhs.object == rhs.object
しかありません。なぜ、これでうまくいくのでしょうか?
それは、次のようなコードを実行してみるとよく分かるかと思います。
class A {
func f() { print("I am A.") }
}
class B: A {
override func f() { print("I am B.") }
}
let object: A = B()
(object as A).f()
なんと表示されましたか?
let object: A = B()
とobject
の型はA
であると宣言し、f()
というインスタンスメソッドを実行する時にも(object as A).f()
とobject
はA
だと散々言い聞かせたにもかかわらず、表示されたのは無情にも
I am B.
もちろんオブジェクト指向としては至極当然の動作です。object
はB()
と生成されているので、B
のインスタンスです。(object as A).f()
としたところで、実行時にB
のインスタンスメソッドが呼び出されることになります。
案②ではこの動作を利用しています。MyWrapper
におけるlhs.object == rhs.object
ではMyClass
のstatic funcである==
が呼び出されるのは変更前と変わりませんが、その中ではisEqual(to:)
というインスタンスメソッドが呼び出されています。となると、I am B.
の時と同じように、lhs
(またはrhs
)がMyClass
として渡されたとしてもそれがMySubclass
のインスタンスであれば、ちゃんとMySubclass
のisEqual(to:)
が呼び出されるのです。仮にMyClass
に別のサブクラスを作成したとしても、そのクラスのisEqual(to:)
だけ実装すれば問題は生じません3。
この実装では、Equatble
におけるSymmetry4を保証するために、MyClass
の==
がreturn lhs.isEqual(to:rhs) && rhs.isEqual(to:lhs)
と冗長になってしまっているところが玉に瑕。ただ、例えば、仕様上MyClass
とMySubclass
の.a
の値は必ず異なるといった場合(.a
がインスタンスごとに割り振るIDであるなど)であれば、return lhs.isEqual(to:rhs)
だけで済むこともあるはずです。
案③
本当にそれはクラスである必要がありますか?
Swiftではstruct
もいい働きをしますよ?そして折角のProtocolという仕組みです。さらにGenericsなんていうものもあります。クラスにこだわる必要もないのではないですか?
まだ"point of no return"を超えていないのであれば設計から見直しませんか?
こんなコードはいかがですか?
public protocol MyProtocol: Equatable {
var a: Int { get }
}
public struct MyFirstStructure: MyProtocol {
public var a: Int
public init(a:Int) { self.a = a }
public static func ==(lhs:MyFirstStructure, rhs:MyFirstStructure) -> Bool {
return lhs.a == rhs.a
}
}
public struct MySecondStructure: MyProtocol {
public var a: Int
public var b: Int
public init(a:Int, b:Int) { self.a = a; self.b = b }
public static func ==(lhs:MySecondStructure, rhs:MySecondStructure) -> Bool {
return lhs.a == rhs.a && lhs.b == rhs.b
}
}
public struct MyWrapper<Instance: MyProtocol>: Equatable {
public var instance: Instance
public init(instance:Instance) { self.instance = instance }
public static func ==(lhs:MyWrapper<Instance>, rhs:MyWrapper<Instance>) -> Bool {
return lhs.instance == rhs.instance
}
}
これで済む場合も少なからずあるのではないでしょうか?ただ、もちろん、この場合はMyFirstStructure
とMySecondStructure
には継承関係がないので、それぞれのインスタンスを直接比較するにはまためんどくさいコードを書かないといけません。一方、最初から同一のクラスでしか比較をしないような設計なら、思い切ってstruct
を使うのも手です。
その他の案
コメント欄を参照(他力本願)
おわりに
Swiftの教科書(なんてものがあれば)のProtocolの項で最初に出てきそうなEquatable
ですが、単純な実装では場合によってバグを生み出すことがあるかもしれないことは意識しておかないといけないようです。
-
私見。ComparableやHashableなどもEquatableを継承しているから、直接ではなくても皆んな使用しているよね? ↩
-
Swift 4.0-dev 現在: https://github.com/apple/swift/blob/swift-4.0-branch/stdlib/public/core/Equatable.swift#L156 ↩
-
その別のサブクラスでも
.a
の値さえ同じなら同一とみなすのであれば、改めて実装する必要さえありません。自動的にMyClass
のisEqual(to:)
が呼び出されます。 ↩ -
対称性。
a == b ⇔ b == a
というやつ。 ↩