Swiftのプロトコル | オプショナルプロトコル要求(Optional Protocol Requirements)

Swiftのプロトコル | オプショナルプロトコル要求(Optional Protocol Requirements)

オプショナルプロトコル要求(Optional Protocol Requirements)です。実装しなくても良いプロパティやメソッドを提供できる機能ですが、必ずObject-C attribute(@objc)と同時に定義する必要があります。

ここでは、オプショナルプロトコル要求とは何か?から始めて、その定義構文について簡単に説明した後、委譲デザインパターンを使った具体例でオプショナルプロトコル要求について説明します。

オプショナルプロトコル要求(Optional Protocol Requirements)って何?

実装しなくても良い要求

日本語として少しおかしな気もしますが、オプショナルプロトコル要求(Optional Protocol Requirements)とは、プロトコルで要求したプロパティ(properties)メソッド(methods)が必須では無くオプションになっている場合のことを指しています。

この後具体的に見ていきますが、これはObject-Cの名残りらしく、Swiftでは余り望ましくない仕様のようです。Swift 3ではそのようなニュアンスがより全面的に出されていて、オプションであることを示す識別子が

@objc optional

になりました(以前まではoptionalのみ)。ちゃんと理解しているわけではありませんが、Object-Cとの互換性を保つために残してある機能という位置づけなのかもしれません。

また、@objcを付けたプロトコルは、構造体(structures)列挙型(enumerations)では採用することが出来ません。公式マニュアルによると、

“Note that @objc protocols can be adopted only by classes that inherit from Objective-C classes or other @objc classes. ”

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

と書いてあり、Object-Cクラスまたは@objcの付いた既存クラスを継承しているクラスだけが採用可能、とあります。後で試してみますが、採用するだけなら通常のクラスでも可能です。

オプショナルプロトコル要求の定義構文

先程も少し述べましたが、オプショナルプロトコル要求の定義構文は

// protocolの前に@objc
@objc protocol ProtocolName {
    // オプショナルにする場合は「@objc optional」を付ける
    @objc optional func optionalMethod()
    @objc optional var optionalProperty: type { get set }
}

という感じになります。ポイントを箇条書きにすると、

  • protocolの前に@objcを付ける(@objc protocol
  • オプショナル要求にする場合、プロパティやメソッドの前に@objc optionalを付ける

です。

オプショナルな要求を定義するためには、プロトコル自体に@objcを付けておかないといけません。普通のプロトコルにオプショナル要求を定義しようとすると、コンパイルエラーになります。

// 普通のプロトコルにオプショナル要求は定義できない
protocol SomeProtocol {
    @objc optional var property: Double { get }
}
//error: @objc can only be used with members of classes, @objc protocols, and concrete extensions of classes

オプショナルプロトコル要求の具体例 | 委譲(delegation)デザインパターン

オプショナルプロトコル要求を使う場合、委譲デザインパターンと相性が良いようです。また、自然にオプショナルチェイン(optional chaining)が出てきます。

// オプショナルプロトコル(delegate用)
@objc protocol TravelDelegate {
    @objc optional func airplane() -> String
    @objc optional var train: String { get }
    var walk: String { get }
}

// 実装クラス
class WalkOnly: TravelDelegate {
    var walk: String { return "ひたすら歩く" }
}

class UseTrain: TravelDelegate {
    var train: String { return "基本は列車" }
    var walk: String { return "時々歩く" }
}

class TripToAbroad: TravelDelegate {
    func airplane() -> String { return "メインは飛行機" }
    var walk: String { return "現地では歩き" }
}

// 委譲用インスタンスdelegateを用意したクラス
class Traveler {
    var delegate: TravelDelegate?
    func talk() {
        if let airplane = delegate?.airplane?() {
            print(airplane)
        }
        if let train = delegate?.train {
            print(train)
        }
        if let walk = delegate?.walk {
            print(walk)
        }
    }
}

// delegateに様々なクラスをセットして挙動を確認
let traveler = Traveler()

traveler.delegate = WalkOnly()
traveler.talk()
//"ひたすら歩く"

traveler.delegate = UseTrain()
traveler.talk()
//"基本は列車"
//"時々歩く"

traveler.delegate = TripToAbroad()
traveler.talk()
//"メインは飛行機"
//"現地では歩き"

長いですが、基本的な構造は公式マニュアルに載っているサンプルコードと同じです。順番に見ていきます。

オプショナルプロトコル: TravelDelegate

// オプショナルプロトコル(delegate用)
@objc protocol TravelDelegate {
    @objc optional func airplane() -> String
    @objc optional var train: String { get }
    var walk: String { get }
}

オプショナル要求を定義したいので、プロトコルの前に@objcが付いています。このプロトコルは全部で3つ要求を持っていますが、

  • オプショナルメソッド: airplane()
  • オプショナルプロパティ: train
  • 通常のプロパティ: walk

このようになっています。@objc optionalの付いていないwalkだけは、クラス側で必ず実装しなければいけません。しかし、オプショナル要求の2つは必ずしも実装する必要はありません。

オプショナルプロトコルを採用したクラス: WalkOnly, UseTrain, TripToAbroad

今回用意したクラスは3つで、オプショナル要求を実装するかどうかで場合分けしてあります。他にも組み合わせはありますが、簡単なケースのみを用意しました。

// 実装クラス
class WalkOnly: TravelDelegate {
    var walk: String { return "ひたすら歩く" }
}

class UseTrain: TravelDelegate {
    var train: String { return "基本は列車" }
    var walk: String { return "時々歩く" }
}

class TripToAbroad: TravelDelegate {
    func airplane() -> String { return "メインは飛行機" }
    var walk: String { return "現地では歩き" }
}

先程も述べましたが、walkプロパティだけは必ず実装する必要があります。WalkOnlyクラスでは、その名前の通りwalkのみが実装してあります。次のUseTrainクラスでは、walkに加えてtrainプロパティを定義しています。最後のTripToAbroadクラスでは、airplane()メソッドを実装しています。

今回は作りませんでしたが、当然全てのプロトコル要求を実装したようなクラスを定義することも可能です。

オプショナルプロトコル要求はオプショナル型

オプショナルプロトコルで定義されているオプショナル要求(@objc optional付きのプロパティやメソッド)は、その全体がオプショナル型(optionals)になります。

これはプロトコル要求定義時の型が非オプショナル型(つまり通常の型、IntとかString等々)でも適用されます。つまり、オプショナルプロトコ要求を定義した場合、どのように型を決めても必ずオプショナル型になります。

「その全体」というのがややこしいですが、プロパティの場合は自明で、例えば型がIntの場合はInt?です。メソッドの場合はパラメータも含めた全体がオプショナルになるので、例えば今回の例airplane()の場合、() -> String(() -> String)?になります。

実際に試してみると分かりますが、

// クラスをTravelDelegateにキャスト
let useTrain: TravelDelegate = UseTrain()
print(useTrain.walk) //"時々歩く"
print(useTrain.train) //Optional("基本は列車")

let tripToAbroad: TravelDelegate = TripToAbroad()
tripToAbroad.airplane()
//コンパイルエラー
//error: value of optional type '(() -> String)?' not unwrapped; did you mean to use '!' or '?'?

通常のプロパティ要求walkStringですが、オプショナルプロパティ要求のtrainString?(オプショナル型のString)になっているのが分かります。また、最後のメソッド実行はエラーを起こしていますが、エラーメッセージから型が(() -> String)?であることが分かります。このメソッドから値を取り出す方法は次のセクションで説明します。

委譲を実装したクラス: Traveler

// 委譲用インスタンスdelegateを用意したクラス
class Traveler {
    var delegate: TravelDelegate?
    func talk() {
        if let airplane = delegate?.airplane?() {
            print(airplane)
        }
        if let train = delegate?.train {
            print(train)
        }
        if let walk = delegate?.walk {
            print(walk)
        }
    }
}

クラスTravelerはメソッドtalk()のみを持つようなクラスです。メソッドtalk()の中身を1つずつ見ていきます。

オプショナルプロパティ要求呼び出し

委譲先クラスTravelDelegateはオプショナルになっているので、

// 機能をTravelDelegateに委譲
var delegate: TravelDelegate?

実装してもしなくても良いという仕様です。

プロパティの呼び出しから見る方が分かりやすいですが、

// オプショナルチェイン
if let train = delegate?.train {
    print(train)
}
if let walk = delegate?.walk {
    print(walk)
}

これはオプショナルチェインになっています。したがって、もしユーザがvar delegateにクラスインスタンスを渡さなかった場合、delegatenilですから、上記のoptional binding全体がnilになってif文の中身は実行されません。

オプショナルメソッド要求: 名前とカッコの間に?を付ける

メソッドの場合もオプショナルチェインですが、少し特殊な呼び出し方になっています。

// メソッド名とカッコ()の間に?を付ける
if let airplane = delegate?.airplane?() {
    print(airplane)
}

オプショナルなメソッド要求(@objc optionalが付いたメソッド要求)を呼び出す場合、上記サンプルコードのようにメソッド名とカッコ()の間に?を挟みます。

// オプショナルなメソッド要求の呼び出し型
delegate?.airplane?()

// これではダメ
//delegate?.airplane()?

オプショナルチェインで連鎖する場合には、メソッドのカッコの後ろに?を付けていましたが、オプショナルプロトコル要求の場合そうではないので注意が必要です。

delegateにクラスインスタンスを代入

// delegateに様々なクラスをセットして挙動を確認
let traveler = Traveler()

traveler.delegate = WalkOnly()
traveler.talk()
//"ひたすら歩く"

traveler.delegate = UseTrain()
traveler.talk()
//"基本は列車"
//"時々歩く"

traveler.delegate = TripToAbroad()
traveler.talk()
//"メインは飛行機"
//"現地では歩き"

実際に使用する時は、delegate変数にTravelDelegateを採用しているクラスのインスタンスを代入します。上記サンプルコードでも分かるように、実行しているメソッドは常にtalk()ですが、delegateの中身が変わると実行結果が変わっているのが分かります。

まとめ

  • オプショナルプロトコル要求(Optional Protocol Requirements)は、採用クラスで実装しなくても良い(オプション的な)プロパティやメソッドのこと
  • オプショナルプロトコル要求には@objc optionalを付ける
  • オプショナルプロトコル要求を持つプロトコルには、protocolの前に必ず@objcを付ける必要がある
  • オプショナルプロトコル要求は、プロパティならその型がオプショナルに、メソッドの場合その全体(引数と戻り値を含めた全体)がオプショナルになる
  • オプショナルなメソッド要求を呼び出す際には、メソッドの名前とカッコの間に?を付ける