🟰

Swiftで型をEquatableに準拠させる基準

2025/02/26に公開

TL;DR

  • 自動EquatableならいつでもつけてOK
  • 手動Equatableが必要になるならほとんどやめたほうがいいものばかり
    • classを準拠させるのはNG
    • @restoactive が必要なパターンもNG

用語

手動Equatable

Equatableに準拠させるとき、自分で == の定期が必要なパターンのこと。
Swift4.1以前は手動Equatableしかなかった。

名前は以下の記事の「手動Codable」を参考にして命名。
https://qiita.com/s_emoto/items/deda5abcb0adc2217e86#手動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は?」など複雑な問題が発生し == を適切に定義することは極めて難しい。
https://qiita.com/YOCKOW/items/00892049d3d4c30b5eb9

そのため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).

https://github.com/swiftlang/swift-evolution/blob/main/proposals/0112-nserror-bridging.md#motivation

Discussion