プロトコルとは、型のインターフェースを定義するものです。
インターフェースとは、型がどのようなプロパティやメソッドを持つか示したものになります。
#型のインターフェースを定義する方法
プロトコルは、型が特定の性質や機能を持つために必要なインターフェースを定義するものです。
また、プロトコルが要求するインターフェースを型が満たすことを準拠と言います。
私の記事でたまに登場する、
「○○型は△△プロトコルに準拠しているので、このメソッドを使うことができます。」
というフレーズはそれを意味しています。
プロトコルを利用することで、複数の型で共通となる性質を抽象化できます。
例えば、2つの値が同値であるかどうかを同値性と言い、
同値性が検出可能であるという性質は、
標準ライブラリのEquatableプロトコルで表現されています。
同値性、すなわち**==演算子**は、Equatableプロトコルに定義されています。
そして、Equatableプロトコルに準拠する方は==演算子に対する実装を定義する必要があります。
プロトコルのおかげで様々な恩恵を受けるのですが、
その中の一つで、プロトコルに準拠している型のみを扱うことが可能になります。
次のサンプルコードでは、
関数の引数を2つ指定し値が同じかどうか出力していますが、
渡せる引数の型をEquatableプロトコルに準拠している型のみに制限しています。
制限している箇所は、func equate<T: Equatable>(・・・)
の部分になります。
T型はEquatableプロトコル準拠している型ですよ。と制限をかけています。
(TではなくAでもBでも実行可能です。Tを使う傾向があるそうです・・・。)
今回、引数に渡している値はInt型やString型になり、
これらの型はEquatableプロトコルに準拠しているので実行できます。
(_ value1: T, _ value2: T)
とあるように、
第一引数と第二引数で引数の型をT型で統一しているので、
異なる型を引数に渡すとコンパイルエラーが発生します。
func equate<T: Equatable>(_ value1: T, _ value2: T) {
if value1 == value2 {
print("値は同じです。")
} else {
print("値が異なります。")
}
}
equate("abc", "abc")
equate(1, 1)
equate(1, 2)
// equate(1, "1") // コンパイルエラー
実行結果
値は同じです。
値は同じです。
値が異なります。
#プロトコルの基本
プロトコルの定義方法や準拠方法、利用方法について説明します。
##定義方法
プロトコルはprotocolキーワード
を使用して宣言し、
{ }内にプロパティやメソッドなどの構成要素を定義していきます。
構成要素については後ほど記載します。
protocol プロトコル名 {
プロトコルの定義
}
##準拠方法
型はプロトコルに準拠することにより、
プロトコルで定義されたインターフェースを通じて扱うことが可能になります。
型をプロトコルに準拠されるためには下記のように記述します。
(今回は構造体をプロトコルに準拠させています。)
また、,
で区切ることにより複数のプロトコルに準拠させることが可能です。
struct 構造体名: プロトコル名1, プロトコル名2 ・・・ {
構造体の定義
}
プロトコルに準拠するには、
プロトコルが要求している全てのインターフェースに対する実装を用意する必要があります。
次のサンプルコードのように、プロトコルでメソッドを定義した場合は、
そのプロトコルに準拠している方は同じメソッドを型内に定義する必要があります。
定義がされていない場合はコンパイルエラーになります。
protocol SampleProtocol {
func sampleMethod()
}
struct Sample: SampleProtocol {
func sampleMethod() { }
}
struct Sample2: SampleProtocol { } // コンパイルエラー
エラー内容:Type 'Sample2' does not conform to protocol 'SampleProtocol'
和訳:タイプ「Sample2」はプロトコル「SampleProtocol」に準拠していません
###クラス継承時の準拠方法
構造体だけでなくクラスでもプロトコルに準拠することが可能ですが、
先ほどの定義方法だとスーパークラスなのかプロトコルなのかわかりません。
実は決まりがあり、継承とプロトコルへの準拠を同時に行う場合は、
最初に継承するスーパークラス名を書き、次にプロトコルを書く必要があります。
class クラス名: スーパークラス, プロトコル名1, プロトコル名2 ・・・ {
クラスの定義
}
###エクステンションによる準拠方法
プロトコルへの準拠はエクステンションで行うことも可能です。
エクステンションで準拠対象のプロトコルを追加する方法は下記になります。
extension エクステンションを定義する型: プロトコル名 {
プロトコルが要求する定義
}
1つのエクステンションに対して複数のプロトコルを準拠させることも可能です。
これは人によって別れると思いますが可読性を高めるためには1つずつがいいと思います。
というのも、一度に複数のプロトコルに準拠させると、
どのプロトコルに対する定義なのかが分かりづらくなるからです。
コード量は増えますが、一度のエクステンションで一つのプロトコルに準拠させれば、
どのプロトコルで宣言されている内容なのかが分かりやすくなります。
自分以外の人もメンテナンスする可能性があるので、
可読性を高くするためにも分けた方がいいかな?と私は思います。
実際に分けると下記のような記述になります。
// 1つ目のプロトコル
protocol SampleProtocol1 {
func sampleMethod1()
}
// 2つ目のプロトコル
protocol SampleProtocol2 {
func sampleMethod2()
}
//構造体
struct Sample {
let a = 1
}
// エクスションション1回目
extension Sample: SampleProtocol1 {
func sampleMethod1() { }
}
// エクスションション2回目
extension Sample: SampleProtocol2 {
func sampleMethod2() { }
}
###コンパイラによる準拠チェック
あるプロトコルに準拠した型がそのプロトコルの要件を満たしているかどうかは
コンパイラによってチェックされ、一つでも欠けている場合はコンパイルエラーとなります。
次のサンプルコードでは、
SampleProtocol
に準拠した型であるSample型
がありますが、
プロトコルで定義されているsampleMethod()
を構造体で定義していません。
この場合、要件を満たしていないのでコンパイルエラーが発生します。
protocol SampleProtocol {
func sampleMethod()
}
struct Sample: SampleProtocol{ }
エラー内容:Type 'Sample' does not conform to protocol 'SampleProtocol'
和訳:タイプ「Sample」はプロトコル「SampleProtocol」に準拠していません
##利用方法
プロトコルは、構造体・クラス・列挙型・クロージャと同様に、
変数・定数・引数の型として利用することができます。
プロトコルに準拠している型はプロトコルにアップキャストすることが可能です。
なので、次のサンプルコードような処理が可能です。
func sample(x: SampleProtocol)
のように引数の型にプロトコルを指定した場合、
引数に渡すことができる型はSampleProtocolに準拠した型のみになります。
x.value
のように、
プロトコルに定義されているプロパティやメソッドを使えるのも特徴です。
なお、プロトコルに準拠していない型を引数に渡した場合は
コンパイルエラーが発生します。
protocol SampleProtocol {
var value: Int { get }
}
struct Sample: SampleProtocol{
var int: Int
var value: Int {
return int * 10
}
}
// 引数の型にプロトコルを指定
func sample(x: SampleProtocol) -> Int {
// 引数xのプロパティやメソッドの内
// SampleProtocolで定義されているものが使える
x.value
}
let a = 1 // Int型
let b = Sample(int: 2) // Sample型
sample(x: a) // コンパイルエラー
sample(x: b) // 20
エラー内容:Argument type 'Int' does not conform to expected type 'SampleProtocol'
和訳:引数タイプ「Int」が予期されるタイプ「SampleProtocol」に準拠していません
なお、連想型を持つプロトコルは変数や定数、引数の型として使用できず、
ジェネリクスの型引数の型制約の記述のみに利用可能です。
(連想型やジェネリクスについては別記事で説明します。)
###プロトコルコンポジション
プロトコルコンポジションとは、複数のプロトコルに準拠した型を表現する仕組みです。
使用方法は、複数のプロトコル名を&
で区切って記述していきます。
次のサンプルコードでは、
Protocol1とProtocol2の両方に準拠している型を引数に指定しています。
なので、x.value1 + x.value2
のように、
各プロトコルで定義されているプロパティを使うことができます。
protocol Protocol1 {
var value1: Int { get }
}
protocol Protocol2 {
var value2: Int { get }
}
struct Sample: Protocol1, Protocol2 {
var value1: Int
var value2: Int
}
func plus(x: Protocol1 & Protocol2) -> Int {
x.value1 + x.value2
}
let sample = Sample(value1: 10, value2: 5)
plus(x: sample) // 15
複数のプロトコルに準拠させる場合は、
特に型を統一する必要はなく次のようなコードを記述することも可能です。
protocol Protocol1 {
var value: Int { get }
}
protocol Protocol2 {
var name: String { get }
}
struct Sample: Protocol1, Protocol2 {
var value: Int
var name: String
}
func plus(x: Protocol1 & Protocol2) {
print("\(x.name)さんが\(x.value)円くれました。")
}
let sample = Sample(value: 1000, name: "近藤")
plus(x: sample)
実行結果
近藤さんが1000円くれました。
以上がプロトコルの概念と定義方法、利用方法になります。
少し長くなってしまい申し訳ないです。
プロトコルは出現頻度も高く必修科目かと思うのでぜひ使えるようになってください!
この記事の他にもプロトコルについて説明している記事がありますので、
お時間ありましたらそちらもご覧ください。
・【Swift】プロトコルを構成する要素
・【Swift】プロトコルエクステンションについて
最後までご覧いただきありがとうございました!