Edited at

Swiftのextensionは3パターンだけ〜そして条件付き適合へ・・・〜

More than 1 year has passed since last update.

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以下の条件を満たす場合のみ有効になるデフォルト実装を追加するという構文になります。

以下の例は、プロトコルSwimmableRunnableの両方に適合している型でのみ有効になるインスタンスメソッド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に適合させたい場合、

実直に書くとこのようになると思いますが...。


ViewController.swift

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
}

}


これを以下のように書き換えることができます。


ViewController.swift

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 ←こう書けるようにしたい

ただし、現時点ではこのコードはコンパイルエラーになってしまいます。

QueueEquatableに適合していないのだから当然か。

print(queue1 == queue2)

// 結果: コンパイルエラー
Binary operator '==' cannot be applied to two 'Queue<Int>' operands

というわけで、QueueEquatableに適合させましょう。せっかくなので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だと別のエラーメッセージが出るのでご注意ください)

Screen Shot 2018-03-27 at 11.14.16 PM.png

赤色のエラーメッセージ部分に着目すると、

Elementsに対し==を呼び出すには、Queueの型パラメータ<Element>Equatableに適合している必要があるよ」、

<Element>Equatableに適合してないよ」、と言われています。

さらに褐色のエラーメッセージ部分に着目すると(画像の左側にあるペインの、背景が青色になっている部分をクリックすると表示される)、

ElementEquatableに適合している必要があるよ」、

「"条件付き適合" "[Element]Equatableに"」という、いかにもなキーワードが並んでいます。

これらはどういうことなのでしょうか?

今見ているように、Element型の配列どうしが同じかどうか確かめるコード lhs.elements == rhs.elementsは、コンパイルエラーになってしまいます。

ただし奇妙なことに、こちらのコードは正常に実行でき、期待通りの結果が返ります。


この配列同士はエラーにならず、ちゃんと比較できている...!?

print([1,2,3] == [1,2,3])  // true


なぜ、配列によって == を呼び出せる場合とそうでない場合があるのでしょうか?

実は、条件付き適合は既にSwiftの標準ライブラリの中にも取り入れられており、その影響はArrayにも及んでいました。

今回のケースに関連する部分を見てみます(関連する部分だけを抽出しています)。


Array.swift(4.1)

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)を実行できない」

そういうのであれば、逆に

「もしElementEquatableだった場合に限っては配列自体も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の型パラメータElementEquatableだとみなせる場合のみ、QueueEquatableに適合する」ということは当然、

Queueの型パラメータElementEquatableだとみなせない場合は、QueueEquatableに適合しない」ですよね。

// `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に引数でしか紐付いてないですが、こちらも一種の関数による型の性質の表現、といえるでしょう。


Array.swift(4.0)

// ウソみたいだろ グローバル関数だったんだぜ

public func ==<Element : Equatable>(lhs: Array<Element>, rhs: Array<Element>) -> Bool {
...
}

個人的な感覚でも、Swift 4.1以降の定義の方が、自然で気持ち良いと感じます。


総仕上げ(実際のソースコードから)

最後に総仕上げとして、extensionが実際のソースコードでどのように用いられているかを簡単にチェックしていき、知識の定着を試みます。

以下、extensionはどのような用法で用いられているでしょうか...?


例1

ReactiveX/RxSwiftからの例です。


UILabel+Rx.swift

...

// 筆者補足: `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.swift

...

// 筆者補足: `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からの例です。


Content.swift

...

// 筆者補足: `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はプロトコルContentRequestDecodableRequestEncodableResponseDecodableResponseEncodableに適合する。(用法3+where)


おわりに

私がextensionに出くわすたび、その用法につきいつも混乱していたため、それを払拭すべく書き始めた今回の記事ですが、実際のソースコードを地道に整理していくことで、その実はとてもシンプルだったことが理解できました。

条件付き適合という新機能によって、extensionの3用法テーブルの最後のピースが埋まり、美しい対応関係がSwiftの文法にもたらされた、と個人的に感じています。

Swiftにおけるジェネリクスプログラミングは同言語の中心的なトピックであり、その進化が続いていくことは確実なので、今後も遅れを取らぬよう理解を深めていきたいです。


links

Swift 4.1+ (記事を書くに当たり何度も参照させていただきました。Swift 4.1は条件付き適合以外にも目を引く進化があり、それらを総ざらいできる必読の記事だと思います)





  1. Swift実践入門(初版)の説明を参考にさせていただきました。