Swift におけるオプショナルなメソッドについて真面目に考える

この記事は RECRUIT MARKETING PARTNERS Advent Calendar 2015 の投稿記事です。

どーも、英単語サプリ iOS 担当の平井です。

久しぶりに iOS アプリ開発に携わり、慣れ親しんだ Objective-C からようやく離れ、遅咲きながら Swift と戯れる日々を過ごしています。Swift に触れてまず驚かされたのは、Optional や Generics などがもたらす豊かな表現力です。Objective-C ではドキュメントなしには伝えられなかったことを、Swift ではそのほとんどをソースコードだけで表現できるようになりました。

iOS アプリ開発という観点では、開発言語がより現代的・一般的な仕様となることで、これまで iOS アプリを開発したことがない・敬遠していた人にとっては、より受け入れやすいものになったと思います。

しかし、iOS アプリの開発現場では、iOS SDK やサードパーティ製のライブラリで見られるようにまだまだたくさんの Objective-C 時代の名残があります。その1つが オプショナルなメソッド です。

Objective-C におけるオプショナルなメソッド

Objective-C におけるプロトコルを説明する際によく引き合いに出されるのが Java などのインターフェース(Interface)です。違いは、 Java などのインターフェースは実装(implements)するものであるのに対して、プロトコルは 採用(adopt)するものであることです。

具体的にいうと、プロトコルでは実装が必須であるメソッドと必須でないメソッドを定義できます
したがって、Objective-C では、プロトコルを採用しているからといって 必ず is-a 関係になるわけではないため、動的にメソッドの実装の有無を判定する必要性が生じてしまいます。

慣れ親しんだ Objective-C の実装例を見てみましょう。

@protocol AProtocol <NSObject>
@required
- (void)requiredMethod;
@optional
- (void)optionalMethod;
@end

AProtocol は以下のように説明できます。

  • AProtocol を採用する場合は requiredMethod を実装しなければならない
  • AProtocol を採用する場合は optionalMethod を実装してもしなくてもよい

AProtocol を採用したクラスのインスタンスを利用する例を以下に示します。

@property (nonatomic) id<AProtocol> aInstance;
- (void)doSomething {
    [self.aInstance requiredMethod];
    if ([self.aInstance respondsToSelector:@selector(optionalMethod)]) {
        [self.aInstance optionalMethod]
    }
}    

実装が必須である requiredMethod は必ず実装されている(はずw)なので、そのまま呼び出せます。実装が必須でない optionalMethod は、実装しているかどうかを動的に判別してから実行しなければなりません。

この概念は、iOS SDK で多用される Delegate パターンを理解する上で非常に重要です。
例えば、UITableViewDataSource を見てみると、 numberOfSectionsInTableView のように実装が必須でないメソッド、すなわち オプショナルなメソッド が数多く存在しています。

はず、と書いたのは 仮に aInstance を定義するクラスで requiredMethod が実装されていない場合でも Xcode 上では警告が出るだけで、それを無視して実行すると結局実行時エラーになるからです。

Swift におけるオプショナルなメソッド

Swift ではどのようにしてオプショナルなメソッドを定義するのでしょうか。

結論から言うと、純粋な Swift ( Pure Swift ) にはオプショナルなメソッドは存在しません。しかし、前述したように iOS SDK には未だ数多くのオプショナルなメソッドが存在しています。そこで登場するのが @objc 修飾子 です。

@objc 修飾子を利用する

Swift では @objc 修飾子をプロトコルに記述すると、宣言に optional を付与することができ、Objective-C と同様にオプショナルなメソッドを定義できます。Objective-C のソースコードを @objc 修飾子を利用して Swift で書き直したコードを見てみましょう。

@objc protocol AProtocol {
    func requiredMethod()
    optional func optionalMethod()
}

@objc を利用した AProtocol を採用したクラスを利用する例を以下に示します。

let aInstance: AProtocol
func doSomething() {
    aInstance.requiredMethod()
    aInstance.optionalMethod?()
}

@objc を利用して定義されたプロトコルのオプショナルなメソッドを呼び出す場合は、 aInstance.optionalMethod?() のように ? をつけます。

@objc をつけることの問題点

プロトコルを定義する際に @objc 修飾子を闇雲につければいいわけではありません。そもそも @objc 修飾子つけるということは、そのプロトコルが Objective-C からも利用できるということを意味します。
言い換えると、@objc 修飾子つけると Objective-C のプログラムで利用できない構成要素を含むことはできないということになります。

例えば、@objc 修飾子をつけて定義されたプロトコルは、クラスのみでしか採用できず構造体や列挙型では利用できません。また、 Generics も利用できません。

このように、@objc を付与することで Swift で得られるはずだった様々な恩恵を受けることができなくなるのです。

Pure Swift でのオプショナルなメソッド

オプショナルなメソッドを実現するために Swift の素晴らしさを犠牲にしたくはありません。Pure Swift で実現すべきです。Pure Swift でオプショナルを実現する方法の1つとして、Optional Protocol Methods in Pure Swift で非常に興味深いことが述べられています。

An optional method is another way of saying "a method with default behavior." A callback protocol method defaults to doing nothing, while a provider method has a default behavior or value. Required methods on the other hand, do not have default behavior: consider what the 'default' should be for trying to archive an object that doesn't conform to NSCoding? (Hint: it rhymes with rash.)

要するに、

  • 実装が必須でないメソッドはデフォルトの振る舞いが決まっている
  • 実装が必須であるメソッドはデフォルトの振る舞いが決まっていない

と言い換えることができるということです。

この表現を実現するためには、Swift 2.0 から登場した プロトコル拡張 を利用します。

protocol AProtocol {
    func requiredMethod()
    func optionalMethod()
}
extension AProtocol {
    func optionalMethod() {
        // Default behavior
    }
}

プロトコル拡張を用いて、optionalMethod のデフォルトの振る舞いを定義しました。プロトコル拡張を利用した AProtocol を採用したクラスを利用する例を以下に示します。

let aInstance: AProtocol
func doSomething() {
    let aInstance: AProtocol = AClass()
    aInstance.requiredMethod()
    aInstance.optionalMethod()
}

@objc 修飾子を利用したときとの違いは aInstance.optionalMethod()? がつかないことです。この方法の重要な点は Pure Swift であることです。Pure Swift であるということは構造体や列挙型でも採用することができ、Generics を利用することもできます

本記事では言及しませんが、aInstance.optionalMethod()? があるかないかというのも、実は非常に重要なことです。興味がある方は Optional Protocol Methods in Pure Swift を見てみてください。

まとめ

前述した通りオプショナルなメソッドは、iOS SDK で多用される Delegate パターンを実現する上で非常に重要な概念になります。私たちはこの概念を踏まえつつ、Swift の素晴らしさを最大限に活かすことを求められていると思います。そして、その上で @objc 修飾子やプロトコル拡張などの使いどころを見極めていく必要があります。

@objc 修飾子は Swift 本来の良さを最大限に活かすことができなくなります。一方、プロトコル拡張は使いどころを誤ると、思わぬバグを仕込む可能性を孕んでいます。

重要なことは「便利だから」や「なんとなく」ではなく、これらの特性を理解した上でどういった意図でプログラムを書くのかを意識することだと思います。

本記事が、これから iOS アプリを開発する方や、私のように Objective-C の呪縛から中々解放されない方に少しでも役に立てれば幸いです。