目次
はじめに
プロトコルとは
プロトコルの基本
構成要素
プロトコルエクステンション
クラスonlyプロトコル
はじめに
プロトコルとはイベント通知でdelegateを使う際に扱うものというようなざっくりとしたイメージしか持っていなかったため、今回改めてプロトコルとは何なのか、どうゆう機能が備わっているのかということを復習しながら解説したいと思います。
プロトコルとは
プロトコルとは、型のインターフェイスを定義するものです。インターフェイスは、型がどのようなプロパティやメソッドを持っているかを示します。また、プロトコルが要求するインターフェイスを満たすことを準拠と言います。
プロトコルはよくイベント通知機能のdelegateなどで使われがちですが、本質的な利点として、複数の型で共通となる性質を抽象化できるということがあります。例えば、2つの値が同じであるかどうかが検証可能であるという性質は、標準ライブラリのEquatableプロトコルとして表現されています。つまり、Equatableプロトコルに準拠してればどんな型でも2つの値が同じであるかを検証することができます。
プロトコルの基本
定義方法
プロトコルはprotocolキーワードを使用して宣言し、{ }内にプロパティやメソッドなどのプロトコルを構成する要素を定義していきます。
protocol プロトコル名 {
プロトコルの定義
}
準拠方法
型はプロトコルに準拠することにより、プロトコルで定義されたインターフェイスを通じて扱うことが可能になります。
型を準拠させるには、型名の後に「:」を追加し、準拠する対象のプロトコル名をつづけます。型は複数のプロトコルに準拠でき、複数のプロトコルに準拠するにプロトコルを「 , 」区切りで追加します。
struct 構造体名: プロトコル名1,プロトコル名2... {
構造体の定義
}
プロトコルに準拠するには、プロトコルが要求しているすべてのインターフェイスに対する実装を用意する必要があります。
protocol SomeProtocol {
func someMethod()
}
struct SomeStruct1: SomeProtocol {
func someMethod() {}
}
// someMethod()が定義されていないため、コンパイルエラー
struct SomeStruct2: SomeProtocol {}
エクテンションによる準拠方法
プロトコルの準拠はエクステンションで行うこともできます。
複数のプロトコルに準拠する時などは特に、どのプロパティやメソッドがどのプロトコルで宣言されているものなのか分かりにくくなりがちですが、エクステンションを利用すればコードの可読性を高めることができます。
protocol SomeProtocol1 {
func someMethod1()
}
protocol SomeProtocol2 {
func someMethod2()
}
struct SomeStruct {
let someProperty: Int
}
extension SomeStruct: SomeProtocol1 {
func someMethod1() {}
}
extension SomeStruct: SomeProtocol2 {
func someMethod2() {}
}
利用方法
プロトコルは構造体、クラス、列挙型、クロージャと同様に、変数、定数や引数の型として利用できます。プロトコルに準拠している型はプロトコルにアップキャスト可能であるため、型がプロトコルの変数や定数に代入できます。型がプロトコルの変数と定数では、プロコトルで定義されているプロパティやメソッドを使用できます(これがイベント通知でdelegateプロパティを作成して、それを通じてメソッドなどが使える理由ですね)。
protocol SomeProtocol {
var variable: Int { get }
}
func someMethod(x: SomeProtocol) {
// 引数xのプロパティやメソッドのうち、SomeProtocolで定義されているものが使用可能
x.variable
}
プロトコルポジション
プロトコルポジションは、複数のプロトコルに準拠した型を表現するための仕組みです。プロトコルポジションを使用するには、複数のプロトコル名を「&」区切りで指定してプロトコル名1&プロトコル名2のように記述します。
protocol SomeProtocol1 {
var variable1: Int { get }
}
protocol SomeProtocol2 {
var variable2: Int { get }
}
struct SomeStruct: SomeProtocol1, SomeProtocol2 {
var variable1: Int
var variable2: Int
}
func someFuction(x: SomeProtocol1 & SomeProtocol2) {
x.variable1 + x.variable2
}
let a = SomeStruct(variable1: 1, variable2: 2)
someFuction(x: a) // 3
構成要素
プロパティ
プロトコルにはプロパティを定義でき、プロトコルに準拠する型にプロパティの実装を要求できます。
定義方法
プロトコルのプロパティではプロパティ名、型、ゲッタとセッタの有無のみを定義し、プロトコルに準拠する型で要求に応じてプロパティを実装します。プロトコルのプロパティは常にvarキーワードで宣言し、{ }内にゲッタとセッタの有無に応じてそれぞれgetキーワードとsetキーワードを追加します。letキーワードが使用できないのは、プロトコルのプロパティにはストアドプロパティやコンピューテッドといった区別がないためです(ここで私はなぜ区別がないの?と疑問を感じたのですが、未だ明確な答えを出し切れていないです泣)。
protocol プロトコル名 {
var プロパティ名: 型 { get set }
}
ゲッタの実装
プロパティが定義されているプロトコルに準拠するには、プロトコルで定義されているプロパティを実装する必要があります。プロパティがゲッタしかない場合は、変数または定数のストアドプロパティを実装するか、ゲッタを持つコンピューテッドプロパティを実装します。
protocol SomeProtocol {
var id: Int { get }
}
// 変数のストアドプロパティ
struct SomeStruct1: SomeProtocol {
var id: Int
}
// 定数のストアドプロパティ
struct SomeStruct2: SomeProtocol {
let id: Int
}
// コンピューテッドプロパティ
struct SomeStruct3: SomeProtocol {
var id: Int { return 1 }
}
セッタの実装
プロトコルで定義されているプロパティがセッタも必要としている場合は、変数のストアドプロパティを実装するか、ゲッタとセッタの両方を持つコンピューテッドプロパティを実装します。なお、定数のストアドプロパティでは変更が不可能であるため、プロトコルの要件を満たすことはできません。
protocol SomeProtocol {
var title: String { get set }
}
// 変数のストアドプロパティ
struct SomeStruct1: SomeProtocol {
var title: String
}
// コンピューテッドプロパティ
struct SomeStruct2: SomeProtocol {
var title: String {
get { return "title" }
set {}
}
}
// 定数のストアドプロパティ
struct SomeStruct3: SomeProtocol {
let title: String // コンパイルエラー
}
メソッド
プロトコルにはメソッドを定義でき、プロトコルに準拠する型にメソッドの実装を要求できます。
定義方法
プロトコルのメソッドではメソッド名、引数の型、戻り値の方のみを定義し、プロトコルに準拠する型でその要求を満たす実装を提供します。
protocol プロトコル名 {
func 関数名(引数) -> 戻り値の型
}
メソッドの実装
メソッドが定義されているプロトコルに準拠するには、同じインターフェイスを持つメソッドを実装します。
protocol SomeProtocol {
func someMethod() -> Void
static func someStaticMethod() -> Void
}
struct SomeStruct: SomeProtocol {
func someMethod() -> Void {
// メソッドの実装
}
static func someStaticMethod() -> Void {
// メソッドの実装
}
}
連想型
ここまでの方法ではプロトコルの定義時にプロパティの型やメソッドの引数や戻り値の型を具体的に指定する必要がありました。しかし、連想型(associated type)を用いると、プロトコルの準拠時にこれらの型を指定できます。プロトコルでは連想型はプレースホルダとして働き、連想型の実際の型は準拠する型のほうで指定します。連想型を使用すれば、1つの型に依存しない、より抽象的なプロトコルを定義できます。
定義方法
プロトコルの連想型の名前は、associatedtypeキーワードを用いて定義します。プロトコルの連想型は、同じプロトコル内でのプロパティやメソッドの引数や戻り値の型として使用できます。
protocol プロトコル名 {
associatedtype 連想型名
var プロパティ名: 連想型名
func メソッド名(引数名: 連想型名)
func メソッド名() -> 連想型名
}
連想型の実際の型は、プロトコルに準拠する型ごとに指定できます。連想型の実際の型の指定には型エイリアスを使用し、準拠する型の定義の内部で、連想型と同名の型エイリアスをtypealias 連想型名 = 指定する型名と定義します。ただし、実装から連想型が自動的に決定する場合は、型エイリアスの定義を省略できます。また連想型は、型エイリアスだけでなく、同名のネスト型によって指定することもできます。
protocol SomeProtocol {
associatedtype AssociatedType
// 連想型はプロパティやメソッドでも使用可能
var value: AssociatedType { get }
func someMethod(value: AssociatedType) -> AssociatedType
}
// AssociatedTypeを定義することで要求を満たす
struct SomeStruct1: SomeProtocol {
typealias AssociatedType = Int
var value: AssociatedType
func someMethod(value: AssociatedType) -> AssociatedType {
return 1
}
}
// 実装からAssociatedTypeが自動的に決定する
struct SomeStruct2: SomeProtocol {
ver value: Int
func someMethod(value: Int) -> Int {
return 1
}
}
// ネスト型でAssociatedTypeを定義することで要求を満たす
struct SomeStruct3: SomeProtocol {
struct AssociatedType {}
var value: AssociatedType
func someMethod(value: AssociatedType) -> AssociatedType {
return AssociatedType()
}
}
型制約の追加
プロトコルの連想型が準拠すべきプロトコルや継承すべきスーパークラスを指定して、連想型に制約を設けることができます。
次の例では、連想型AssociatedTypeにSomeClass型を継承していなければならないという制約を設けています。プロtコルの準拠時にAssociatedType型にInt型のようなSomeClass型を継承していない型を指定すると、プロトコルに準拠していないことになりコンパイルエラーとなります。
class SomeClass {}
protocol SomeProtocol {
associatedtype AssociatedType: SomeClass
}
class SomeSubClass: SomeClass {}
// SomeSubClassはSomeClassのサブクラスなのでAssociatedTypeの制約を満たす
struct ConformedStruct: SomeProtocol {
typealias AssociatedType = SomeSubClass
}
// IntはSomeClassのサブクラスではないのでコンパイルエラー
struct NonCoformedStruct: SomeProtocol {
typealias AssociatedType = Int
}
where節
プロトコル名につづけてwhere節を追加すると、より詳細な制約を追加できます。where節では、プロトコルに準拠する型自身をSelfキーワードで参照でき、その連想型も「 . 」を付けて「Self.連想型」のように参照できます。またSelfキーワードを省略して連想型とすることもできます。.を続けてSelf.連想型.連想型の連想型と記述することで、連想型の連想型も参照できます。
次の例では、SomeDataプロトコルの連想型ValueContainerの連想型Contentが、Equatableプロトコルに準拠するという制約を設けています。
protocol Container {
associatedtype Content
}
protocol SomeData {
associatedtype ValueContainer: Container where
ValueContainer.Content: Equatable
}
また:によるプロトコルへの準拠やクラスの継承の制約に加えて、==による型の一致の制約も設定できます。
protocol Container {
associatedtype Content
}
protocol SomeData {
associatedtype ValueContainer: Container where
ValueContainer.Content == Int
}
プロトコルの継承
プロトコルは他のプロトコルを継承できます。プロトコルの継承は、単純に継承元のプロトコルで定義されているプロパティやメソッドなどをプロトコルに引き継ぐものであり、クラスにおけるオーバライドのような概念はありません。
protocol ProtocolA {
var id: Int { get }
}
protocol ProtocolB {
var title: String { get }
}
protocol ProtocolC: ProtocolA, ProtocolB {}
// ProtocolAとProtocolBで定義したプロパティを実装する必要がある
struct SomeStruct: ProtocolC {
var id: Int
var title: String
}
プロトコルエクステンション
プロトコルエクステンションはプロトコルが要求するインターフェイスを追加するものではなく、プロトコルに実装を追加するものです。プロトコルエクステンションでは、通常のエクステンションと同様の実装を行えます。
定義方法
プロトコルエクステンションを定義するには、extensionキーワードを使用します。
次の例では、Furnitureプロトコルのエクステンションにdescriptionプロパティを実装しているため、Furnitureプロトコルに準拠しているKitchen型でもdescriptionプロパティを使用できます。
protocol Furniture {
var name: String { get }
var category: String { get }
}
extension Furniture {
var description: String {
return "家具名: \(name), カテゴリ: \(category)"
}
}
struct Kitchen: Furniture {
let name: String
var category: String {
return "食器"
}
}
let plate = Kitchen(name: "お皿")
print(plate.description) // 家具名: お皿, カテゴリ: 食器
デフォルト実装による実装の任意化
プロトコルに定義されているインターフェイスに対してプロトコルエクステンションで実装を追加すると、プロトコルに準拠する型での実装は任意となります。準拠する型が実装を再定義しなかった場合はプロトコルエクステンションの実装が使用されます。これをデフォルト実装と言います。
protocol Furniture {
var name: String { get }
var caution: String? { get }
}
extension Furniture {
// デフォルト実装を定義しているため、準拠する際実装しなくてもよい。
var caution: String? { return nil }
var description: String {
var description = "家具名: \(name)"
if let caution = caution {
description += "、 注意事項: \(caution)"
}
return description
}
}
struct Dining: Furniture {
let name: String
}
struct Kitchen: Furniture {
let name: String
var caution: String? {
return "割れ物になります"
}
}
let table = Dining(name: "テーブル")
print(table.description) // 家具名: テーブル
let plate = Kitchen(name: "お皿")
print(plate.description) // 家具名: お皿、 注意事項: 割れ物になります
型制約の追加
プロトコルエクステンションには型制約を追加でき、条件を満たす場合のみプロトコルエクステンションを有効にできます。プロトコルエクステンションの型制約は、プロトコル名に続くwhere節内に記述します。
extension プロトコル名 where 型制約 {
制約を満たす場合に有効となるエクステンション
}
次の例では、Collectionプロトコルの連想型ElementがInt型と一致する場合にのみ利用可能となるエクステンションを定義し、sumプロパティで各要素の合計を返します。(Collectionプロトコルとは、配列やdictionary型などを扱う際にで使用できるプロトコルです。)
extension Collection where Element == Int {
var sum: Int {
return reduce(0) { return $0 + $1 }
}
}
let integers = [1, 2, 3]
integers.sum // 6
let strings = ["a", "b", "c"]
strings.sum // stringsの要素はInt型でないため、コンパイルエラー
クラスonlyプロトコル
プロトコルにAnyObjectプロトコルを継承させることで、プロトコルの準拠をクラスのみに制限することができます。AnyObjectプロトコルとはクラスが暗黙的に準拠しているプロトコルです。
protocol SomeProtocol: AnyObject {
}
class SomeClass: SomeProtocol {
} // OK
protocol SomeProtocol: AnyObject {
}
struct SomeStruct: SomeProtocol {
} // コンパイルエラー
参考
・Swift実践入門 (https://gihyo.jp/book/2020/978-4-297-11213-4)
・公式リファレンス (https://docs.swift.org/swift-book/)