Swiftで型をEquatableに準拠させる基準
TL;DR
- 自動EquatableならいつでもつけてOK
- 手動Equatableが必要になるならほとんどやめたほうがいいものばかり
- classを準拠させるのはNG
-
@restoactive
が必要なパターンもNG
用語
手動Equatable
Equatableに準拠させるとき、自分で ==
の定期が必要なパターンのこと。
Swift4.1以前は手動Equatableしかなかった。
名前は以下の記事の「手動Codable」を参考にして命名。
自動Equatable
方の定義に : Equatable
とつけるだけでEquatableに準拠させることができるパターンのこと。
Swift4.1で実装された[SE-0185]Synthesizing Equatable and Hashable conformanceによって実現されている。
自動Equatableを使用するには、
- 準拠させたい方のpropertyやrawValue/associatedValueがEquatableに準拠している
- 準拠させたい方を定義したモジュールと同じモジュールで準拠する
必要がある。
自動Equatable→OK
: Equatable
をつけただけでコンパイルが通る場合は準拠してOK。
(OKだからコンパイラが自動で実装を合成してくれる)
手動Equatable→基本NG
==
を自分で定義しなければならない場合、基本的にEquatableに準拠すべきではない。
自動Equatableが使えない場合、大抵は値の透過性を定義するのが難しい、もしくは不可能な場合が多い。
それなのに適当に定義してしまうと後々トラブルになりかねないので手動で定義するにしても細心の注意が必要。
classをEquatableに準拠させる→NG
classの場合継承が存在するため、「MyClassとMySubClass1を透過と評価してよいか?またMySubClass1とMySubClass2は?」など複雑な問題が発生し ==
を適切に定義することは極めて難しい。
そのためclassをEquatableに準拠させるべきではない。
@retroactive
が必要な場合→NG
Swift6で導入された @retroactive
は別のモジュールで定義された型に対してプロトコルを準拠させた場合に発生する警告を抑制するattributeである。
[SE-0364]Warning for Retroactive Conformances of External Types
そもそも、別のモジュールで定義された型に対してプロトコルを準拠させることはそのモジュールが更新されたときにトラブルが起きがちであるため避けるべきである。
よってEquatableの準拠もするべきではない。
Equatableで迷ったときは
Equatableをつけたくなったけど手動Equatableが必要になったときの対処法を以下に挙げる。
enumならパターンマッチを活用してEquatableを避ける
enumのcaseに対応して値を返したいときはパターンマッチを活用する。
enum State {
case initial
case running
case end
var isRunning: Bool {
switch self {
case .running:
true
case .initial, end:
false
}
}
// 別パターン。網羅性が必要ない場合はこちら
var isRunning2: Bool {
if case .running = self {
true
} else {
false
}
}
}
ErrorをassociatedValueとして持つ場合→NSErrorにキャストしてdomainとcodeを比較する
なんらかの状態を表すenum等の場合、accosicated valueにError型を持たせたい場合がある。
その場合は、ErrorをNSErrorにキャストすることでdomainとcodeから同一性を判定することができる。
enum MyError: Error {
case foo
case bar
}
enum MyState: Equatable {
case initial
case error(Error)
static func == (lhs: Self, rhs: Self) -> Bool {
switch (lhs, rhs) {
case (.initial, .initial):
true
case (.initial, .error), (error, .initial):
false
case (.error(let left as NSError), .error(let right as NSError)):
left.code == right.code && left.domain == right.domain
}
}
}
print(MyState.initial == MyState.initial) // => true
print(MyState.initial == MyState.error(.foo)) // => false
print(MyState.error(.foo) == MyState.error(.foo)) // => true
print(MyState.error(.foo) == MyState.error(.bar)) // => false
it can be interpreted as an NSError by Objective-C with an appropriate error domain (effectively, the mangled name of the HomeworkError type) and code (effectively, the case discriminator).
Discussion