# プロトコルとは
Swiftのプロトコルは、型のインターフェースを定義するものです。インターフェースを定義し、抽象化することによって、複数の型で共通の機能を実装することができます。プロトコルはクラス、構造体、列挙型などで使われ、プロトコルのインターフェースを満たす型は、プロトコルに準拠している と言われています。
例えば、次のプロトコルは message()
というインターフェースを定義しています。このプロトコルに準拠するためには、message()
を実装しなければなりません。
protocol MessageProtocol {
func message() -> String
}
struct APIMessage: MessageProtocol {
func message() -> String {
return "APIMessage"
}
}
struct DBMessage: MessageProtocol {
func message() -> String {
return "DBMessage"
}
}
上記の二つの型は、MessageProtocol
プロトコルを準拠していると言えます。インターフェースとして定義することによって、複数の型で共通の機能を持つという同値性を保証することができるのです。
message()
を実装しないとコンパイラは、整合性がないとしてエラーを出力します。
protocol MessageProtocol {
func message() -> String
}
struct DBMessage: MessageProtocol {}
/* 実行結果 */
// error: type 'DBMessage' does not conform to protocol 'Message'
// struct DBMessage: Message {
// note: protocol requires function 'message()' with type '() -> String'; do you want to add a stub?
// func message() -> String
このような検知により、Swiftは型の安全性を提供しています。
# プロトコルの定義方法
プロトコルは、protocol
キーワードで定義することができます。
protocol プロトコル名 {}
次の例では、ErrorProtocol
プロトコルを定義しています。このプロトコルはインターフェースに printError
を要求します。
protocol ErrorProtocal {
func printError()
}
# 構造体
構造体を、プロトコルに準拠させるためには、型名のあとに 型名: プロトコル
のフォーマットでプロトコルを記述します。
protocol ErrorProtocol {
func printError()
}
// プロトコルを準拠する
struct APIError: ErrorProtocol {
func printError() {
print("error!")
}
}
複数のプロトコルを準拠するには、カンマ区切りで書くことができます。
protocol ErrorProtocol {
func printError()
}
protocol MessageProtocol {
func message()
}
// カンマ区切りでプロトコルを記述する。
struct APIError: ErrorProtocol, MessageProtocol {
func printError() {
print("error!")
}
func message() {
print("message!")
}
}
# クラス
クラスでプロトコルを使用するには、構造体と同様に、型名: プロトコル
のフォーマットでプロトコルを記述します。
protocol ErrorProtocol {
func printError()
}
// プロトコルを準拠する
class APIError: ErrorProtocol {
func printError() {
print("error!")
}
}
継承 をする場合は、スーパークラスを先に書いて、そのあとにプロトコルを記述します。
protocol ErrorProtocol {
func printError()
}
class ErrorSuperClass {}
// スーパークラスを先に書く
class APIError: ErrorSuperClass, ErrorProtocol {
func printError() {
print("error!")
}
}
# エクステンションで準拠する
Swiftでは、エクステンションでプロトコルを準拠させることもできます。
次のフォーマットで、プロトコルを準拠させることができます。
extension 型: プロトコル {}
次の例では、構造体にエクステンションを使ってプロトコルを準拠させています。
protocol ErrorProtocol {
func printError()
}
struct APIError {}
// エクステンションでプロトコルを準拠させる
extension APIError: ErrorProtocol {
func printError() {
print("error!")
}
}
カンマ区切りで複数のプロトコルを記述すると、どのプロパティやメソッドがどのプロトコルに準拠させているか分かりづらいという点があります。これに対して、エクステンションを使うと、個別に一つずつ準拠できるので可読性が上がるというメリットがあります。
次の例では、2つのプロトコルをカンマ区切りで準拠させています。
protocol ErrorProtocol {
func printError()
}
protocol MessageProtocol {
func message()
}
struct APIError: ErrorProtocol, MessageProtocol {
func printError() {
print("error!")
}
func message() {
print("message!")
}
}
現状は、2つしかないのでどのメソッドがどのプロトコルに属しているか判断できますが、これが増えていくとメンテナンスのコストが高くなっていきます。
一方、エクステンションでの準拠を見てみましょう。
次の例では、2つのプロトコルをそれぞれエクステンションを使って準拠させています。
protocol ErrorProtocol {
func printError()
}
protocol MessageProtocol {
func message()
}
struct APIError {}
// ErrorProtocolを準拠
extension APIError: ErrorProtocol {
func printError() {
print("error!")
}
}
// MessageProtocolを準拠
extension APIError: MessageProtocol {
func message() {
print("message!")
}
}
カンマ区切りで定義するよりも明確にプロトコルが区別されているのが分かります。
# 引数の型
プロトコルは型なので、引数の型としても使用することができます。
次の例では、関数の引数の型としてプロトコルを使用しています。
protocol NameProtocol {
var name: String { get }
}
// NameProtocolを準拠
struct Person: NameProtocol {
var name: String
}
// NameProtocolの型で引数を定義
func printPersonName(person: NameProtocol) {
print(person.name)
}
let person1 = Person(person: "Taro")
// PersonはNameProtocolを準拠しているので、引数として渡すことができる
printPersonName(person: person1) // Taro
# プロトコルコンポジション | Protocol Composition
プロトコルコンポジションを使うことで、複数のプロトコルを組み合わせて準拠させることができます。
&
を記述して、複数のプロトコルを組み合わせます。
protocol NameProtocol {
var name: String { get }
}
protocol AgeProtocol {
var age: Int { get }
}
struct Person: NameProtocol, AgeProtocol {
var name: String
var age: Int
}
// & でプロトコルを組み合わせる
func printPersonNameAndAge(person: NameProtocol & AgeProtocol) {
print(person.name)
print(person.age)
}
let person1 = Person(name: "Taro", age: 20)
printPersonNameAndAge(person: person1)
// Taro
// 20
NameProtocol
と AgeProtocol
を引数の型として定義し、この2つを準拠させる型のみを許容しています。
# プロパティ
プロトコルには、インターフェースとしてプロパティを定義することができます。
プロパティを定義するには、ゲッタとセッタの有無を定義します。
protocol プロトコル名 {
var プロパティ名: 型 { get set }
}
次の例では、NameProtocol
でゲッタを定義し、AgeProtocol
でゲッタとセッタを定義しています。
protocol NameProtocol {
// ゲッタのみを定義
var name: String { get }
}
protocol AgeProtocol {
// ゲッタとセッタを定義
var age: Int { get set }
}
struct Person: NameProtocol, AgeProtocol {
// NameProtocolのプロパティを準拠
var name: String
// AgeProtocolのプロパティを準拠
var age: Int {
get {
return 10
}
set {}
}
}
# 関連型 | Associated Type
関連型を使用すると、1つの型に依存しない抽象的なプロトコルを定義することができます。通常はプロトコルの定義時に型を定義しますが、関連型を使用すると、具体的な型はプロトコルを準拠させるタイミングで指定させることができます。
関連型の定義方法は、associatedtype
キーワードを使います。
protocol プロトコル {
associatedtype 関連型
var プロパティ名: 関連型
func メソッド名(引数: 関連型)
}
次の例では、associatedtype
キーワードで関連型を定義しています。プロトコルを準拠させる構造体の方では、typealias
キーワードを使って、具体的な型を指定しています。
protocol ItemProtocol {
// associatedtype キーワードで関連型を定義して抽象化させる
// この時点では、具体的な型は決まっていない
associatedtype Item
var items: [Item] { get }
}
struct ProductItem: ItemProtocol {
// String型で具体的な型を指定する
typealias Item = String
var items = ["item1", "item2"]
}
struct CartItem: ItemProtocol {
// Int型で具体的な型を指定する
typealias Item = Int
var items = [1,2,3]
}
let productItem = ProductItem()
productItem.items // ["item1", "item2"]
let cartItem = CartItem()
cartItem.items // [1, 2, 3]
ProductItem
ではString型を使用し、CartItem
ではInt型を使用しています。プロトコルに型を指定しなくても、準拠先で柔軟に変更することができました。
typealias
キーワードを明示的に書かなくても、実装から型推論によって自動的に型を特定させることもできます。
protocol ItemProtocol {
associatedtype Item
var items: [Item] { get }
}
struct CartItem: ItemProtocol {
// typealias を使わないで定義する
var items = [1,2,3]
}
let cartItem = CartItem()
cartItem.items // [1, 2, 3]
# 型の制約
関連型には、スーパークラスやプロトコルを指定して型の制約を追加することができます。コンパイラは型の制約が満たされているかチェックし、満たさない場合はエラーを出力します。
制約の追加は、:
を記述してプロトコルかスーパークラスを指定します。
protocol プロトコル {
associatedtype 関連型: プロトコル名 or スーパークラス名
}
次の例では、ItemClass
をスーパークラスにして、プロトコルの関連型に制約を追加しています。ItemClass
を満たす型のみプロトコルを準拠させることができます。
class ItemClass {}
protocol ItemProtocol {
// ItemClass をスーパークラスにして、ItemClassを満たす型のみを許容する
associatedtype Item: ItemClass
var items: [Item] { get }
}
class ItemSubClass: ItemClass {}
struct ProductItem: ItemProtocol {
// ItemSubClass は ItemClass を継承しているので制約を満たしている
typealias Item = ItemSubClass
var items = [ItemSubClass]()
}
String型を指定すると、ItemClass
を満たしていないのでコンパイラはエラーを出力します。
class ItemClass {}
protocol ItemProtocol {
associatedtype Item: ItemClass
var items: [Item] { get }
}
struct ProductItem: ItemProtocol {
// String型は ItemClass を満たしていない
typealias Item = String
var items = [String]()
}
/* 実行結果 */
// error: type 'ProductItem' does not conform to protocol 'ItemProtocol'
// struct ProductItem: ItemProtocol {
// note: possibly intended match 'ProductItem.Item' (aka 'String') does not inherit from 'ItemClass'
// typealias Item = String
// note: protocol requires nested type 'Item'; do you want to add it?
# 継承
プロトコルは他のプロトコルを継承させることができます。プロトコルを継承させることで、継承元のインターフェースを引き継ぐことができます。
継承させるには、:
の後に、継承元のプロトコルをカンマ区切りで書きます。
次の例では、RectangleProtocol
と ColorProtocol
を継承するプロトコルを正義しています。
protocol RectangleProtocol {
var width: Int { get }
var height: Int { get }
}
protocol ColorProtocol {
var color: String { get }
}
// RectangleProtocol と ColorProtocol を継承する
protocol RectangleColorProtocol: RectangleProtocol, ColorProtocol {}
struct RedRectangle: RectangleColorProtocol {
// 全てのインターフェースを満たす
var width = 200
var height = 200
var color = "red"
}
準拠させる構造体では、全てのインターフェースを満たす必要があります。
# クラス専用のプロトコル | Class-Only Protocol
プロトコルには、クラスのみを準拠させるように制約をかけることができます。
class
キーワードを使うと準拠させる側は、クラスのみを使わなければなりません。
// class キーワードを指定する
protocol NameProtocol: class {
var name: String { get }
}
// クラスに準拠させる
class Person: NameProtocol {
var name: String
init(name: String) {
self.name = name
}
}
構造体などを準拠させると、コンパイラはエラーを出力します。
protocol NameProtocol: class {
var name: String { get }
}
// 構造体などを準拠させる
struct Person: NameProtocol {
var name: String
}
/* 実行結果 */
// error: non-class type 'Person' cannot conform to class protocol 'NameProtocol'
// struct Person: NameProtocol {
# エクステンション
プロトコルは、エクステンション を使って拡張させることができます。インターフェースを拡張するのではなく、実装の処理を追加するのに使われます。
次の例では、NameProtocol
にメソッドを追加しています。準拠先では、追加されたメソッドを実行することができます。
protocol NameProtocol {
var name: String { get }
}
// extension を記述してプロトコルを拡張する
extension NameProtocol {
func printName() {
print(self.name)
}
}
struct Person: NameProtocol {
var name: String
}
var person1 = Person(name: "Taro")
// printName() はプロトコルに実装済みなので実行できる
person1.printName() //Taro