Swiftのプロトコル
Objective−Cのプロトコルの概念は、Swiftに引き継がれています。本連載第4回で、「Swiftでは同値性やハッシュ値などのデフォルト実装を持ったルートクラスは存在せず、プログラマが定義したクラス、ストラクチャをプロトコルに準拠させることで、それらの機能を持った型を定義できる」と説明しました。
Swiftの標準APIにあるArray、Dictionary、Intなどのストラクチャの多くは様々なプロトコルに準拠して実装されています。プロトコルに対する理解を深めることは、Swiftの標準APIを理解するのために不可欠です。
プロトコルの宣言
プロトコルは次のような形式で宣言されます。
protocol プロトコル名 : プロトコル1, プロトコル2, ... {
プロトコルの内容
}
プロトコルは他のプロトコルを継承できます。クラスのように継承する対象は単一である必要はなく、複数のプロトコルを継承できます。
自分で定義した型をプロトコルに準拠させるためには、次のような形で宣言します。
// クラス
class クラス名 : プロトコル1, プロトコル2, ... {
クラスの実装
}
// ストラクチャ、列挙型
(struct / enum) 型名 : プロトコル1, プロトコル2, ... {
ストラクチャ、列挙型の実装
}
プロトコルとプロパティ
プロトコルは、準拠する型に対し、指定した名前と型を持つプロパティやタイププロパティを実装するように要求できます。実装を要求するプロパティは、保持プロパティであっても計算プロパティであっても構いません。また、取得のみを行えるか、取得および代入の両方を行えるかも要求できます。
protocol プロトコル名 {
var プロパティ名: 型 { get set / get }
class var タイププロパティ名: 型 { get set / get }
}
プロパティは必ずvarキーワードを付けて宣言します。また、プロパティ名と型の指定の後、値の取得と代入に関する要求を指定するために、{}の中にget setもしくはgetを記述します。
プロトコルを実装する例を見てみましょう。
protocol Drawable {
var visible: Bool { get }
var alpha: Float { get set }
class var visibleLimit: Float { get }
}
struct Component: Drawable {
static let visibleLimit: Float = 0.0
var alpha: Float
var visible: Bool {
return alpha > Component.visibleLimit
}
}
この例のComponentストラクチャは、Drawableプロトコルに準拠しています。そのため、Drawableプロトコルによって、次のことを要求されます。
- visibleは、Bool値の取得が可能なプロパティであること
- alphaは、Float値の取得および代入が可能なプロパティであること
- visibleLimitは、Float値の取得が可能なタイププロパティであること
Drawableプロトコルに準拠する型は、いずれもこれらのプロパティを欠いてはならず、また、型と取得および代入に関する要求も満たすことが求められます。
ただし、プロトコルのプロパティに対する取得、代入に対する要求は、準拠する型が最低限その機能を満たすことを要求するものです。そのため、getのみを指定されたプロパティだからといって、準拠する型のプロパティが代入不可能ということを示しません。そのため、値の取得のみ可能なvisibleLimitタイププロパティだからといって、定数であること(varではなくletキーワードを付けて宣言すること)をプロトコルは要求しません。
一方で、alphaプロパティをletを付けて宣言しようとすると、直ちにコンパイラによって、ComponentストラクチャがDrawableプロトコルに準拠していないとエラーが出されます。letでは定数としての宣言となり、値の代入ができないからです。
プロトコルとメソッド
プロトコルは準拠する型に対し、指定したメソッドやタイプメソッドを実装できます。
protocol プロトコル名 {
func メソッド名(引数のリスト) -> 返り値の型 (Voidの場合不要)
class func タイプメソッド名(引数のリスト) -> 返り値の型 (Voidの場合不要)
}
プロトコルに定義するメソッドの文法は、クラスやストラクチャ、列挙型のそれと同じです。1つだけ注意点として、プロトコルに宣言されたメソッドにはデフォルト引数を持たせることができません。
それでは例を見てみましょう。
protocol Writable {
func write(text: String)
class func placeholder() -> String
}
class TextBox: Writable {
private var inputCount: UInt = 0
private(set) var text: String = TextBox.placeholder()
func write(text: String) {
if inputCount > 0 {
self.text += text
} else {
self.text = text
}
inputCount++
}
class func placeholder() -> String {
return "Write your name!"
}
}
var textBox = TextBox()
textBox.text // "Write your name!"
textBox.write("aaa") // "aaa"
textBox.write("bbbb") // "aaabbbb"
この例はテキストボックスをモデル化したものです。Writableプロトコルに準拠するために、TextBoxクラスはwrite(text: String)メソッドとplaceholder()タイプメソッドを実装しています。textプロパティはファイル外から代入不可能とするために、private(set)を付けて宣言しています。
textプロパティには、writeメソッドでString値(文字列)が蓄積されていきます。デフォルト値はplaceholder()の返り値で、最初にwriteメソッドが呼び出されたときに、その引数(String値)がtextプロパティの値になります。2回目以降のwriteメソッド呼び出しでは、引数のString値がtextプロパティに追加されていきます。
mutatingキーワードも、プロトコルの宣言時にメソッドに追加できます。TextBoxが値型のストラクチャである場合、writeメソッドは、内部のプロパティを書き換える場合がありえます。このように値型に対して内部状態を書き換えるメソッドには、mutatingキーワードを付けておきましょう。
プロトコルでmutatingキーワードを付けられたメソッドは、値型のストラクチャや列挙型がそのプロトコルに準拠する場合、mutating キーワードを付けて型に宣言します。一方、そのプロトコルに準拠するクラスに対しては、キーワードを外して宣言します。
プロトコルとイニシャライザ
プロトコルは準拠する型に対して、引数の取り方を指定したイニシャライザを持つように要求することもできます。
protocol プロトコル名 {
init(引数のリスト)
}
プロトコルにイニシャライザを宣言し、そのプロトコルに準拠した型を定義する際には、プロトコルのイニシャライザは指定イニシャライザ、コンビニエンスイニシャライザのどちらを選んで実装しても構いません。どちらの場合もイニシャライザにはrequiredキーワードを付けます。
次の例は、チェックボックスとチェック機能付きセルの2つをモデル化したものです。どちらもチェックする機能を実現するために、マークできることを示すMarkableプロトコルに準拠しています。Markableプロトコルは、markedプロパティとinit(marked: Bool)イニシャライザを準拠型に求めます。
protocol Markable {
var marked: Bool { get }
init(marked: Bool)
}
class CheckBox : Markable {
private(set) var marked: Bool
required init(marked: Bool) { // 指定イニシャライザ
self.marked = marked
}
}
class CheckCell : Markable {
private(set) var marked: Bool
private(set) var name: String
init(name: String) {
self.name = name
self.marked = true
}
required convenience init(marked: Bool) { // コンビニエンスイニシャライザ
self.init(name: "Default name")
self.marked = marked
}
}
スーパークラスのプロトコルに宣言された指定イニシャライザをオーバーライドする場合には、イニシャライザの頭にreqired overrideキーワードを付けます。
一方、requiredキーワード付きでスーパークラスで宣言されていて、プロトコルのものではない指定イニシャライザをオーバーライドする場合には、overrideキーワードを付ける必要はありません。
プロトコルとsubscript
プロトコルを実装する型に与えたいsubscriptも宣言することができます。
protocol プロトコル名 {
subscript(引数のリスト) -> 返り値 { get / get set }
}
次の例に示したStringAccessibleプロトコルは、subscriptを使って文字列でアクセスできる機能があることを表しています。
protocol StringAccessible {
subscript(key: String) -> Any { get }
}
class ShopRepository: StringAccessible {
private var shops: [String: String]
init(shops: [String: String]) {
self.shops = shops
}
subscript(key: String) -> Any {
return shops[key]
}
}
ShopRepositoryクラスはStringAccessibleプロトコルに準拠しているため、subscriptと文字列を併用することで、初期化時に代入されたお店の名前(shops)を取得できるようになっています。
