この記事は何?
SwiftプログラミングにおけるEquatableプロトコルについて、公式のヘルプドキュメントを翻訳および解説します。
参考: Equatable | Apple Developer Documentation
以下の概念を理解しておくと、より理解が深まります。
- エクステンション
- 型メソッド
- 型の制約
Swiftを基礎から学ぶには
自著、工学社より発売中の「まるごと分かるSwiftプログラミング」をお勧めします。変数、関数、フロー制御構文、データ構造はもちろん、構造体からクロージャ、エクステンション、プロトコル、クロージャまでを基礎からわかりやすく解説しています。
宣言
protocol Equatable
概要
Equatableプロトコルに適合する型は等価演算子==を使用して、「値が等しいかどうか」を比較できます。
また、不等価演算子!=を使用して、「値が等しくないかどうか」を比較できます。
Swift標準ライブラリの基本的な型は、ほとんどがEquatableプロトコルに適合しています。
要素がEquatableに適合している場合、シーケンスやコレクションの操作をより簡単に行えます。
例えば、配列が「特定の値を含むかどうか」を調べるためには「等価性を決定するクロージャ」を提供するかもしれません。
しかしながら、配列の要素がEquatableプロトコルに適合していれば、「調べたい値」を配列のcontains(_:)メソッドに渡すことができます。
次の例は、文字列の配列でcontains(_:)メソッドを使用する方法を示しています。
let students = ["Kofi", "Abena", "Efua", "Kweku", "Akosua"]
let nameToCheck = "Kofi"
if students.contains(nameToCheck) {
print("\(nameToCheck) is signed up!")
} else {
print("No record of \(nameToCheck).")
}
// Prints "Kofi is signed up!"
Equatableプロトコルへの適合
カスタム型をEquatableプロトコルに適合させると、より便利なAPIを利用して「コレクション内の特定のインスタンスを検索する」ことができるようになります。
Equatableは、HashableおよびComparableの基礎となるプロトコルです。
そして、セットを構築したり、コレクションの要素をソートするなど、カスタム型をより多く使い方ができるようにします。
カスタム型の定義において、Equatable適合を宣言し、その型が以下に挙げる基準を満たしている場合、Equatableプロトコルの要件を満たす実装が自動的に生成されます。
- 構造体の場合、すべての格納プロパティが
Equatableに適合している。 - 列挙型の場合、すべての関連値が
Equatableに適合している (関連値がなければ宣言なしでEquatableに適合)。
型のEquatable適合性をカスタマイズしたり、上記の基準を満たさない型にEquatableを採用したり、既存の型を拡張してEquatableに適合させたりするには、型に静的メソッドとして等価演算子==を実装します。
なお、標準ライブラリはEquatable型に対して不等価演算子!=の実装を提供しており、これはカスタムした==関数を呼び出して、その結果を論理否定します。
例として、住所を表す家屋や建物番号、通りの名称、およびオプションのユニット番号を保持するStreetAddressクラスを考えます。
まずは、StreetAddress型を宣言します。
class StreetAddress {
let number: String
let street: String
let unit: String?
init(_ number: String, _ street: String, unit: String? = nil) {
self.number = number
self.street = street
self.unit = unit
}
}
ここで、「住所の配列」に特定の住所が含まれているかチェックしたいとします。
クロージャを要素ごとに呼び出す方法ではなく、 contains(_:)メソッドを使用できるようにしましょう。
そのためには、要素のStreetAddress型を拡張してEquatableプロトコルに適合させます。
extension StreetAddress: Equatable {
static func == (lhs: StreetAddress, rhs: StreetAddress) -> Bool {
return
lhs.number == rhs.number &&
lhs.street == rhs.street &&
lhs.unit == rhs.unit
}
}
このエクステンションによって、StreetAddress型はEquatableに適合しました。
これにより、等価演算子==を使用して「2つのインスタンスが等しいかどうか」をチェックしたり、Equatableに制約されたcontains(_:)メソッドを呼び出したりすることができるようになりました。
let addresses = [StreetAddress("1490", "Grove Street"),
StreetAddress("2119", "Maple Avenue"),
StreetAddress("1400", "16th Street")]
let home = StreetAddress("1400", "16th Street")
print(addresses[0] == home)
// Prints "false"
print(addresses.contains(home))
// Prints "true"
同等に比較される2つのインスタンスは、その値に依存するすべてのコードで互換性を持って使用できます。
置換性を維持するために、等価演算子==はEquatableな型のすべての可視的な側面を考慮する必要があります。
クラスID以外のEquatable型の非値側面を公開することは推奨されず、公開されるものはドキュメントで明示的に指摘する必要があります。
Equatableな型のインスタンス間の等価性は等価関係であるため、Equatableに適合するカスタム型は、任意の値a、b、cに対して、次の3つの条件を満たす必要があります。
-
a == aは常にtureになる (Reflexivity) -
a == bならb == aになる (対称性) -
a == bかつb == cならa == cになる (Transitivity)
さらに、不等式は等式の逆であるため、!=演算子のカスタム実装は、a != b が !(a == b) となることを保証しなければなりません。
デフォルトの!=演算子関数の実装は、この要件を満たしています。
等価性と同一性の違い
クラスにおいて、インスタンスの同一性は「値の一部」ではありません。
整数値をラップしたIntegerRefクラスを考えてみましょう。
IntegerRef型の定義と、それをEquatableに適合させる==関数を以下に示します。
class IntegerRef: Equatable {
let value: Int
init(_ value: Int) {
self.value = value
}
static func == (lhs: IntegerRef, rhs: IntegerRef) -> Bool {
return lhs.value == rhs.value
}
}
この==関数の実装は、valueプロパティに格納されている整数が等しいければ、2つの引数が同じインスタンスであろうとなかろうと、返り値には影響しません。
たとえば...
let a = IntegerRef(100) // bとは別の参照先
let b = IntegerRef(100) // aとは別の参照先
print(a == a, a == b, separator: ", ") // 参照先は異なるが、値は等しい
// Prints "true, true"
一方、クラスの「インスタンスが同一かどうか」は同一演算子===によって比較されます。
たとえば...
let c = a // cは、aと同じ参照先を指す
print(c === a, c === b, separator: ", ") // aとcは参照先が同じだが、bとは異なる
// Prints "true, false"