Xcode 9.3 正式版が一向にリリースされないため、しびれを切らして投稿します。
(追記) -> 翌日にリリース来ました。念願のSwift 4.1や。Swift 4.1の次はSwift 4.2で、その次はSwift 5.0や!
本記事の効能
- Swiftの
extension
は3つの用法しかないことを知り、それぞれのパターンをはっきり峻別できるようになる - また、それぞれのパターンに付与された
where
の意味も完全に把握し、Swift 4.1でお試し解禁された条件付き適合(conditional conformance)
まで知識をビルドアップする - 結果、さらに自信を持って
extension
を扱えるようになり、Swiftがさらに楽しくなる
Swift 4.1以上をご用意のうえ、お読みいただけると嬉しいです。
環境
Xcode 9.3
Swift 4.1
全3パターンを把握する
どんなに複雑そうに見えるextension
定義でも、用法別に見れば、わずか3パターンしかありません。
用法1: クラス・構造体・列挙体に定義を追加する
用法2: プロトコル拡張(protocol extension)
用法3: クラス・構造体・列挙体をプロトコルに適合させる
たったこれだけです。
ひとたび実際のコードの世界に飛び込むと、当たり前のようにwhere
節が絡んだ複雑なextension
定義に遭遇しますが、恐るるに足らず。
よくよく見るとその用法はたった3つしかないことを知るだけで、勇敢に立ち向かう気概が湧いてきます。
それぞれのパターンの見分け方も押さえながら、みっちりと理解していきましょう。
(なお今回は、where
節以下に書ける記法の仔細には立ち入らないこと、ご承知おきください。
自分自身の理解のため、後日改めてまとめたいと欲望しております。)
用法1: クラス・構造体・列挙体に定義を追加する
最も素朴な使い方です。
既存のクラス・構造体・列挙体に対し、新たな定義を追加します。
以下の例は、標準ライブラリの構造体Int
に、インスタンスメソッドtimes
を新たに定義しています。
extension Int {
// 自身の回数だけ、引数に渡されたクロージャを実行するメソッド
func times(repeat: (Int) -> Void) {
// `repeat` は予約語なので、``でエスケープする
(0..<self).forEach { n in `repeat`(n) }
}
}
3.times{ _ in print("Hello!") }
// 結果
Hello!
Hello!
Hello!
こちらは、標準ライブラリの構造体Bool
に、コンピューテッドプロパティnumber
を新たに定義しています。
extension Bool {
// 自身の値に応じた整数を返す
var number: Int { return self ? 1 : 0 }
}
print(true.number) // 1
print(false.number) // 0
構文(見分け方)
// []: 任意で付加される場合がある、という意味です
extension クラス名|列挙体名|構造体名 [where ...] {
...
}
extension
の直後に型名(クラス・列挙体・構造体)が来ればこのパターンです。
追加できる定義
詳解Swift 第3版 P244によると、extension
で新たに追加できる定義は、以下の通りです。
- 格納型(stored)のタイププロパティ、計算型(computed)のインスタンスプロパティ、計算型(computed)のタイププロパティ
- インスタンスメソッド、タイプメソッド
- イニシャライザ
- サブスクリプト
- ネスト型定義
逆に追加できないのは、以下の通りです。
- 格納型(stored)のインスタンスプロパティ
- プロパティオブザーバ
where
が付加された場合
話が前後してしまい恐縮ですが、
そもそもまず、用法1~3に共通する、extension
に付加されるwhere
の基本的な意味について。
where
以下には、特定の制約を付加していくことができます(型制約)。
それにより、制約を全て満たした場合のみ有効となる定義を記述することができます。1
extension ○○ where 制約1, 制約2... {
制約をすべて満たす場合にのみ有効となる定義
}
話を戻して...今回の用法1に対してwhere
が付加された場合、
型がwhere
以下の条件を満たす場合のみ有効になる定義を追加するという構文になります。
以下の例は、Arrayの型パラメータ<Element>
がInt
で特殊化された場合のみ有効になるインスタンスメソッドsum
を追加しています。
// 「[Int]の場合のみ有効になる定義を、これから定義します」
extension Array where Element == Int {
// 配列の各要素の総和を返す
func sum() -> Int {
return reduce(0, +)
}
}
print([1,2,3,4,5].sum()) // 15
print([true, false].sum()) // コンパイルエラー。∵ [Bool]では、インスタンスメソッド `sum`は有効にならない
用法2: プロトコル拡張(protocol extension)
続いてのパターンです。
プロトコルに対しextension
を用いることで、プロトコルのデフォルト実装を定義することができます。
プロトコルに適合する型定義の中にその実装がなくても、
あたかも最初から実装していたかのようにプロパティにアクセスしたり、メソッドを呼び出したりできます。
// パーティメンバーが備えているべき各種ステータスが宣言されたプロトコル
protocol Party {
var name: String { get }
var lv: Int { get }
var HP: Int { get }
}
extension Party {
// プロトコル`Party`が使用できるデフォルト実装
var info: String {
// 本体のプロトコル定義で宣言したプロパティが使用できる点に注目♪
return "名前: \(name), レベル: \(lv), HP: \(HP)"
}
}
struct Devil: Party {
let name: String
var lv: Int
var HP: Int
}
let jack_O_Frost = Devil(name: "ジャックフロスト", lv: 7, HP: 78)
print(jack_O_Frost.info) // `Devil`の定義には`info`は宣言されていないのに、何食わぬ顔で`info`を使用できる!
// 結果
名前: ジャックフロスト, レベル: 7, HP: 78
構文(見分け方)
// []: 任意で付加される場合がある、という意味です
extension プロトコル名 [where ...] {
...
}
extension
の直後にプロトコル名が来ればこのパターンです。
用法1との差異は「プロトコル名か? それとも型名(クラス・列挙体・構造体)か?」だけなので、
名前から判断しかねる場合、定義ジャンプでプロトコルか否か確認するとよいかもしれません。
追加できる定義
プロトコル拡張で新たに追加できる定義は、以下の通りです。
- 計算型(computed)のインスタンスプロパティ、計算型(computed)のタイププロパティ
- インスタンスメソッド、タイプメソッド
- サブスクリプト
反対に、できないものは、以下の通りです。
- イニシャライザ
- ネスト型定義
where
が付加された場合
プロトコル拡張に対してwhere
が付加された場合、
プロトコルに適合する型がwhere
以下の条件を満たす場合のみ有効になるデフォルト実装を追加するという構文になります。
以下の例は、プロトコルSwimmable
とRunnable
の両方に適合している型でのみ有効になるインスタンスメソッドswagger
を追加しています。
(extension
定義に出てくるSelf
は当記事で初めて出現した表現ですが、"実際にこのプロトコルに適合した具体的な型" のことを指します。)
protocol Swimmable {
func swim()
}
protocol Runnable {
func run()
}
// 「`Swimmable`に適合した型が`Runnable`にも適合している場合にのみ有効になる定義を書きます」
extension Swimmable where Self: Runnable {
// いばって歩く
func swagger() {
print("陸でも水でも無敵だぞ😤")
}
}
// ワニさんは水陸両用
struct Alligator: Swimmable, Runnable {
func swim() {
print("ぶくぶく")
}
func run() {
print("シュタタタ")
}
}
// イルカさんは水中専用
struct Dolphin: Swimmable {
func swim() {
print("すいすい")
}
}
let alligator = Alligator()
alligator.swagger()
// 結果
陸でも水でも無敵だぞ😤 (∵ ワニさんは`Swimmable`かつ`Runnable`なので、`swagger`を呼べる)
let dolphin = Dolphin()
dolphin.swagger()
// 結果
コンパイルエラー: (∵ イルカさんは`Swimmable`しか適合していないので、`swagger`を呼べない)
用法3: クラス・構造体・列挙体をプロトコルに適合させる
最後のパターンです。
extension
を使うことで、クラス・構造体・列挙体をプロトコルに適合させることができます。
iOS開発で頻出するこちらの典型例を引き合いに出すのがよいでしょう。
UIViewController
を継承した独自型を、UIKitのプロトコルUITableViewDataSource
に適合させたい場合、
実直に書くとこのようになると思いますが...。
import UIKit
class ViewController: UIViewController, UITableViewDataSource {
override func viewDidLoad() {
super.viewDidLoad()
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 5
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell") as! TableViewCell
cell.label.text = "ヒーホー!"
return cell
}
}
これを以下のように書き換えることができます。
import UIKit
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
}
}
// extensionを用いることでも、型をプロトコルに適合できる♪
extension ViewController: UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 5
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell") as! TableViewCell
cell.label.text = "ヒーホー!"
return cell
}
}
プロトコルへの準拠宣言(: UITableViewDataSource
の部分)と、そのプロトコルが要求する実装を、ViewController
の定義の外にくくり出しただけですね...?
ですがこれにより、各プロトコルが要求する実装が型定義本体から切り分けられ、ひいては可読性が向上するとされています。
また、くくり出したextension
の定義は、別ファイルに記述することも可能です。
構文(見分け方)
// []: 任意で付加される場合がある、という意味です
extension クラス名|列挙体名|構造体名 : プロトコル名 [where ...] {
...
}
用法1と用法2に**:
**は出現しないので、この記号があれば用法3だと即判断できますね。
where
が付加された場合
これまで見てきたパターンと、where
が付加されたときの意味は、それぞれ以下のものでした。
パターン | 意味 |
where が付加された場合 |
---|---|---|
extension ○○(クラス名/列挙体名/構造体名) | 型に定義を追加する | 条件を満たした場合のみ使用できる定義を追加 |
extension ○○(プロトコル名) | プロトコル拡張 | 条件を満たした場合のみ使用できるデフォルト実装を追加 |
extension ○○(クラス名/列挙体名/構造体名): ××(プロトコル名) | 型をプロトコルに適合させる | ????? |
それでは、この用法3にwhere
が付加された場合は...?
?????
『第三のwhere』解禁: 【条件付き適合】発現
これまで出てきた2つの用法にwhere
が付加された場合は、
いずれの場合も「特定の条件を満たした場合のみ、○○する」という意味になったのでした。
それを鑑みると、用法3の場合も、where
が付加された場合の意味の推測が立ちます。
「特定の条件を満たした場合のみ、型をプロトコルに適合させる」
...まさにその通り。
これが、Swift 4.1で実験的機能として追加された、**「条件付き適合(conditional conformance)」**という機能にあたります。
(Proposalには「Status: Implemented (Swift 4.2)」とあるように、
正式な機能として認められるのは完全な機能を有するのはSwift 4.2から、ということのようです。)
(追記) -> いただいたコメントを受け、表現を少し変更しました。コメント欄も参照ください。
パターン | 意味 |
where が付加された場合 |
---|---|---|
extension ○○(クラス名/列挙体名/構造体名) | 型に定義を追加する | 条件を満たした場合のみ使用できる定義を追加 |
extension ○○(プロトコル名) | プロトコル拡張 | 条件を満たした場合のみ使用できるデフォルト実装を追加 |
extension ○○(クラス名/列挙体名/構造体名): ××(プロトコル名) | 型をプロトコルに適合させる | 条件付き適合(特定の条件を満たした場合のみ、型をプロトコルに適合させる) ←new! |
使用例
(この項目は、特にSwift 4.1以降
の説明の仕方であることにご留意いただけると幸いです)
条件付き適合の説明にあたり、 例えばこちらに見られるような**「[Equatable]
はEquatable
でない問題」**が引き合いに出されることが多いと思います(リンク先、型の理解を深める上でも非常に学びになりました)。
なので、今回は自分なりの例を作っての説明にトライしてみます。
Swiftの標準ライブラリにキュー(Queue)が用意されていないことに気づいた私が、
独自のデータ構造Queue
を作りたくなったとします。
今回は、こちらの記事を拝借させていただきながら、キューが空のときにdequeue
すると例外を投げる実装にしてみました。
struct Queue<Element> {
enum QueueError: Error {
case queueIsEmpty
}
private var elements = [Element]()
mutating func enqueue(_ newElement: Element) {
elements.append(newElement)
}
mutating func dequeue() throws -> Element {
guard !elements.isEmpty else { throw QueueError.queueIsEmpty }
return elements.remove(at: 0)
}
}
このように使用することができます。
var queue1 = Queue<Int>()
queue1.enqueue(1) // [1]
queue1.enqueue(2) // [1,2]
try! queue1.dequeue() // 1 elements => [2]
try! queue1.dequeue() // 2 elements => []
そんな折、ふとこんなことを思いつきます:
『どうせなら2つのQueueが同じと見なせるかどうか、判定できるようにしたい』
こんなことができたらいいな、って思ったんですよね。
var queue1 = Queue<Int>()
queue1.enqueue(1) // [1]
queue1.enqueue(2) // [1,2]
var queue2 = Queue<Int>()
queue2.enqueue(1) // [1]
queue2.enqueue(2) // [1,2]
print(queue1 == queue2) // true ←こう書けるようにしたい
ただし、現時点ではこのコードはコンパイルエラーになってしまいます。
Queue
がEquatable
に適合していないのだから当然か。
print(queue1 == queue2)
// 結果: コンパイルエラー
Binary operator '==' cannot be applied to two 'Queue<Int>' operands
というわけで、Queue
をEquatable
に適合させましょう。せっかくなのでextension
を使って。
2つのQueue
が同じだとみなされる条件は...
「Queue
が内部に持つ要素とその順序がすべて同じならば同じ」、これでいきましょう。
// 用法3: `Queue`を`Equatable`に適合させる
extension Queue: Equatable {
static func ==(lhs: Queue<Element>, rhs: Queue<Element>) -> Bool {
// `Queue`が内部に持つ要素とその順序がすべて同じなら、同じとみなす
return lhs.elements == rhs.elements
}
}
ですが、このコードもコンパイルエラーになってしまいます。
(以下のコードはSwift 4.1で試していますが、4.0.3だと別のエラーメッセージが出るのでご注意ください)
赤色のエラーメッセージ部分に着目すると、
「Elements
に対し==
を呼び出すには、Queue
の型パラメータ<Element>
がEquatable
に適合している必要があるよ」、
「<Element>
がEquatable
に適合してないよ」、と言われています。
さらに褐色のエラーメッセージ部分に着目すると(画像の左側にあるペインの、背景が青色になっている部分をクリックすると表示される)、
「Element
がEquatable
に適合している必要があるよ」、
「"条件付き適合
" "[Element]
をEquatable
に"」という、いかにもなキーワードが並んでいます。
これらはどういうことなのでしょうか?
今見ているように、Element
型の配列どうしが同じかどうか確かめるコード lhs.elements == rhs.elements
は、コンパイルエラーになってしまいます。
ただし奇妙なことに、こちらのコードは正常に実行でき、期待通りの結果が返ります。
print([1,2,3] == [1,2,3]) // true
なぜ、配列によって ==
を呼び出せる場合とそうでない場合があるのでしょうか?
実は、条件付き適合は既にSwiftの標準ライブラリの中にも取り入れられており、その影響はArray
にも及んでいました。
今回のケースに関連する部分を見てみます(関連する部分だけを抽出しています)。
extension Array : Equatable where Element : Equatable {
public static func ==(lhs: Array<Element>, rhs: Array<Element>) -> Bool {
...
}
}
この記法はまさに条件付き適合で、「Arrayの型パラメータ<Element>
がEquatable
とみなせる場合に限り、ArrayもEquatable
に適合する」(その結果として、==
も呼べるようになる)という定義となります。
これで、配列によって==
が実行できる場合とそうでない場合がある理由が判明しました。
[1,2,3] == [1,2,3]
の場合は、配列の要素(Int
)がEquatable
に適合しているため、配列自体もEquatable
に適合し、結果として==
を呼び出すことができました。
一方elementsの場合、elementsの要素(Element
)がEquatable
に適合しているという定義は、現時点ではどこにも書いていません。
そのため、elementsはEquatable
に適合しておらず、当然==
(lhs.elements == rhs.elements
の部分)も呼び出すことができなかった(コンパイルエラーになった)、というわけですね。
// elementsが `Equatable` だなんて、これまでどこにも書いてない...
struct Queue<Element> {
...
private var elements = [Element]()
mutating func enqueue(_ newElement: Element) { ... }
mutating func dequeue() throws -> Element { ... }
}
extension Queue: Equatable {
static func ==(lhs: Queue<Element>, rhs: Queue<Element>) -> Bool {
return lhs.elements == rhs.elements // ← ここでコンパイルエラー
}
}
さて、なんとかここまで来れたのであれば、この定義を完成させるのはもう一息です。
「elementsの要素(Element
)がEquatable
に適合していないから配列自体もEquatable
に適合せず、その結果==
(lhs.elements == rhs.elements)を実行できない」
そういうのであれば、逆に
「もしElement
がEquatable
だった場合に限っては配列自体もEquatable
に適合し、その結果==
(lhs.elements == rhs.elements) が実行できる」
このような書き方ができたら非常に好都合ですよね。
...でも、そんな定義の書き方を、既に我々は知っているのではないでしょうか...?
やりましょう! Swiftが4.1以降(しつこくてごめんなさい)なことを確認して、いざ。
// 前述の`Queue`のextension定義の1行目に、"where Element: Equatable" を追加しただけ
extension Queue: Equatable where Element: Equatable {
static func ==(lhs: Queue<Element>, rhs: Queue<Element>) -> Bool {
return lhs.elements == rhs.elements
}
}
コメントにもありますが、このextension
定義の変更点は、1行目に where Element: Equatable
を追加しただけです。条件付き適合の書き方になりましたね。
このwhere
句の追加により発生したことを追うのは複雑なのですが、一歩ずつ見ていきましょう。
まず、この条件(where Element: Equatable
)を追加したことによって、
このextension
定義内({ ... }
)は、Element
型の要素がEquatable
であることが前提の世界になります
(つまり、Element
型の要素がEquatable
であることを前提にコードを書けるようになります)。
するとその結果、elementsがEquatable
に適合します。
(↑の説明で登場した、Arrayの条件付き適合の定義(条件)を思い出してください。
『配列の要素(Element)
がEquatable
なら、配列自体もEquatable
に適合する』を満たすことになります)
従って、elementsはEquatable
に適合したことで、==
が呼べるようになりました。
コンパイルエラーなく定義の内部を完成させることができたので、晴れてQueue
に対する条件付き適合の定義全体も完成しました。
つまり、Queue
の条件付き適合の定義は、その内部でelements
の条件付き適合が成立していることを前提にして書かれているわけですね。
完成した条件付き適合が有効かどうか、確認してみましょう。
var queue1 = Queue<Int>()
queue1.enqueue(1) // [1]
queue1.enqueue(2) // [1,2]
var queue2 = Queue<Int>()
queue2.enqueue(1) // [1]
queue2.enqueue(2) // [1,2]
print(queue1 == queue2)
// 結果
true
queue1.enqueue(3) // [1,2,3]
print(queue1 == queue2)
// 結果
false
ようやく上手くいきました!
「Queue
の型パラメータElement
がEquatable
だとみなせる場合のみ、Queue
はEquatable
に適合する」ということは当然、
「Queue
の型パラメータElement
がEquatable
だとみなせない場合は、Queue
はEquatable
に適合しない」ですよね。
// `Equatable`でない型を用意する
struct Devil {
let name: String
var lv: Int
var HP: Int
}
var notEqQueue1 = Queue<Devil>() // `<Element>`を、`Equatable`でない型(`Devil`)で特殊化
let jack_O_Frost = Devil(name: "ジャックフロスト", lv: 7, HP: 78)
notEqQueue1.enqueue(jack_O_Frost)
var notEqQueue2 = Queue<Devil>() // `<Element>`を、`Equatable`でない型(`Devil`)で特殊化
let jack_O_Lantern = Devil(name: "ジャックランタン", lv: 19, HP: 162)
notEqQueue2.enqueue(jack_O_Lantern)
print(notEqQueue1 == notEqQueue2) // ← エラー ∵ 2つの`Queue`は`Equatable`に適合していないので、`==`を呼べない
// 結果: コンパイルエラー
Type 'Devil' does not conform to protocol 'Equatable'
'<Self where Self : Equatable> (Self.Type) -> (Self, Self) -> Bool' requires that 'Devil' conform to 'Equatable'
こちらもちゃんとコンパイルエラーになってくれました。
###【追記・余談】 型の性質の表現 ー 関数 vs プロトコル
(この項目は、参照記事に、
Swiftはメソッドやプロパティで性質を判断するというより、プロトコルで一段階抽象化して性質を表現します
という記述があり、この点についてもっと詳しく理解したいと思い、調査・追記しました。)
ところで、条件付き適合がなかったSwift 4.1未満でも、
このように書けばQueue
に対し==
を定義し、呼び出すことができました。
こちらのコード、普通にSwift 4.1未満でも有効です。
// 用法1: 型に対して直接、条件付きで定義を追加
extension Queue where Element: Equatable {
static func ==(lhs: Queue<Element>, rhs: Queue<Element>) -> Bool {
return lhs.elements == rhs.elements
}
}
// この方法でも、`Queue`に対して問題なく`==`を呼び出せる
print(queue1 == queue2) // true
extension
の用法1を用い、Queue
という型に直接==
を定義してあげることで、
条件付き適合を用いて行ったこと相当のことを実現できています。
同じことができるのであれば、これまで見てきた小難しいことを考えなくても済むこちらのやり方の方が優れているようにも思えてきます。
この、2つの==
を呼べるようにする方法に、何か違いはあるのでしょうか?
用法1(定義の追加)では、関数で型の性質を表現
用法1を用いた方法は、
「Queue
はプロトコルEquatable
に適合はしていない。
でも、メソッド==
を持っているおかげで、Equatable
に相当する振る舞いがたまたま出来ている。」という状態です。
これは、==
が呼べるというQueue
の型の性質を、関数(メソッド)で表現している状態である、といえます。
こちらの場合、Queue
はあたかもEquatable
であるかのように振る舞えますが、
実際にはEquatable
に適合しているわけではありません。
引用先の*"メソッドやプロパティで性質を判断"*という一文は、
RubyやGoなどといった、ダックタイピングな特性を備えた言語を意識した上での表現なのかな、と考察しました。
『Queue
はメソッド==
を呼べる(持っている)ので、Equatable
として振る舞える』というのは、いかにもダックタイピングな挙動なのではないでしょうか。
用法3(条件付き適合)では、プロトコルで型の性質を表現
一方、用法3を用いた方法は、
Queue
は言うまでもなくプロトコルEquatable
に(条件付きで)「適合」しています。
これは、==
が呼べるというQueue
の型の性質を、(Equatable
という)プロトコルで表現している状態である、といえます。
前者と比較すると、よりSwiftらしい型の表現という意味では、こちらの方に軍配が上がるのでしょう。
とはいえ、同じことを実現するために、複数の表現方法があるのは興味深いです。
Arrayの==
はグローバル関数だった
さらに余談です。
Swift 4.1になった今でこそ、上述のように型に属する形で定義されるようになったArray
の==
ですが、
一つ前のマイナーバージョンである4.0の時点でさえそうではなく、
まさかの**グローバル関数として定義されていたのですね。まるで気づきませんでした...。
タイプメソッドとしてQueue
に属していた==
の例と異なり、こちらはもはやArray
に引数でしか紐付いてないですが、こちらも一種の関数による**型の性質の表現、といえるでしょう。
// ウソみたいだろ グローバル関数だったんだぜ
public func ==<Element : Equatable>(lhs: Array<Element>, rhs: Array<Element>) -> Bool {
...
}
個人的な感覚でも、Swift 4.1以降の定義の方が、自然で気持ち良いと感じます。
総仕上げ(実際のソースコードから)
最後に総仕上げとして、extension
が実際のソースコードでどのように用いられているかを簡単にチェックしていき、知識の定着を試みます。
以下、extension
はどのような用法で用いられているでしょうか...?
例1
ReactiveX/RxSwiftからの例です。
...
// 筆者補足: `Reactive`は構造体です
extension Reactive where Base: UILabel {
/// Bindable sink for `text` property.
public var text: Binder<String?> {
return Binder(self.base) { label, text in
label.text = text
}
}
...
}
回答例)
(構造体)Reactive
に対し、Reactive
の型パラメータ<Base>
がUILabel
とみなせる場合のみ有効になるコンピューテッドプロパティtext
を定義している。(用法1+where)
例2
Nirma/Defaultからの例です。
...
// 筆者補足: `DefaultStorable`はプロトコルです
// 筆者補足: `Self` = "実際にこのプロトコルに適合した具体的な型" のことを指します
extension DefaultStorable where Self: Codable {
...
public static func read(forKey key: String? = nil) -> Self? {
let key: String = key ?? defaultIdentifier
return defaults.df.fetch(forKey: key, type: Self.self) ?? defaultValue
}
...
}
回答例)
(プロトコル)DefaultStorable
に対し、このプロトコルに適合した型がCodable
とみなせる場合のみ有効になるデフォルト実装read
を定義している。(用法2+where)
例3
vapor/vaporからの例です。
...
// 筆者補足: `Key == String` = `Key`が`String`だった場合を指します
extension Dictionary: Content, RequestDecodable, RequestEncodable, ResponseDecodable, ResponseEncodable where Key == String, Value: Content {
/// See `Content.defaultMediaType`
public static var defaultMediaType: MediaType {
return .json
}
}
回答例)
Dictionary
の型パラメータ<Key>
がString
と等しく、かつ型パラメータ<Value>
がContent
とみなせる場合のみ、
Dictionary
はプロトコルContent
とRequestDecodable
とRequestEncodable
とResponseDecodable
とResponseEncodable
に適合する。(用法3+where)
おわりに
私がextension
に出くわすたび、その用法につきいつも混乱していたため、それを払拭すべく書き始めた今回の記事ですが、実際のソースコードを地道に整理していくことで、その実はとてもシンプルだったことが理解できました。
条件付き適合という新機能によって、extension
の3用法テーブルの最後のピースが埋まり、美しい対応関係がSwiftの文法にもたらされた、と個人的に感じています。
Swiftにおけるジェネリクスプログラミングは同言語の中心的なトピックであり、その進化が続いていくことは確実なので、今後も遅れを取らぬよう理解を深めていきたいです。
links
Swift 4.1+ (記事を書くに当たり何度も参照させていただきました。Swift 4.1は条件付き適合以外にも目を引く進化があり、それらを総ざらいできる必読の記事だと思います)
-
Swift実践入門(初版)の説明を参考にさせていただきました。 ↩