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

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

プロトコルの合成(Protocol Composition)です。複数のプロトコルにまとめて準拠させる場合に便利な構文になります。

ここではプロトコル合成とは何か?を簡単に説明した後、具体例を使ってその使い方を見ていきます。プロトコル合成の基本的な用途は関数やメソッドのパラメータです。typealiasを使うと少し面白いことが出来るので、それも合わせて紹介します。

Protocol Compositionって何?

複数のプロトコルにまとめて準拠させる機能

タイトルの通りですが、Protocol Compositionというのは、ある型を複数のプロトコルに準拠させる場合に使える便利な構文になります。Protocol compositionを日本語にすると「プロトコル合成」でしょうか。プロトコル合成では、プロトコルを&マークで繋いで、

SomeProtocol & AnotherProtocol

という感じで指定します。上記の例だと2つのプロトコルの合成ですが、原理的には何個でも繋げることが可能で、複数指定する場合はその都度&マークでプロトコルを並べていきます。

関数等のパラメータ以外にも使える?

プロトコル合成は、基本的には関数(functions)メソッド(methods)のパラメータの型として使います。後で具体例を使って見せますが、typealiasを使うと、関数等のパラメータ以外にも色々(プロトコルの採用、プロトコルの継承プロトコル拡張など)利用出来ます。ただし、わざわざプロトコル合成を使わなくても、(例えばプロトコル継承など)元々用意されている定義構文があるので、そちらを使った方が良いかもしれません。

Swift 2.xとSwift 3では仕様が違う

このサイトではSwift 3の仕様に従って記述しているので、&マークで繋ぐ構文になっています。

Swift 2.xまでは、

protocol<SomeProtocol, AnotherProtocol>

という定義構文でした。Swift 3から&で繋ぐ仕様に変更されています。

プロトコル合成の具体例 | 3つのプロトコルに準拠させる

公式マニュアルでは2つのプロトコル(Named, Aged)を使っていましたが、試しに3つのプロトコルを合成してみます。

3つのプロトコルを採用した構造体、プロトコル合成を使った関数パラメータ

// プロトコルの合成、3つのプロトコル
// シフト、平行移動
protocol Transferable {
    var shift: Int { get }
}

// 回転
protocol Rotatable {
    var angle: Double { get }
}

// 名前
protocol Named {
    var name: String { get }
}

// Point構造体
struct Point: Transferable, Rotatable, Named {
    var shift: Int
    var angle: Double
    var name: String
}

func show(_ point: Transferable & Rotatable & Named) {
    print("名前:\(point.name) | 座標\(point.shift)に平行移動、角度\(point.angle)度だけ回転")
}

var point = Point(shift: 2, angle: 10.2, name: "点1")
show(point)
//"名前:点1 | 座標2に平行移動、角度10.2度だけ回転"と表示

3つのプロトコルTransferableRotatableNamedを作り、それら3つのプロトコルに準拠させた構造体Pointを用意しました。

プロトコル合成を実行しているのは関数show(_:)のパラメータの部分で、

func show(_ point: Transferable & Rotatable & Named) {

という構文になっています。このパラメータの型を日本語にすると、

プロトコルTransferableRotatable、及びNamed全てに準拠している型

ということになります。つまり「上記3つのプロトコルに準拠している限りどんな型でも良い」という条件になります。もっと限定的な言い方に変えれば「上記3つのプロトコルに準拠している型のみ」とも言えるかもしれません。

プロトコルの合成 | 関数の引数で複数のプロトコルを同時に指定

先程も説明しましたが、関数show(_:)引数(パラメータ)の型がプロトコル合成で指定されています。

func show(_ point: Transferable & Rotatable & Named) {
    print("名前:\(point.name) | 座標\(point.shift)に平行移動、角度\(point.angle)度だけ回転")
}

今、Point構造体は3つのプロトコルを採用して作ったので、関数show(_:)の引数として渡すことが出来て、

var point = Point(shift: 2, angle: 10.2, name: "点1")
show(point)
//"名前:点1 | 座標2に平行移動、角度10.2度だけ回転"と表示

実行すると上記のような結果になります。

ここで、試しにNamedだけを採用した構造体を関数show(_:)の引数として渡すと、

// Namedだけを採用した場合
struct Person: Named {
    var name: String
}
let person = Person(name: "太郎")
show(person)
//error: argument type 'Person' does not conform to expected type 'Named & Rotatable & Transferable'

とコンパイルエラーになります。今はNamedでしたが、上記3つのプロトコルを個別に(またはその中から2つ)採用している場合はコンパイルエラーになります。プロトコル合成で指定された全てのプロトコルに準拠していない限りコンパイルは通りません。

公式マニュアルに注意書きがありますが、プロトコル合成は新しいプロトコル型を定義している訳では無く、全てのプロトコル要求を組み合わせた一時的かつローカルな(関数やメソッド内部にスコープが限定された)プロトコルを定義しています。

“NOTE

Protocol compositions do not define a new, permanent protocol type. Rather, they define a temporary local protocol that has the combined requirements of all protocols in the composition.”

抜粋:: Apple Inc. “The Swift Programming Language (Swift 3)”。 iBooks https://itun.es/jp/jEUH0.l

複数のプロトコルを組み合わせた型 | Typealiasで新しい複合型を作る

Typealias指定子を使うと、先程の3つのプロトコルをひとまとめにした新しい型のようなものを作ることが出来ます。

// typealiasを使ってプロトコルをひとまとめ
typealias TransferRotateName = Transferable & Rotatable & Named

この「型の別名」を使って、先程の関数のパラメータを指定することも出来ます。

func show(_ point: TransferRotateName) {
    print("名前:\(point.name) | 座標\(point.shift)に平行移動、角度\(point.angle)度だけ回転")
}

ここまでは単なる書き換えですが、typealiasを使うと、通常のプロトコル合成ではコンパイルが通らない構文で複数のプロトコルを使うことが出来ます。ここでは、プロトコルの採用、プロトコル継承、プロトコル拡張に関して簡単に説明します。

ただし、最初に

わざわざプロトコル合成を使わなくても、(例えばプロトコル継承など)元々用意されている定義構文があるので、そちらを使った方が良いかもしれません。

と述べましたが、複数のプロトコルを当てはめる構文は提供されているので、わざわざプロトコル合成のtypealiasを使う理由は余りありません。敢えてプロトコル合成を使う理由を考えるなら、複数のプロトコルを採用する時に毎回何個もプロトコルを並べて書くのが面倒くさい、でしょうか。

複数のプロトコルを採用

// プロトコル合成でまとめて採用
struct AnotherPoint: TransferRotateName {
    var shift: Int
    var angle: Double
    var name: String
}

Typealiasを使うと型として認識されるようで、上記のAnotherPoint構造体を作ったような構文が通ります。

一方で、明示的にプロトコル合成で採用しようとすると、

// こう書くとだめ
struct OtherPoint: Transferable & Rotatable & Named { // <--ここでエラー
    var shift: Int
    var angle: Double
    var name: String
}
//エラーメッセージ: Protocol composition is neither allowed nor needed here

という感じで怒られます。「そもそもprotocol compositionを使う場所じゃないだろ?」と言われています。普通に複数のプロトコルを採用する場合はコンマ(,)で繋ぎます。

// 複数のプロトコルを採用する時はコンマで繋ぐ
struct OtherPoint: Transferable, Rotatable, Named {

複数のプロトコルを継承

プロトコル継承の場合も同様で、

// プロトコル継承
// これは通る
protocol SomeProtocol: TransferRotateName { }

// これはだめ
protocol AnotherProtocol: Transferable & Rotatable & Named { }
//エラーメッセージ: Protocol composition is neither allowed nor needed here

typealiasを使ったSomeProtocolはコンパイルが通りますが、プロトコル合成で継承させようとしているAnotherProtocolはエラーが出ます。怒られる時のエラーメッセージは全く一緒です。プロトコル継承も通常の構文ではコンマで区切ります。

プロトコル拡張で複数のプロトコルを指定

前回プロトコル拡張という機能を説明しましたが、その場合の指定も可能で

// プロトコル拡張
struct SomeStruct {}
extension SomeStruct: TransferRotateName {
    var shift: Int { return 0 }
    var angle: Double {
        set {
            angle = newValue
        }
        get {
            return self.angle
        }
    }
    var name: String { return "" }
}

このようにtypealiasで指定した型TransferRotateNameだとコンパイルが通りますが、明示的にプロトコル合成で指定した場合

extension AnotherStruct: Transferable & Rotatable & Named {
    ....
//エラーメッセージ: Protocol composition is neither allowed nor needed here

コンパイルは通りません。この場合も通常の構文では、複数のプロトコルをコンマで区切ります。

複数のプロトコルを指定する方法まとめ

一応まとめると、基本的に複数のプロトコルを指定する場合は、それぞれのプロトコルをコンマ(,)で区切ります。具体的には、上記で紹介したケース: プロトコルの採用、プロトコル継承、プロトコル拡張等が挙げられます。プロトコル合成だけが特殊で、この場合はそれぞれのプロトコルを&マークで繋ぎます。

// プロトコル採用、継承、拡張(protocol composition以外全てのケース)
SomeProtocol, AnotherProtocol, OtherProtocol

// プロトコル合成
SomeProtocol & AnotherProtocol & OtherProtocol

まとめ

  • プロトコルの合成(Protocol Composition)は複数のプロトコルを関数やメソッドのパラメータに指定する時に使う
  • プロトコルの連結には&マークを使う: SomeProtocol & AnotherProtocol
  • Swift 2.xとSwift 3では、プロトコル合成の仕様が違うので注意
  • プロトコル合成は、typealiasを使うと様々なケース(プロトコル継承など)で使用可能になるが、通常の定義構文を使用する方が無難、だと思う

Swiftのプロトコル | プロトコル準拠の確認