Swiftのプロトコル | プロトコル拡張

Swiftのプロトコル | Extensionを使ったプロトコル適合、プロトコル継承

Extensionとプロトコルを組み合わせると、大きく分けて2通りの拡張が可能です。1つはプロトコルを採用しつつ既存の型を拡張する方法、もう1つはプロトコル自身を拡張する方法です。前者は拡張する対象が異なる型になりますが、後者は自分自身を拡張することになります。後者の自分自身を拡張する方法を、プロトコル拡張(protocol extensions)と呼びます。

ここでは、まずextensionを使ったプロトコル準拠に関して詳しく説明します。それに関連して、プロトコル型のコレクションについて簡単に触れます。次にプロトコル継承について紹介した後に、プロトコル拡張について具体例を使いつつ説明します。

Extensionを使ったプロトコル準拠 | Adding Protocol Conformance with an Extension

Extensionを使ったプロトコル準拠の定義構文

Extensionsのページで簡単に紹介しましたが、extensionの定義時にプロトコルを採用することで、既存の型にプロトコルで定義した機能を追加することが出来ます。繰り返しになりますが、定義構文は

extension ExistingType: SomeProtocol {
    // プロトコル要求の追加
}

という形になります。

複数のプロトコルを採用する場合は、SomeProtocolの後ろにコンマを付けて別のプロトコルを加えます。

extension ExistingType: SomeProtocol, AnotherProtocol, OtherProtocol {
    // プロトコル要求の追加
}

Extensionを使ったプロトコル準拠の具体例 | サイコロクラスを拡張

「型としてのプロトコル」のページで作ったDiceクラスに、「プロトコル」のページで作ったHasNameプロトコルを採用してみます。

protocol HasName {
    var name: String { get }
}

// HasNameプロトコルを、既存クラスDiceに採用
extension Dice: HasName {
    var name: String {
        return "\(sides)面サイコロ"
    }
}

実際に実装してみると分かりますが、extensionを使ったプロトコル準拠の定義構文は、普通にプロトコル準拠させる場合とほとんど同じです。唯一違うのは、既存の型の拡張ですので、独自型の定義部分がextensionキーワードになっている点です。

このようにextensionでプロトコル準拠することで、DiceインスタンスをHasName型として取り扱うことが出来ます。

let d24 = Dice(sides: 24, generator: LinearCongruentialGenerator())
print(d24.name)
//"24面サイコロ"と表示

また、公式マニュアルにも注意書きがありますが、extensionによるプロトコル準拠も既存のインスタンスに適用されます。ですから、前回作った12面のDiceインスタンスd12からnameにアクセスすることが可能になります。

//既に作ったインスタンスにも適用される
print(d12.name)
//"12面サイコロ"と表示

同様に、人生ゲームをクラス化したJinseiGameクラスも、extensionを使ってプロトコルHasNameに準拠させることが出来ます。

extension JinseiGame: HasName {
    var name: String {
        return "人生ゲーム(\(maxBoardNumber)マス)"
    }
}
print(game.name)
//"人生ゲーム(10マス)"と表示

gameインスタンスは以前作ったJinseiGameのインスタンスです。

空のextensionでプロトコル採用(事前にプロトコル要求を満たしている型の場合) | Declaring Protocol Adoption with an Extension

ここでは、

プロトコルを採用してない型が、プロトコルの要求を満たしている場合はどうなるのか?

という点について取り上げます。結論から言うと、このケースでは自動的にプロトコルが採用されないので注意が必要です。もし同じ要求を持ったプロトコルに準拠させようと思ったら、明示的にプロトコルを採用しないといけません。

例えば、HasNameの持つnameプロパティを既に持っているPersonという構造体を考えます。

struct Person {
    var name: String
}

Personnameプロパティを持っていますが、HasNameプロトコルを採用していませんので、デフォルトではHasNameには準拠していません。

この構造体をHasNameプロトコルに準拠させたい場合は、空のextensionを使って

extension Person: HasName {}

このようにプロトコルHasNameを採用することが可能です。HasNameが持つnameプロパティ要求はすでに満たしているので、このような書き方が可能になります。

こうするとPersonHasName型としても利用できるようになります。この後説明する配列(コレクション型)の要素としてプロトコル型を利用する場合に非常に強力です。

let tarou = Person(name: "太郎")
print(tarou.name)
//"太郎"と表示

// Personで作ったインスタンスをHasName型のインスタンスに代入
let nameTarou: HasName = tarou
print(nameTarou.name)
//"太郎"と表示

プロトコル型のコレクション(配列) | Collection of Protocol Types

プロトコルを型として使う場合に、

配列、辞書またはその他のコレクション型要素の型として使う

ことが出来ると書きました。先程HasNameプロトコルを既存クラスに準拠させていましたが、これらをひとまとめにして配列に入れることが出来ます。

// HasName型要素を持った配列
let nameArray: [HasName] = [d24, game, tarou]
for item in nameArray {
    print(item.name)
}
// printの表示結果
//24面サイコロ
//人生ゲーム(10マス)
//太郎

一つ前の例でtarouHasName型のnameTarouに代入したケースと同様で、ここでの配列要素の型はHasNameです。したがって、for-inループで取り出している配列要素のitemの型はHasNameであって、Diceなどではありません。

それぞれのインスタンス本来の型を取り出したい場合は、型キャストを使う必要があります。

for item in nameArray {
    if item is Dice {
        print("サイコロクラス: \(item.name)")
    }
}
//"サイコロクラス: 24面サイコロ"と表示

プロトコル継承 | Protocol Inheritance

プロトコル継承の定義構文

プロトコル継承(Protocol Inheritance)というのは、別のプロトコルを継承することで、継承元の要求(プロパティやメソッドなど)を引き継ぐことです。

採用(adopt)と言っても良いと思いますが、公式では継承(inherit)を使っています。

定義構文は

// プロトコル継承
protocol InheritingProtocol: SomeProtocol, AnotherProtocol {
    // プロトコルの定義
}

という具合に、継承したいプロトコルをプロトコル定義構文の後ろにコロン:を付けて続けます。複数のプロトコルを継承したい場合はコンマで区切ります。独自型に複数のプロトコルを採用するケースと構文は同じです。

プロトコル継承の具体例

HasNameプロトコルを継承したHasTitle

公式マニュアルと同じ構造を持ったプロトコルを作ってみます。

protocol HasTitle: HasName {
    var title: String { get }
}

この例では、HasNameプロトコルを継承した、プロトコルHasTitleを定義しました。HasTitleを採用したクラスなどは、その継承元であるHasNameの要求も満たさないといけません。

例えば、

struct SomeStruct: HasTitle {
    var title: String { return "some structure" }
}
//コンパイルエラー
// error: type 'SomeStruct' does not conform to protocol 'HasName'
// note: protocol requires property 'name' with type 'String'

このようにHasTitleを採用してtitleのみを実装した場合、「HasNameに準拠していない」と怒られて、コンパイルエラーになります。

人生ゲームにHasTitleを採用してさらに機能拡張

人生ゲームクラスJinseiGameHasTitleを採用して機能拡張してみます。

extension JinseiGame: HasTitle {
    var title: String {
        var output = name + ":\n"
        for index in 0..<maxBoardNumber {
            switch board[index] {
            case 0: // 0点
                output += "○"
            case 1000: // 1000点
                output += "●"
            default: // それ以外
                output += "□"
            }
        }
        output += "\n"
        output += "○: 0点\n"
        output += "●: 1000点"
        
        return output
    }
}

print(game.title)
//人生ゲーム(10マス):
//○●●●●●●●●●
//○: 0点
//●: 1000点

JinseiGameクラスは一度HasNameを採用しているので、すでにHasNameのプロパティ要求nameは実装済みです。したがって、ここの拡張で実装しないといけないのはHasTitleのプロパティ要求titleのみです。

もし一度もextensionでプロトコルを採用していない型の場合は、採用したプロトコルが持っている要求全てを満たすように実装しないとコンパイルエラーになります。

クラス限定のプロトコル | Class-Only Protocols

プロトコルをクラス限定として使いたい場合には、プロトコル継承の際に、継承リストの先頭にclassキーワードを付けます。

protocol SomeClassOnlyProtocol: class, SomeInheritingProtocol {
    // プロトコルの定義
}

と、公式マニュアルでは書いてありますが、単体のプロトコルをクラス専用として定義したい場合には、

protocol SomeClassOnlyProtocol: class {
    // プロトコルの定義
}

という書き方が可能です。これなら別に継承するプロトコルが無い場合でも使えます。

具体的な使い方は実際やってみないと分かりませんが、循環参照を避ける場合や、構造体のメソッドで明示的にmutatingを書くのが面倒くさいという場合に使えるような気がします。

プロトコル拡張 | Protocol Extensions

プロトコル拡張では中身を実装する

Extensionsのページで少し触れましたが、プロトコル自身の機能を拡張することをプロトコル拡張(Protocol Extensions)と呼びます。プロトコル拡張ではプロトコル自身にプロパティやメソッドの中身を実装します。これが通常のプロパティ・メソッド要求と大きく異なる点です。

公式マニュアルと同様にRandomNumberGeneratorプロトコルの機能拡張をしてみます。公式ではrandomBool()というメソッドを実装していますが、ここではrandomInt()メソッドを実装してみます。

extension RandomNumberGenerator {
    // 整数の擬似乱数、範囲は[min, max)
    // DoubleからIntへの変換の関係から、minは含むがmaxは含まれない
    func randomInt(_ min: Double, _ max: Double) -> Int {
        return Int((max-min)*random() + min)
    }
}

パラメータで擬似乱数の範囲を指定すると、その範囲内でランダムな整数を返すメソッドです。プロトコル拡張でプロパティやメソッドを実装することで、そのプロトコルを採用している全ての型が拡張機能を持つことになります。

今回のケースだと、RandomNumberGeneratorプロトコルを採用しているLinearCongruentialGeneratorクラスが該当します。

let rand = LinearCongruentialGenerator()
print("整数の乱数: ", terminator:"")
for _ in 0...10 {
    print(rand.randomInt(10, 20), terminator:" ")
}
print()
//"整数の乱数: 13 17 16 17 15 11 15 13 10 15 12"と表示

このように、RandomNumberGeneratorプロトコルの機能拡張をすると、それを採用している型(ここではLinearCongruentialGenerator)からrandomInt(min:max:)メソッドを呼び出すことが出来ます。

デフォルトの実装を提供する | Providing Default Implementations

プロトコル拡張を使うと、既存の要求(プロパティ要求やメソッド要求)に対して、デフォルトの中身を実装することも出来ます。具体例で示した方が分かりやすいと思いますが、例えば

// HasTitleプロトコルを拡張、titleにデフォルト機能を実装
extension HasTitle {
    var title: String {
        return name
    }
}

のように、既存のプロパティ要求titleを拡張して中身を実装し、デフォルトの振る舞いを決めることが出来ます。

実際にこのプロトコルを採用する場合、titleプロパティ要求にはデフォルトの実装があるので、

// titleには中身があるので、name要求だけを満たせばコンパイルは通る
struct SomeStruct: HasTitle {
    var name: String { return "some structure" }
}
let someStruct = SomeStruct()
print("name:\(someStruct.name), title:\(someStruct.title)")
//"name:some structure, title:some structure"と表示

このようにプロトコルの要求を明示的に満たさなくてもコンパイルは通ります。また、titleにアクセスすると、デフォルトで実装されているようにnameの内容がそのまま返ってきます。

プロトコル拡張に制限を加える | Add Constraints to Protocol Extensions

プロトコル拡張を定義する際に、where構文を使って何かしらの制約を加える(条件を付ける)ことが出来ます。

今回は公式の具体例を流用します。

extension Collection where Self.Iterator.Element: HasName {
    var name: String {
        let itemAsText = self.map { $0.name }
        return "[" + itemAsText.joined(separator: ", ") + "]"
    }
}

let jirou = Person(name: "次郎")
let saburou = Person(name: "三郎")
let brothers = [tarou, jirou, saburou]
print(brothers.name)
//"[太郎, 次郎, 三郎]"と表示

色々な要素があるので1つずつ順番に説明します。

Collectionプロトコル

まずそもそもCollectionとはなんぞや?ということですが、名前の通りコレクション型(配列など)のベースになるプロトコルです。Collectionがプロトコルであるというのは、公式API Referenceにある「Swift Standard Library」を見ると分かります。

Swift 2.xまでは「CollectionType」という名前のプロトコルでしたが、Swift 3から名前の付け方のガイドラインが変更になり、「Collection」という名前のプロトコルになりました

参考:
API Reference, Swift Standard Library
https://developer.apple.com/reference/swift
から「Collection」を検索、大文字と小文字は関係なし。

API Reference, Swift Standard Library, Collectionのページのスクリーンショット
Collectionプロトコルのページを開くと、上記画像のようなページになります。確かにプロトコルであることが分かります。


プロトコル拡張にwhere構文で制約を加える

whereを使った定義構文はif文などで使用する場合と同様で、

extension TypeName where condition1, condition2, ...  {
    // プロトコル拡張の定義
}

という形で条件を付けることが可能です。複数の条件を同時に付ける場合は条件をコンマで繋ぎます。

今回の例では条件は1つで、

extension Collection where Self.Iterator.Element: HasName

となっています。条件の中身はこれから見ていくとして、上記のプロトコル拡張を読み取ると、

Collectionプロトコルを拡張し、かつIterator.ElementHasNameというプロトコルを採用する

ということになります。

Iterator.Elementって何?

条件中に入っているIterator.Elementは、その記述から一瞬プロパティ要求のように見えます。しかし、リファレンスを見ると、IteratorCollection内部でassociatedtypeとして宣言されていることが分かります。宣言本体を見ると、

associatedtype Iterator : IteratorProtocol = IndexingIterator<Self>

となっています。難しく見えますが、パーツに分けて見ていくと理解出来ると思います。

associatedtype Iterator

この部分だけを抜き出した記述が最も基本的な形です。このassociatedtypeというのは、Genericsのページで詳しく説明しますが、プロトコルで特定の型を定義したくない場合に使える記述です。つまり、Iteratorを規定する制約は一切ありませんので、どんな型を実装しても良いことになります。

実はElementIteratorProtocolが持つassociatedtypeです(後述)。

IteratorはIteratorProtocolプロトコル

もう一歩踏み込んで、Iterator元々の定義の左辺のみを見てみると、

associatedtype Iterator : IteratorProtocol

となっています。したがって、IteratorというassociatedtypeIteratorProtocolに準拠した型であることが分かります。APIリファレンスを見ると分かりますが、IteratorProtocolはプロトコルです(名前から自明かもしれません)。

APIリファレンスによるとIteratorの役割は

A type that provides the collection’s iteration interface and encapsulates its iteration state.

ですから、forループなどで要素を取り出す時の、その要素(イテレータ)自身とそいつが持つ機能を規定した型、ということになります。

ここで大元のIteratorの定義に戻ると、

associatedtype Iterator : IteratorProtocol = IndexingIterator<Self>

となっていました。繰り返しになりますが、associatedtypeであるIteratorIteratorProtocolプロトコルに準拠した型です。さらに、デフォルトではIndexingIterator<Self>という構造体で初期化されています。

右辺の

IndexingIterator<Self>

Genericsの記述なのでややこしいですが、Selfというのはプロトコルで使う場合の自分自身への参照です。最初が大文字なのがポイントで、プロトコルでは必ずこの大文字から始まるSelfを使います。これまで見てきた小文字から始まるselfとは別物なので注意が必要です。

IndexingIteratorという構造体はIteratorProtocolプロトコルに準拠しているので、このような代入が可能です。デフォルトでIteratorにはIndexingIterator<Self>が割り当てられているので、ユーザはIteratorに特定の型を対応させなくても良いことになります。

ElementはIteratorProtocolが持つassociatedtype

タイトルのままですが、リファレンスを見るとElementというのはIteratorProtocolが持っているassociatedtypeで、コレクション型の要素の型を規定するモノになります。

associatedtype Element

where以降はCollection内部の定義

通常型を定義する時などは

// 型の外
struct SomeType {
    // 型の中
}

という感じで、カッコの中か外でスコープを分けます。そういう視点で最初のプロトコル拡張定義を見ると、

extension Collection where Iterator.Element: HasName

というのはwhere以降がCollectionの外側の定義のように見えますが、実際はこれまで見てきたようにIterator.ElementCollection内部で定義されているモノです。

配列要素をコンマで分けて繋げる

var name: String {
    let itemAsText = self.map { $0.name } // itemAsTextは配列
    return "[" + itemAsText.joined(separator: ", ") + "]"
}

敢えてnameプロパティで実装しているのでややこしいですが、これはHasNameプロトコルのプロパティ要求nameとは関係ありません(nameを適当な名前に変えて実行してみると分かります)。

2行目

let itemAsText = self.map { $0.name } // itemAsTextは配列

がクロージャ表現になっているので一見ややこしいです。省略しない表現に戻してみると、

let itemAsText = self.map({ (s1: Iterator.Element) -> String in
    return s1.name })

となります。selfCollectionに準拠した型のインスタンスになります。(ここで使われている)Collectionmapは、パラメータが(Self.Iterator.Element) throws -> T、戻り値がrethrows -> [T]です。throwsrethrowsはエラー処理に関連するキーワードなので、それを省略すると

パラメータ: (Self.Iterator.Element) -> T(関数型)
戻り値: [T]T型の配列)

となります。これを上記の具体例と見比べてみると、TStringに対応しているのが分かります。

このmapで実行しているのは単純に配列要素からnameプロパティを持ってきて、それを配列itemAsTextへ順番に入れているだけです。

3行目

return "[" + itemAsText.joined(separator: ", ") + "]"

では、配列要素をコンマ", "で繋げて連結し出力しています。この連結を実行しているのが、配列Arrayが持っているメソッドjoined(separator:)です。

配列からnameプロパティにアクセス

これでようやく最初のサンプルコードに戻りますが、プロトコル拡張によって実装したnameプロパティを配列から呼び出すと、

let jirou = Person(name: "次郎")
let saburou = Person(name: "三郎")
let brothers = [tarou, jirou, saburou]
print(brothers.name)
//"[太郎, 次郎, 三郎]"と表示

確かに配列に入れた順番にPersonインスタンスが並んでいて、そのインスタンスが持つプロパティがコンマで分けられて連結されているのが分かります。

まとめ

  • extensionを使うと既存の型にプロトコル機能を追加出来る
  • プロトコル準拠は明示的にプロトコルを採用している場合のみ適用される
  • プロトコルは型としても使えるので、配列の要素として取り扱うことが出来る
  • プロトコル継承によって、あるプロトコルに別にプロトコルの機能を追加することが可能
  • プロトコルをクラス限定としたい場合、プロトコル継承リストの先頭にclassキーワードを入れる
  • プロトコル拡張で、プロトコルにデフォルト機能を実装出来る
  • プロトコル拡張に制限を加える場合はwhere構文

Swiftのプロトコル | プロトコルの合成