Codable Tech Blog

iPhoneアプリケーション開発と AWS(Amazon Web Service)活用に関する記事を配信

プロトコル

プロトコルはメソッドとプロパティの雛形で、実装はもちません。プロトコルは、単純にプロトコルとして定義されているメソッドやプロパティを実装することを要求するだけのものです。プロトコルとして定義されているメソッドやプロパティを実装することを「プロトコルに適合する」と呼びます。プロトコルに適合することで、本来であれば関連性のない型Aと型Bが同じプロトコルに適合した同一の型Cとして扱うことが出来るようになります。以降では、プロトコルの定義、適合、利用方法についてみていきます。

プロトコル

プロトコルの定義

プロトコルはprotocolキーワードを利用して定義することができます。そして、プロトコル定義の中に実装を要求したいプロパティ、メソッドを記述します。

protocol プロトコル名 {
    //プロトコル定義
}

プロトコルへの適合

構造体やクラスがプロトコルで要求されているメソッドやプロパティを実装することを、「プロトコルに適合する」といいます。構造体をプロトコルに対して適合させる構文は次のようになります。なお、適合させたいプロトコルが複数ある場合は、カンマ区切りで複数のプロトコルを記述します。

struct 構造体名: プロトコル名1, プロトコル名2 {
    // 構造体定義
}

クラスをプロトコルに対して適合させる構文は次のようになります。継承を行う場合は、継承したいクラス名を記述した後に適合させたいプロトコルを記述します。

class クラス名: スーパークラス名, プロトコル名1, プトロコル名2 {
    // クラス定義
}

プロパティの実装要求

プロトコルは特定の名前と型をもつインスタンスプロパティやタイププロパティをクラスや構造体に実装するよう、要求することができます。プロトコルではプロパティの実装に対して、ゲッターのみかゲッターとセッターの両方かを指定できます。ゲッターはgetキーワード、ゲッターとセッターはget,setキーワードをつけます。また、プロパティでプロパティの実装を要求する場合、常にvarをつけてプロパティを宣言します。次の例はプロパティの実装を要求するプロトコルの例です。

PropProtocolではsetAndGetValとgetOnlyValの2つのプロパティを実装することを要求しています。setAndGetValはInt型のプロパティでgetとsetキーワードが付いているため、ゲッターとセッターを実装することを要求しています。getOnlyValはInt型のプロパティでgetキーワードしかついてないので、実装時にはゲッターを実装することを要求しています。
このPropProtocolに適合した構造体とクラスは以下のようになります。

TestStructはPropProtocolに適合した構造体です。構造体の場合、プロパティの定義時にget、setキーワードを利用した実装をすることはできないため、直接、プロパティの値を参照したり、更新したりします。つまり、構造体においてはプロトコルのゲッター、セッター要求は意味を成しません。
TestClassはPropProtocolに適合したクラスです。クラスの場合、プロトコルの要求に応じてゲッター、セッターを提供する必要があります。setAndGetValプロパティはゲッターとセッターが要求されているプロパティなので、ゲッターとセッターの両方を実装しています。getOnlyValプロパティはゲッターのみが要求されているプロパティなので、ゲッターのみが実装されています。

プロトコルを実装した構造体・クラスのインスタンス化

プロトコルを実装した構造体・クラスは、通常通り、イニシャライザを利用してインスタンス化することができます。

プロトコルに適合することのメリットは、一見、まったく関連性のないTestStruct構造体のインスタンスとTestClassクラスのインスタンスを同じPropProtocol型のインスタンスとして扱えることです。次の例では、TestStruct構造体のインスタンスとTestClassクラスのインスタンスをPropProtocol型を扱う配列に格納しています。

このように、同一の型として扱ってあげることで配列の中でこの2つのインスタンスを同時に扱うことができるようになります。その結果、for-in文を利用して、それぞれのインスタンスに対して同じ処理を実行させることが出来ているのです。このように、関連性のない型同士を同じ型として扱えるようにできることがプロトコルを利用するメリットになります。

タイププロパティの実装要求

タイププロパティを要求する場合、classキーワードをつけてプロパティを定義します。構造体、列挙体にタイププロパティを要求する場合でも、これは変わりません。次の例ではTypePropProtocolプロトコルを定義しています。このプロトコルはclassキーワードをプロパティに付けていますのでタイププロパティの実装を要求するプロトコルになります。

このTypePropProtocolプロトコルを実装した構造体、クラスの例は以下のようになります。

TestTypeStruct構造体ではtypeValプロパティを実装するためにstaticキーワードを付けています。構造体の場合はstaticキーワードをつけることでタイププロパティを定義できるのでした。また、TestTypeClassではclassキーワードを付けてtypeValプロパティを実装しています。typeValはゲッターのみを要求するタイププロパティなので、TestTypeClassはゲッターのみを実装したtypeValプロパティを実装しています。

メソッドの実装要求

プロトコルではインスタンスメソッドとタイプメソッドの実装を要求することもできます。プロトコルでメソッドを定義する時、括弧{}をつけない形でメソッドを定義します。また、可変パラメータをもつメソッドも提供可能ですが、デフォルト値はもつことはできません。タイプメソッドを定義するときはclassキーワードを前につけます。
次の例では、メソッドの実装を要求するmethodProtocolプロトコルを定義した例です。

methodProtocolでは次の3つのメソッドを実装することを要求しています。

  1. パラメータと戻り値を持たないインスタンスメソッドtestMethod1の実装
  2. Int型の可変パラメータをもち、Int型の戻り値をもつインスタンスメソッドtestMethod2の実装
  3. パラメータと戻り値を持たないタイプメソッドtestTypeMethodの実装

このmethodProtocolプロトコルに適合した構造体とクラスは次のようになります。

testStruct、testClassはともにmethodProtocolプロトコルに適合しています。注意してほしいのは構造体でタイプメソッドを定義する時はstaticキーワードを付けることです。それぞれの関数の処理をどのように実装するかは任意ですので、好きなように実装してかまいません。この例ではtestMethod1ではコンソールに「test」という文字を出力しています。testMethod2では渡されたInt型の値の合計値を求めて戻り値として返しています。testTyepMethodはtestMethod1同様に「test」という文字を出力しています。

ミュータブルなメソッドの実装要求

構造体、列挙体ではプロパティの値を変更したい時、mutatingキーワードを付けた関数を提供していました。同様に、プロトコルで構造体や列挙体にプロパティの値を変更するメソッドの実装を要求したい場合、プロトコルにmutatingキーワードをつけたメソッドを定義します。以下に示すEditableプロトコルではmutatingキーワードを付けたtoggleメソッドを定義しています。

このEditableプロトコルを実装した構造体が次のEditor構造体です。

Editor構造体はeditableプロパティをもっています。toggleメソッドはmutatingキーワードが付いたメソッドですので、構造体が持つプロパティeditableの値を変更することができます。toggleメソッドではeditableの値がtrueであればfalseに、falseであればtrueに反転する処理を実装しています。

イニシャライザの実装要求

プロトコルを利用して、イニシャライザの実装を要求することも可能です。イニシャライザを要求するプロトコルを定義する構文は次のようになります。

protocol プロトコル名 {
    init(パラメータ名:型,...)
}

プロトコルで要求されたイニシャライザの実装

イニシャライザを要求するプロトコルに適合したクラスを実装する場合、イニシャライザにrequiredキーワードをつける必要があります。このため、イニシャライザを要求するプロトコルに適合したクラスを実装する場合、次のようになります。

initProtocolはイニシャライザの実装を要求するプロトコルです。このイニシャライザはint型のパラメータを一つ受け取ります。initProtocolを実装したクラスがinitClassです。このクラスではイニシャライザの実装を要求するプロトコルを実装していますので、initメソッドにrequiredキーワードをつけてイニシャライザを定義しています。
requiredキーワードがついたイニシャライザをもつクラスを継承する場合、継承したクラスでは同じパラメータをもつイニシャライザを実装してあげる必要があります。次の例はinitClassを継承したinitSubClassの実装です。

initSubClassはrequiredキーワードの付いたクラスであるinitClassを継承していますので、int型のパラメータをもつイニシャライザを実装しています。
補足事項として、finalキーワードが付いたクラスがサブクラスが作成されないことを保証されています。このため、finalキーワードが付いたクラスがイニシャライザを要求するプロトコルに適合しても、required識別子は必要ありません。

プロトコルの継承

プロトコルは別のプロトコルは継承することが可能です。別のプロトコルを継承するプロトコルを定義する構文は次のようになります。なお、複数のプロトロコルを継承する場合はカンマで区切ります。

protocol プロトコル名 : 継承したいプロトコル名1,継承したいプロトコル名2 {
    //プロトコル定義
}

次の例ではBaseProtocolを定義し、さらにこのBaseProtocolを継承したSubProtorocolを定義しています。

この場合、SubProtocolプロトコルはbaseMethodメソッドとsubMethodメソッドの2つのメソッドの実装を要求するプロトコルということになります。このため、このSubProtocolプロトコルを実装するクラスではbaseMethodメソッドとsubMethodメソッドの2つを実装しなければなります。
以下のTestClassクラスはSubProtocolプロトコルを実装したクラスです。

TestClassクラスはSubProtocolプロトコルが実装を要求しているbaseMethodメソッドとsubMethodメソッドを実装しています。このように、プロトコルを実装側は継承したプロトコルと継承元のプロトコルで要求されているメソッドやプロパティを全て実装する必要があります。

クラス限定のプロトコル

プロトコルは特に何も指定しなければ、構造体、列挙体、クラスのいずれでも利用することが出来ます。もし、自分が定義するプロトコルをクラスだけが利用できるように用途を限定したければ、classキーワードを利用します。

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

次のClassOnlyProtocolプロトコルはクラスだけが利用できるプロトコルです。

このClassOnlyProtocolプロトコルはクラスが実装する場合はこれまでのプロトコルと同様に扱うことができます。しかし、構造体や列挙体でこのプロトコルを実装しようとするとコンパイルエラーが発生します。下記の例でいれば、ClassOnlyクラスは問題ありませんが、TestStructは構造体であるため、コンパイルエラーが発生します。

プロトコルコンポジション

複数のプロトコルを実装した型を要求したい場合、プロトコルコンポジションを利用するとよいでしょう。プロトコルコンポジションは複数のプロトコルの要求を満たした型を定義するものです。プロトコルコンポジションを利用して型を定義する構文は次のようになります。

protocol<プロトコル名1, プロトコル名2>

下記はCountryプロトコルとTelプロトコルを定義し、そのプロトコルを実装したPersonInfoクラスを定義しています。Countryプロトコルは自分が居住している国の情報、Telプロトコルは電話番号を出力してほしい目的でそれぞれ用意したプロトコルです。

PersonInfoクラスではそれぞれのメソッドで自分が居住している国と電話番号を出力しています。
さて、次にCountryプロトコルとTelプロトコルを実装した型をパラメータとして持つ関数printInfoを定義します。この場合、パラメータの型は「protocol」として定義できます。

パラメータinfoには、CountryプロトコルとTelプロトコルを実装した型の値が代入されますので、関数内でprintCountryメソッドとprintTelメソッドを呼び出すことが出来ます。

プロトコル適合チェック

変数や定数があるプロトコルに適合しているかどうかを確認することが出来ます。適合しているかどうかをチェックするのに利用するのがis演算子です。is演算子はどの型に属しているかを調べることが出来る演算子です。次のような構文を利用します。

変数名 or 型名

例えば、変数testがクラスABCに属しているかどうか調べたければ「test is ABC」という形で記述します。
次の例ではContentクラスのインスタンスを作成し、そのインスタンスがMovieプロトコルに適合しているかどうかをチェックしています。

しかし、このプログラムは17行目の箇所でコンパイルエラーが発生してしまいます。
変数や定数があるプロトコルに適合しているかどうかをチェックするためには、プロトコル定義において「@objc」属性が付けられている必要があります。

@objc protocol プロトコル名 {
    //プロトコル定義
}

@objc属性はプロトコルがobjcと相互運用できるプロトコルであることを示すものです。ですが、プロトコルをSwiftとObjective-Cで相互運用するかどうかにかかわらず、適合確認のためには@objc属性が必要となります。また、@objc属性がついたプロトコルはクラスタイプにだけ適用可能な属性となります。
次の例では先ほどMovieプロトコルに@objc属性をつけて書き直しています。

@objec属性をMovieプロトコルにつけた結果、コンパイルエラーが解消され、プログラムが動作します。content定数はMovieプロトコルを実装したクラスのインスタンスですので、コンソールに「要素はMovie型を実装しています」というメッセージが表示されます。

実装のオプション指定

今まで見てきたプロトコルでは、プロトコルに定義されているメソッドやプロパティは必ず実装しなければなりませんでした。プロトコルでは実装のオプション指定、つまり、実装しなくてもよいものを指定することができます。プロトコルの宣言の中でoptionalキーワードを利用することによって、オプション指定を行うことができます。ただし、オプション指定を行うプロトコルを宣言したい場合は必ずプロトコルを宣言する時に@objec属性をつける必要があります。このため、オプション指定を行うプロトコルの構文は次のようになります。

@objc protocol プロトコル名 {
    optional func メソッド
    optional var プロパティ
}

次のUserInfoプロトコルではメソッド、プロパティをそれぞれオプション指定しています。

さらに、次のUserクラスではUserInfoプロトコルを実装しています。

UserInfoプロトコルのプロパティ、メソッドはすべてオプション指定されています。このため、UserクラスはUserInfoプロトコルで定義されているメソッドやプロパティを実装しなくてもコンパイルエラーになりません。
オプション指定されたメソッドを呼び出す場合は、そのメソッドを呼び出す前にメソッドが存在しているかどうかをチェックしないといけません。存在しているかどうかをチェックするには「オプショナルチェイン」を利用します。オプショナルチェインはメソッドを呼び出す時、メソッド名の後に?をつけることで、そのメソッドが存在するかどうかをチェックしてくれます。もし、メソッドが存在しない場合、オプショナルチェインはnilを返してくれます。下記の例ではgetInfoメソッドをオプショナルチェインを利用して呼び出しています。

UserクラスではgetInfoメソッドが実装されていません。このため、user.getInfo?()の呼び出しはnilが返却されます。