Swiftのプロトコル(Protocols)

Swiftのプロトコル(Protocols)

プロトコル(Protocols)です。和訳すると「外交儀礼」「手順」「手続き」「(コンピュータの)通信規約」などの意味があります。Swiftのプロトコルは、平たく言うとおおざっぱな決まりが書かれたもので、他のプログラミング言語でいうところのインターフェイスに近いようです。

ここでは、プロトコルとは何か?から始めて、プロトコルの定義構文、プロトコルで定義(要求、requirements)するプロパティ、メソッド、初期化子の構文について詳しく説明していきます。

プロトコル(Protocols)って何?

プロトコル = プロパティやメソッドの設計図

プロトコル(Protocol)

プロパティメソッドなどの設計図

を定義するためのものです。

この「設計図」というのは、簡単に言うとメソッドなどの名前や型のみを定義することです。プロトコルは、基本的に中身は提供せず、メソッドなどの雛形(インターフェイスと言っても良いかもしれません)を提供するための機構です。

実装はクラスなどで行う

プロトコルではメソッドやプロパティの雛形だけを提供します。これらメソッド等の中身を実装するのは、プロトコルを採用したクラス、構造体、または列挙型です。プロトコルを採用すると、例えばクラスAとBは同じ名前のメソッドを持つけど中身がぜんぜん違う、といった実装が可能になります。このように

共通のインターフェイスを使いたいけど個々の実装は色々変えたい

というケースで特にプロトコルは役に立ちます。

プロトコルの要求を満たすような型のことを、プロトコル準拠(protocol conformance)、またはプロトコルに準拠する、と言います。

「準拠する」という部分は「適合する」と言っても良いかもしれません。

プロトコル拡張(Protocol Extensions)という機能もある

また、Extensionsのページで少し説明しましたが、プロトコル自体を拡張することによって、プロトコルに何かしらの機能を実装することも可能です。これはプロトコル拡張(Protocol Extensions)で詳しく説明します。

プロトコルの構文 | Protocol Syntax

プロトコルの定義はprotocolキーワード

プロトコルを定義する時は、protocolキーワードを使います。定義の仕方はクラスなどと同様の構文になります。

protocol SomeProtocol {
    // プロトコルの定義
}

またプロトコルの名前は、他の型と同様に大文字から始めます。

プロトコルを採用する(適用する) | Adopting Protocol(s)

プロトコルを採用(または適用)するには、

struct SomeStructure: FirstProtocol, AnotherProtocol {
    // 構造体の定義
}

という感じで、型を定義する際に、型名の後ろにコロン:を置いて、その後ろにプロトコルを続けます。形式としてはクラスの継承と全く一緒です。違いは複数のプロトコルを採用できることで、その場合はコンマ,でプロトコルを分けます。

また、サブクラスがスーパークラスを継承しようとしている場合、プロトコル名はその後ろに続けます。例えば、

// 先ずスーパークラスの名前、その後ろにプロトコル
class SubClass: SuperClass, FirstProtocol, AnotherProtocol {
    // クラスの定義
}

という感じです。

プロパティの要求 | Property Requirements

要求(Requirements)って何?定義とは違うのか?

プロトコル内部でプロパティやメソッド等を定義することをrequirementと呼んでいます。和訳すると「要求」「要件」です。例えば、プロトコルでプロパティを定義することをproperty requirements、メソッドだとmethod requirementsです。

なんでこんな呼び方になっているかと言うと、プロトコルで定義されたプロパティ等は、そのプロトコルを採用したクラスなどで必ず実装しなければならないからです。つまり、実質プロトコルで定義されたプロパティ(またはメソッドなど)が、採用側に

このプロパティを実装しろ

と要求していることになるから、プロパティの要求(property requirements)なんだと思います。

プロパティの名前と型だけが必要

プロパティ要求で必要な項目を列挙すると、

  • プロパティの名前と型
  • Gettable、またはgettable and settableの指定

この2つだけです。また、プロパティ要求では、それが格納プロパティ(stored property)計算プロパティ(computed property)かどうかは問わないので、どちらにするかは実装先のクラス等で決めることになります。後で具体例を使って説明しますが、型プロパティを作ることも出来ます。

プロパティ要求の構文

プロパティ要求の基本構文

プロパティ要求の基本的な定義構文は、

// プロパティ要求
protocol SomeProtocol {
    var mustBeSettable: Int { get set }
    var doesNotNeedToBeSettable: Int { get }
}

の2通りです。上記ソースコードを見てもらうと分かりますが、

プロトコルでは常にvarでプロパティを定義

しないといけません。計算プロパティがlet指定出来ないという理由だと思いますが、そもそもプロトコルでは中身を実装しないので定数にする意味が無いと思います。

Gettableとsettable

また、プロパティ要求に必要な項目で書きましたが、

  • gettable: { get }
  • gettable and settable: { get set }

の指定も必要です。{ get set }{ set get }でも大丈夫です。

「gettable and settable」というのは、どちらの条件も満たす(条件としてはAND)プロパティのことを指しますから、「gettable」な要求よりも実は厳しい条件を課していることになります。

Gettableとsettableの集合図
概念的な話にしかなりませんが、集合で書くと上の図のような感じです。円の大きさは適当ですが、gettableがプロパティの全体を指すのに対して、それよりもキツイ条件を課している「gettable and settable」というのは、プロパティ全体に対する特定の部分を指すことが分かります。

具体的には、例えば定数プロパティ(constant stored property)や読み取り専用計算プロパティ(read-only computed property)は、settableの条件を満たすことが出来ません。プロパティは常に「gettable」なので、実装側のクラス等で「settable」なプロパティでも、gettableの条件は満たしています。

型プロパティ要求

型プロパティの要求では、常にstaticキーワードを使います。

// 型プロパティ要求
protocol SomeProtocol {
    static var typeProperty: Int { get set }
}

これは実装側でclassキーワードを使って型プロパティを実装するケースでも関係なく、プロトコルでは常にstaticキーワードを使うということです。

プロパティ要求の具体例

プロパティ要求のサンプルコードを作ってみます。

protocol HasName {
    var name: String { get }
}

プロトコルHasNameは、gettableなプロパティnameを要求しています。つまり、実際にこのプロトコルを採用する型(クラスなど)は、gettableなインスタンスプロパティname(型はString)を持つ必要があります。

プロトコルHasNameを採用した構造体SomeAppを作ってみました。

// 実装側でsetter機能を付けても良い
struct SomeApp: HasName {
    var name: String
}

let foodTracker = SomeApp(name: "フードトラッカーアプリ")
print(foodTracker.name)
//"フードトラッカーアプリ"と表示

この例だと、実装側SomeAppが持つプロパティはsettableな機能も持っています。要求されている機能はgettableのみですから、settableかどうかは問われていません。

Gettableな機能のみを実装した、読み取り専用計算プロパティでも問題ありません。

// read-only computed propertyで実装
class AnotherApp: HasName {
    var name: String { return "別アプリ" }
}

let another = AnotherApp()
print(another.name)
//"別アプリ"と表示

このケースではクラスAnotherAppを作り、HasNameプロトコルを採用して、nameプロパティを読み取り専用計算プロパティとして実装しています。

メソッドの要求 | Method Requirements

メソッド要求もプロトコル要求の考え方と基本的には同じで、

メソッドのインターフェイス(名前や型)を提供する機能

です。ここでの型というのは、パラメータや返り値の型です。

メソッド要求の構文

メソッド要求の構文は

// メソッド要求(名前と型)
protocol SomeProtocol {    
    func someInstanceMethod() -> Double
    func anotherInstanceMethod(parameter: Int)
    static func typeMethod()
}

のように、

  • メソッドの名前
  • パラメータの名前と型
  • 返り値の型

を指定します。プロパティの場合と同様に、インスタンスメソッド型メソッドを定義することができます。型メソッドの場合はstaticキーワードを指定します。

プロパティ要求の場合と同様、実装側のクラスでclassキーワードを使って型メソッドを作る場合でも、プロトコルでは常にstaticキーワードで型メソッドを定義しておきます

サンプルコードを見ると分かると思いますが、プロトコルのメソッド要求が普通のメソッド定義と違う点は、メソッド本体(波括弧{}で囲まれた部分)が無いことです。また、パラメータにデフォルト値を設定することも出来ません。

メソッド要求の具体例 | 線形合同法による擬似乱数生成

公式マニュアルに載っている、線形合同法(linear congruential generator)の例が面白そうだったので、今回はこれをそのまま使います。

// 擬似乱数生成用プロトコル
protocol RandomNumberGenerator {
    func random() -> Double
}

プロトコルRandomNumberGeneratorは、メソッド要求random()を持っていて、パラメータは無し・返り値はDoubleです。(擬似)乱数生成を仮定したメソッドなので、返り値は0.0から1.0の範囲内という暗黙の制限が入っていますが、それはどこかに明記されている訳ではありません。

公式マニュアルにある線形合同法を使った擬似乱数生成クラスを作ってみると、

// 線形合同法
class LinearCongruentialGenerator: RandomNumberGenerator {
    var lastRandom = 42.0
    let m = 139968.0
    let a = 3877.0
    let c = 29573.0
    func random() -> Double {
        lastRandom = (lastRandom * a + c).truncatingRemainder(dividingBy: m)
        return lastRandom / m
    }
}

let generator = LinearCongruentialGenerator()

for _ in 0..<5 {
    print("Random number: \(generator.random())")
}
//Random number: 0.37464991998171
//Random number: 0.729023776863283
//Random number: 0.636466906721536
//Random number: 0.793481367169639
//Random number: 0.538544524462734

このようになります。最後にmで割っているのがポイントで、これにより0から1の区間の実数を返すメソッドが出来上がります。

線形と言っているのは、式

lastRandom = (lastRandom * a + c).truncatingRemainder(dividingBy: m)

lastRandomに対して1次(二乗とかが入ってない)だからです。また、計算した値を再びlastRandomに入れているので、次回同じ計算を実行した際に値が変わることが分かると思います。これが線形合同法における基本的な擬似乱数生成の仕組みです。

上記の式は一見複雑ですが、

lastRandom = (lastRandom * a + c) % m

と書くと分かりやすいかもしれません。Swift 3から浮動小数点数に対する剰余演算子が廃止になったので、truncatingRemainder(dividingBy:)メソッドを使わないといけません。

参考:線形合同法、Wikipedia

ただし、上記コードの場合、初期値(種、seedと言ったりします)が42.0で固定なので、(何度か実行すると分かりますが)出てくる数字は全く一緒になります。

Mutatingメソッドの要求 | Mutating Method Requirements

Mutatingメソッドは値型用

値型(構造体と列挙型)の場合、デフォルトでは、インスタンスメソッドからプロパティを変更出来ません。これを可能にするのがmutatingキーワードでしたが、プロトコルでもこの指定が可能です。

公式マニュアルに注意書きがありますが、mutatingキーワードは値型のための機能なので、クラスのメソッドには一切関係ありません。あくまでも構造体と列挙型でmutatingメソッドを使うための要求になります。

“NOTE

If you mark a protocol instance method requirement as mutating, you do not need to write the mutating keyword when writing an implementation of that method for a class. The mutating keyword is only used by structures and enumerations.”

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

先程乱数生成用にLinearCongruentialGeneratorクラスを作りましたが、試しにこれを構造体に変更すると、

//error: cannot assign to property: 'self' is immutable

と怒られます。これはメソッドrandom()内部でプロパティlastRandomを変更しているためです。このようなケースでは、プロトコルのメソッド要求にmutatingキーワードを付けないとコンパイルが通りません(実装先にもmutatingが必要)。

Mutatingメソッド要求の具体例 | トグルスイッチ

公式マニュアルにある列挙型を使ったスイッチの例は非常に分かりやすいです。以前mutatingメソッドの説明の時に出て来たtristate switchの2段バージョンです。

protocol Togglable {
    mutating func toggle()
}

enum OnOffSwitch: Togglable {
    case off, on
    mutating func toggle() {
        switch self {
        case .off:
            self = .on
            print("スイッチON")
        case .on:
            self = .off
            print("スイッチOFF")
        }
    }
}

var lightSwitch = OnOffSwitch.off
lightSwitch.toggle() //スイッチON
lightSwitch.toggle() //スイッチOFF
lightSwitch.toggle() //スイッチON

分かりやすいようにprint()関数を挟んでいます。Toggleというのは、電気をON/OFFするスイッチのように、同じ操作によって2つの状態が変わる機構一般を指します。「Togglable」という名前のプロトコルを作っていますが、togglableというのは造語で「トグル出来る」というような意味合いです。

今プロトコルTogglableが持つメソッドにはmutatingが入っています。したがって、このプロトコルを採用する値型では、toggle()メソッド内部でプロパティを変更することが可能です。注意点は実装先(ここではOnOffSwitch)でもmutatingキーワードが必要なことです。

列挙型OnOffSwitchtoggle()メソッド内部では、自分自身の状態に応じてselfプロパティを変更しています。したがって、toggle()メソッドにはmutating指定が必要で、mutatingキーワードをプロトコルと列挙型から外すとコンパイルエラーになります。

初期化子の要求 | Initializer Requirements

初期化子(initializers)もプロトコルで要求することが出来ます。定義構文は、

// 初期化子の要求
protocol SomeProtocol {
    init(parameter: Int)
}

このように初期化子のパラメータ名とその型を指定するだけです。初期化子はメソッドの特別形態ですから、要求の仕方もメソッドと同様だと思えば理解しやすいと思います。

初期化子要求をクラスに実装 | Class Implementations of Protocol Initializer Requirements

初期化子要求の実装にはrequiredキーワード

初期化子の要求をクラスに実装する場合、必ずrequiredキーワードをつけなければいけません。これはサブクラスでもプロトコルからの初期化子要求を満たすためです。

class SomeClass: SomeProtocol {
    required init(parameter: Int) {
        // プロトコル要求に従うdesignated initializerの実装
    }
}

class AnotherClass: SomeProtocol {
    init() {
        // designated initializerの実装
    }
    required convenience init(parameter: Int) {
        // プロトコル要求に従うconvenience initializerの実装
        self.init()
    }
}

初期化子要求をクラスで実装する場合、実装側の初期化子はdesignated initializerまたはconvenience initializerのどちらでも構いません(上記のサンプルコード参照)。requiredを付けることによって、派生先のサブクラスでも同じパラメータを持った初期化子を実装しないといけなくなるので、プロトコルの実装を派生先にも要求することが可能です。

試しにrequiredキーワードを外すと、

class SomeClass: SomeProtocol {
    init(parameter: Int) {
        // designated initializerの実装
    }
}
// コンパイルエラー
//error: initializer requirement 'init(parameter:)' can only be satisfied by a `required` initializer in non-final class 'SomeClass'

という感じで「requiredをつけなさい」と怒られます。

先程も述べましたが、requiredが無いと、サブクラスでプロトコルの要求に従わせるための強制力がありません。その場合、プロトコルを間接的には採用しているのに、サブクラスでは初期化子の実装が無いというケースがあり得ます。そのため、finalキーワードの無いクラスでは、プロトコルからの初期化子要求に対しては、requiredキーワードが必須です。

最初に強調しましたが、requiredキーワードが必要なのはクラスだけで、継承の無い構造体や列挙型では必要ありません

初期化子のオーバーライドと重なる場合はoverrideキーワードも必要

面白いのは、サブクラスがスーパークラスのdesignated initializerをオーバーライド(overriding)し、かつサブクラスが初期化子要求を持ったプロトコルを採用した場合で、

class SomeSuperClass {
    init(parameter: Int) {}
}

// クラスを継承して初期化子をオーバーライド
// 同時にプロトコルを採用、初期化子要求があるのでrequiredが必要になる
class SomeSubClass: SomeSuperClass, SomeProtocol {
    required override init(parameter: Int) {
        super.init(parameter: parameter)
    }
}

その場合は上記のサンプルコードのような感じになります。まずdesignated initializerをオーバーライドしているのでoverrideキーワードが必要で、さらにプロトコルを採用しているのでrequiredキーワードも付けないといけません。この時キーワードの順序は関係ないので、override requiredでも問題ありません。

Failable Initializerの要求

Failable initializerの要求も可能なようです。

protocol Failable {
    init?()
}

struct FailableStruct: Failable {
    init?() {
        // Failable initializerの実装
    }
}

struct NonFailableStruct: Failable {
    init() {
        // noo-failable initializerの実装
    }
}

プロトコル側での定義構文は、通常の初期化子の場合と同様です。実装側では、そのままfailable initializerとして実装しても構いませんが、nonfailable initializerとして実装することも出来ます。上記コードでは通常の初期化子を実装しましたが、init!(implicitly unwrapped failable initializer)で実装することも可能です。

まとめ

  • プロトコル(Protocols) = プロパティやメソッドの設計図(雛形)
  • 中身はクラス等で実装(protocol conformance、プロトコルの準拠)
  • プロトコルの定義はprotocolキーワード
  • プロパティ要求では、プロパティの名前、型、gettable/settableの指定
  • メソッド要求では、メソッドの名前、パラメータ(名前と型)、戻り値の型の指定
  • 初期化子init要求の基本はメソッドと同じ
  • クラスに初期化子要求を実装する場合はrequiredキーワード
  • オーバーライドを併用する時はrequired override(逆でも可)
  • Failable initializerの要求に対しては、failable/nonfailableどちらの初期化子を実装しても良い