WWDCにて、C++/Boostで知られ、現在はAppleでSwift Standard Libraryグループのリーダを務めるDave Abrahams氏が、Swiftをプロトコル指向言語として紹介し、プロトコルがコード改善にどう使えるのか説明した。
プロトコル指向プログラミングというのは、OOP(オブジェクト指向プログラミング)のパラダイムの一つで(注:Abrahams氏はそうは言っていないとのこと)、クラスよりもプロトコル(インターフェイスに相当)と構造体の利用を好んでいる。
クラスは素晴らしい?
OOPで知られているように、クラスは以下を提供するのに使われる。
実のところ、これらはすべて型の特性であり、クラスは型を実装する一つの方法にすぎないとAbrahams氏は言う。だが、クラスはプログラマに多大な犠牲を強い、次のようなことが起こるおそれがある。
class Ordered { func precedes(other: Ordered) -> Bool { fatalError("implement me!") } } class Label : Ordered { var text: String = "" ... } class Number : Ordered { var value: Double = 0 override func precedes(other: Ordered) -> Bool { return value < (other as! Number).value } }
Abrahams氏によると、プロトコル指向プログラミングは、以下の点で、より優れた抽象化の仕組みだという。
プロトコル指向プログラミング
Swiftで新たに抽象化を考えるとき、最初のステップは常にプロトコルであるべきだとAbrahams氏は言う。そして、Ordered
クラスの例をプロトコルと構造体を使って書き直すことで、実装がいかにクリーンになるか説明した。
protocol Ordered { func precedes(other: Self) -> Bool } struct Number : Ordered { var value: Double = 0 func precedes(other: Number) -> Bool { return self.value < other.value } }
precedes
のプロトコル要件にSelf
を使うことで、Number
クラスのprecedes
メソッド実装は、適切なパラメータを正しく取得することができ、キャストが不要になる。
Self
要件は、それを含むプロトコルの使用に重要な意味を持っている。Ordered
のインスタンスの配列を引数にとるbinarySearch
メソッドを定義すると、次のようなコードになるだろう。
class Ordered { ... } func binarySearch(sortedKeys: [Ordered], forKey k: Ordered) -> Int { var lo = 0 var hi = sortedKeys.count while hi > lo { let mid = lo + (hi - lo) / 2 if sortedKeys[mid].precedes(k) { lo = mid + 1 } else { hi = mid } } return lo }
これに対し、Self
要件を含むプロトコルを使うと、ジェネリックなメソッドを定義する必要がある。
protocol Ordered { ... } func binarySearch(sortedKeys: [T], forKey k: T) -> Int { ... }
Self
要件を使うのと使わないのとの違いは、広範囲に及ぶ。とりわけ、Self
要件は私たちを静的ディスパッチの場に置き、ジェネリックスとホモジニアスなコレクションを要求する。違いは以下の図にまとめられている。
Retroactive modeling
Abrahams氏はプロトコルと構造体がクラス階層を置き換えるのにどう使えるか説明するために、幾何図形をレンダリングすることを目的としたRenderer playgroundを紹介した。これはプロトコルと構造体が提供するretroactive modelingの可能性を示すものだ。この例では、Retroactive modelingを適用して、Renderer
プロトコル要件を実装したCGContext
の拡張を作っている。
protocol Renderer { func moveTo(p: CGPoint) func lineTo(p: CGPoint) func arcAt(center: CGPoint, radius: CGFloat, startAngle: CGFloat, endAngle: CGFloat) } extension CGContext : Renderer { ... }
こうすることで、CGContext
型はRenderer
以前に定義されていたにもかかわらず、Renderer
型が使えるところで使えるようになる。
その一方で、幾何図形のテキスト表現を出力するTestRenderer
クラスにを通して、プロトコルの匿名実装を提供することができる。
struct TestRenderer : Renderer { func moveTo(p: CGPoint) { print("moveTo(\(p.x), \(p.y))") } func lineTo(p: CGPoint) { print("lineTo(\(p.x), \(p.y))") } ... }
これら2つのRenderer
実装は、入れ替えて使うことができる。
プロトコル拡張
プロトコルがもっと便利に使えるよう、Swift 2.0にはプロトコル拡張という新機能が導入された。この機能を使うことで、プロトコル要件のデフォルト実装を提供することが可能になる。これは以下のコードを使って説明された。
protocol Renderer { func moveTo(p: CGPoint) func lineTo(p: CGPoint) func circleAt(center: CGPoint, radius: CGFloat) func arcAt(center: CGPoint, radius: CGFloat, startAngle: CGFloat, endAngle: CGFloat) } extension Renderer { func circleAt(center: CGPoint, radius: CGFloat) { arcAt(center, radius: radius, startAngle: 0, endAngle: twoPi) } }
Renderer
のプロトコル拡張にcircleAt
を定義すると、その実装はCGContext
とTestRenderer
で共有される。
制約付き拡張
プロトコル拡張を使うことで、使われる型の制約を明示することができる。以下の例では、コレクションの要素が要件を満たすときだけ、CollectionType
のプロトコル拡張が定義される。
extension CollectionType where Generator.Element : Equatable { public func indexOf(element: Generator.Element) -> Index? { for i in self.indices { if self[i] == element { return i } } return nil } }
Generator.Element
をEquatable
と宣言することで、indexOf
内で==
オペレータが利用できるようになる。
最後に、ジェネリック関数定義をきれいにするなど、プロトコル拡張と制約で実現できるテクニックがいくつか紹介された。たとえば以下は、
func binarySearch< C : CollectionType where C.Index == RandomAccessIndexType, C.Generator.Element : Ordered >(sortedKeys: C, forKey k: C.Generator.Element) -> Int { ... }
次のように書くことができる。
extension CollectionType where Index == RandomAccessIndexType, 2 Generator.Element : Ordered { ... }
いつクラスを使うべきか?
Abrahams氏はプレゼンテーションの最後で、もしあなたが暗黙の共有を望むなら、クラスにもまだ居場所があると述べた。具体的には、以下のような場合だ。
さらにAbrahams氏はこう語る。Cocoaのような、オブジェクトとサブクラスの考え方で構築されたフレームワークを使う場合、システムに対して戦いを挑んでも仕方がない。だが、大きなクラスをリファクタリングするときには、プロトコルと構造体を使って取り除くと、もっと改善できるだろう。