動機づけ
次のようにプロトコルで宣言したオブジェクトの等価性を扱いたいとします。
public protocol FooProtocol {
var foo: String { get }
var bar: Int { get }
var baz: Double { get }
}
class FooProtocolTest: XCTestCase {
var hoge: FooProtocol!
var fuga: FooProtocol!
func test_example() {
hoge = ... // FooProtocol プロトコルに準拠したオブジェクト
fuga = ... // FooProtocol プロトコルに準拠したオブジェクト
XCTAssertEqual(hoge.foo, fuga.foo)
XCTAssertEqual(hoge.bar, fuga.bar)
XCTAssertEqual(hoge.baz, fuga.baz)
}
}
このように、プロパティ各々を比較するのではなく、 XCTAssertEqual(hoge, fuga)
のようにすることで、プロトコルで定義したプロパティをまとめて比較できれば便利そうです。
そのような単純化の方法について考えてみたいと思います。
【失敗】Equatable
準拠
Swift で等価性を扱う場合、通常は Equatable
に準拠することになります。
しかし、この方法は簡単には上手くいきません。
たとえば、次のように Equatable
のサブプロトコルとして定義します。
public protocol FooProtocol: Equatable {
var foo: String { get }
var bar: Int { get }
var baz: Double { get }
}
このプロトコルに適合することで、==
で評価することができるようになります。
public struct Hoge: FooProtocol {
public var foo: String
public var bar: Int
public var baz: Double
public var qux: [String]
}
let hoge1 = Hoge(foo: "ほげほげ", bar: 23, baz: 42, qux: ["ほげ"])
let hoge2 = Hoge(foo: "ほげほげ", bar: 23, baz: 42, qux: ["ほげ"])
print(hoge1 == hoge2) // true
しかし、これは Hoge
としての等価性であって、FooProtocol
で定義されたプロパティの等価性ではありません。
上記の場合は、FooProtocol
で定義されていない変数 qux
まで評価されます。
また、次のように FooProtocol
に適合した別の型との比較はできません。
public struct Fuga: FooProtocol {
public var foo: String
public var bar: Int
public var baz: Double
public var quux: [Int : String]
}
let hoge = Hoge(foo: "ほげほげ", bar: 23, baz: 42, qux: ["ほげ"])
let fuga = Fuga(foo: "ほげほげ", bar: 23, baz: 42, quux: [1: "ほげ"])
print(hoge == fuga) // コンパイルエラー
Hoge
と Fuga
がそれぞれ Equatable
に準拠しているだけなので、自分自身と同じ型でなければ ==
による比較ができないためです。
また、プロトコルを Equatable
に準拠させたので、冒頭のテストコードはコンパイルエラーになります。
class FooProtocolTest: XCTestCase {
// 次の理由でコンパイルエラー
// Protocol 'FooProtocol' can only be used as a generic constraint
// because it has Self or associated type requirements
var hoge: FooProtocol!
var fuga: FooProtocol!
}
Self
または associated type を使用すると上記のような型宣言ができなくなります。
Equatable
が Self
を使用したプロトコルなので、上記の通りコンパイルエラーになります。
public protocol Equatable {
static func == (lhs: Self, rhs: Self) -> Bool
}
Equatable
準拠の問題点まとめ
失敗作の問題点をまとめると次の2点になります。
-
FooProtocol
で定義したプロパティの等価性ではなく、適合先の構造体の等価性になってしまった -
FooProtocol
がSelf
を使用したプロトコルなので、型宣言ができなくなってしまった
上記から、FooProtocol
自体を Equatable
のサブプロトコルにしてしまうのは、問題の解決にならないことがわかりました。
【成功】AnyHashable
を利用する
冒頭の XCTAssertEqual(hoge, fuga)
のように、FooProtocol
自体を Equatable
にすることは諦めて、
次のような作戦をとることにしました。
XCTAssertEqual(hoge.fooHashable, fuga.fooHashable)
この fooHashable
の部分が「プロトコルで定義されているプロパティを内部的に保持している何か」で、Equatable
に準拠していているようにします。
具体的には、次のような実装になります。
public protocol FooProtocol {
var foo: String { get }
var bar: Int { get }
var baz: Double { get }
}
public extension FooProtocol {
var fooHashable: AnyHashable {
return Anonymous(foo: foo, bar: bar, baz: baz)
}
}
private struct Anonymous: FooProtocol, Hashable {
let foo: String
let bar: Int
let baz: Double
}
コードを言葉で表現すると次のようになります。
-
Hashable
に適合したプライベートな型(Anonymous)を作る - プロトコルのデフォルト実装で
AnyHashable
にラップして上記のオブジェクトを返す
こうすることで、Anonymous
で定義されたプロパティの等価評価をすることになり、実質的には FooProtocol
で定義しているプロパティをまとめて評価することができるようになります。
次のように異なる型で宣言していても、FooProtocol
の意味での等価評価ができます。
let hoge: Hoge = Hoge(foo: "ほげほげ", bar: 23, baz: 42, qux: ["ほげ"])
let fuga: Fuga = Fuga(foo: "ほげほげ", bar: 23, baz: 42, quux: [1: "ほげ"])
print(hoge.fooHashable == fuga.fooHashable) // true
そして、冒頭のようなプロトコルで宣言したオブジェクトの等価評価も、プロパティの各々を比較する必要がなくなります。
class FooProtocolTest: XCTestCase {
var hoge: FooProtocol!
var fuga: FooProtocol!
// 等価比較:FooProtocol の意味で一致していること
func test_example_1() {
hoge = Hoge(foo: "ほげほげ", bar: 23, baz: 42, qux: ["ほげ"])
fuga = Fuga(foo: "ほげほげ", bar: 23, baz: 42, quux: [1: "ほげ"])
XCTAssertEqual(hoge.fooHashable, fuga.fooHashable)
}
// 等価比較:FooProtocol の意味で一致していないこと
func test_example_2() {
// 「ふが」と「ほげほげ」の不一致
hoge = Hoge(foo: "ふが", bar: 23, baz: 42, qux: ["ほげ"])
fuga = Fuga(foo: "ほげほげ", bar: 23, baz: 42, quux: [1: "ほげ"])
XCTAssertNotEqual(hoge.fooHashable, fuga.fooHashable)
}
}
ということで、コードを少し簡潔にすることができました。
また、この実装は、Hoge
や Fuga
自体の等価性と無関係な実装ですので、必要に応じて適合先の構造体やクラスで Equatable
に準拠することも可能です。
そうすると、次のように FooProtocol
と Hoge
の2通りの意味での等価評価が可能になります。
public struct Hoge: FooProtocol, Equatable { // Equatable に準拠
public var foo: String
public var bar: Int
public var baz: Double
public var qux: [String]
}
let hoge1: Hoge = Hoge(foo: "ほげ", bar: 23, baz: 42, qux: ["ほげ"])
let hoge2: Hoge = Hoge(foo: "ほげ", bar: 23, baz: 42, qux: ["ほげ", "ふが"])
// `FooProtocol` の意味での評価は qux が無視されるので true
print(hoge1.fooHashable == hoge2.fooHashable)
// Hoge の意味での評価は qux が不一致で false
print(hoge1 == hoge2)
結び
AnyHashable
と言えば、[AnyHashable: Any]
のような辞書のキーとしての利用をよく見かけますが、Equatable
から Self
を型消去した等価評価のためのオブジェクトとしても利用できることが分かりました。
プロパティの個数が多い場合や、テストケースを増やしたい場合などに役に立ちそうなテクニックですね。