この記事は何?
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"