プロトコルとは
プロトコルとはそのプロトコルに準拠しているクラスや構造体に対して共通のルールを設定するものです。
感覚としてはジェネリクスに近いのでまずはジェネリクスの例から考えてみます。
ジェネリクスの考え方
例えば、二つの入力された整数の積を計算する関数を考えます。
import SwiftUI
print(multiple(10, 5)) // 50
func multiple(_ a: Int, _ b: Int) -> Int {
return a * b
}
すると計算結果として正しく 50 を得ることができます。
ただ、これだと整数同士での掛け算にしか対応していません。もし次のように実数同士を引数に与えると型が違うので計算できないと言われてしまいます。
print(multiple(10.0, 5.0)) // Cannot convert value of type 'Double' to exptected argument type 'Int'
それでは困るので「実数同士」でも計算できるようにしてみます。
Swift は関数のオーバーロードに対応しているので、全く同じ関数名でも引数が少し違えば定義可能です。
import SwiftUI
print(multiple(10, 5))
print(multiple(10.0, 5.0))
func multiple(_ a: Int, _ b: Int) -> Int {
return a * b
}
func multiple(_ a: Double, _ b: Double) -> Double {
return a * b
}
しかし、よく考えるとこの実装方法は愚直であることに気付きます。一方だけがInt
のときや、型がCGFlaot
のときなどありとあらゆるパターンを考えていると関数の定義だけがどんどん増えてしまうからです。
そこで任意の型を引数にとれるように関数自体を改良します。
func multiple<T>(_ a: T, _ b: T) -> T {
return a * b
}
というわけで、任意の型であるT
を引数としてとり、その結果として型T
を返す関数に改良しました。
が、これはそのままではコンパイルエラーが発生します。
というのも、計算式の途中にあるa * b
が計算可能であるためにはT
が掛け算可能な型である必要があるためです。なので、T
は単なる「任意の型」ではなく「任意の掛け算可能な型」として再定義します。
func multiple<T: Numeric>(_ a: T, _ b: T) -> T {
return a * b
}
掛け算可能な型であることを明示するためにはT
がNumeric
に準拠させればよいです。FloatingPoint
に準拠させても同様の処理は可能ですが、FloatingPoint
は実数であることを前提としているので返り値も必ず実数になってしまいます。
整数同士での計算は整数で返したいのでこの場合はNumeric
の方が良いでしょう。
算術プロトコル
算術プロトコルにはたくさんあるのですが、まあとりあえず以下の三つがよく出てきます。
プロトコル | 加算 | 減算 | 乗算 | 除算 |
---|---|---|---|---|
AdditiveArithmetic | YES | YES | - | - |
Numeric | YES | YES | YES | - |
FloatingPoint | YES | YES | YES | YES |
除算までサポートしようとすると FloatingPoint を利用する必要があるわけですね。
そこで、二つの数を引数にとってa / b
の値を返すdivide()
の関数を以下のようにつくります。
func divide<T: FloatingPoint>(_ a: T, _ b: T) -> T {
return a / b
}
これはこれで別に問題なく動作するのですが、整数型同士で計算した場合少し違和感があります。
print(10/5) // 2
print(divide(10, 5)) // 2.0
というのも単に整数型同士で除算した場合は、計算結果も整数になるのに対して、divide()
を利用した場合は返り値がFloatingPoint
型のために必ず実数で出力されてしまうという点です。
また、Int
型はFloatinPoint
に準拠していないため以下のコードのように変数の型を明示してしまうとコンパイルエラーが発生してしまいます。
let a: Int = 10
let b: Int = 5
print(divide(a, b)) // Global function 'divide' requires that 'Int' conform to 'FloatingPoint'
暫定処置
FloatingPoint
とInt
に互換性がない以上は一つの関数で処理するのは難しそうなので以下のようにコードを改良するのが一つの手ではあります。
それか、引数に代入するときにDouble
やCGFloat
などにキャストします。もっと上手い解決策がありそうなのですが、わからなかったのでとりあえずこれで対応しています。
func divide<T: FloatingPoint>(_ a: T, _ b: T) -> T {
return a / b
}
func divide(_ a: Int, _ b: Int) -> Int {
return a / b
}
プロトコルを型として利用する
さて、ここまでの話はプロトコルを使って変数の引数を柔軟に扱おうという話でした。
ここからは更に一方進んでプロトコルに準拠したクラスや構造体をつくり、それらを変数として扱いたい場合を考えます。
話がややこしいので具体例を出します。例えば Dog クラスと Cat クラスを作成し、プロパティとして名前をもたせるとします。
class Dog {
let name: String
}
class Cat {
let name: String
}
そして、次に飼い主のクラスを作成します。愚直に書くと以下のようになります。
猫を飼っている人がいるかも知れませんし、犬を買っている人がいるかも知れないので犬と猫のどちらもプロパティにもつ必要があります。
class Person {
let cats: [Cat]
let dogs: [Dog]
}
ここで問題になるのは、動物の種類が増えるとプロパティ名が無数に増えていってしまい可読性が低下するという点です。
プロトコルで解決する
そこで、犬と猫をどちらも一括で扱えるようなAnimal
プロトコルを作成します。
protocol Animal {
var name: String { get } // Required
}
class Dog: Animal {
var name: String // Required
}
class Cat: Animal {
var name: String // Required
}
class Person {
let animals: [Animal]
}
イニシャライザを定義する
このままだとわかりにくいのでイニシャライザをつけてコンパイルが通るようにします。
プロトコルで設定されている変数や関数は必ずそのプロトコルを準拠するクラスなどでは宣言しなければいけません。変数の場合はそのままかけばいいのですが、イニシャライザの場合はrequired
とつけてプロトコルの準拠のために必要であることを明示する必要があります。
import SwiftUI
protocol Animal {
var name: String { get } // Required
init(name: String)
}
class Dog: Animal {
var name: String // Required
required init(name: String) { // Required
self.name = name
}
}
class Cat: Animal {
required init(name: String) { // Required
self.name = name
}
var name: String // Required
}
class Person {
var animals: [Animal]
init(animals: [Animal] = []) {
self.animals = animals
}
}
サンプルコード
let mike: Cat = Cat(name: "Mike")
let nike: Dog = Dog(name: "Nike")
let tom = Person(animals: [mike, nike])
for animal in tom.animals {
print(animal.name) // Mike, Nike
}
For 文の中でそれぞれ異なるクラスのオブジェクトをループさせているのにanimal.name
で名前を呼び出せるのは、animal
がAnimal
プロトコルに準拠しており、必ずname
のプロパティを持っていることが担保されているためです。
protocol Animal {
var name: String { get } // <- Required
init(name: String)
}
もしここでこの行をコメントアウトするとValue of type 'Animal' has no member 'name'
とコンパイルエラーが表示されます。
プロトコルに準拠した Enum を作成する
今回考えて悩んだのはここでした。
いまネットワーク系のライブラリを作成しているのですが、そのライブラリは通信が失敗した際にはエラーを返します。ここではそのライブラリが返すエラーはAPIErrorA
というEnum
だとします。
そして仮にエラーの種類が二種類しかないとすると、次のように定義すれば良いわけです。
enum APIErrorA: Error {
case forbidden
case invalid
}
そして次にそのライブラリを使用するアプリを考えてみます。アプリは基本的にはこのAPIErrorA
を使ってエラーを表示すれば良いのですが、エラーを更に細分化したい場合があります。
例えば、ある XXX というエンドポイントを叩いてinvalid
が返ってきた場合にはinvalidXXX
, YYY というエンドポイントの場合はinvalidYYY
という具合です。
ライブラリ側に追加すればそれはそれで解決なのですが、アクセスするエンドポイント名ごとに Enum を増やしていてはほとんどの人は使わない無意味なcase
がライブラリに組み込まれてしまいます。
そのような定義はライブラリではなくアプリ側で実装すべきです。
// コンパイルエラー
extension APIErrorA {
case invalidXXX
case invalidYYY
}
という風に Extension で追加できれば良いのですが、実は Extension を使って Enum の case を追加することは不可能です。
となれば新たにエラーのクラスを作成するしかありません。
enum APIErrorB: Error {
case invalidXXX
case invalidYYY
}
こうすれば実装はできるのですが、利用する上で大変不便です。
何故なら、ライブラリはAPIErrorA
の Enum で返してくるので当然受け取る側の変数もlet error: APIErrorA
のようにAPIErrorA
型であることを明示しなければなりませんが、こうなるとアプリが返してくるはずのAPIErrorB
のエラーを受け取れないからです。
エラーを受け取る変数を二つ用意すればいいのですが、それをやると先程の動物の例と同じように冗長なコードになってしまいます。
そこでライブラリ側にはエラーの拡張を許すようにプロトコルを使ってエラーを定義します。
// ライブラリ
protocol PlatformError: LocalizedError { }
enum APIError: PlatformError {
case forbidden
case invalid
}
// ライブラリを利用するアプリ
enum APPError: PlatformError {
case invalidXXX
case invalidYYY
}
今回はプロトコルに準拠させるだけなので別にプロトコル内には何も書かなくて大丈夫です。
このようにプロトコルに何も特別なことを書かない場合は簡単に利用することができます。
プロトコルの罠
ところが、PlatformError
をLocalizedError
だけでなくIdentifiable
にも準拠させるとコンパイルエラーが発生します。
protocol PlatformError: LocalizedError, Identifiable {
var id: String { get }
}
let errors: [PlatformError] = [APIError.forbidden, APPError.change] // Protocol 'PlatformError' can only be used as a generic nostraint because it has Self or associated type requirements
コンパイルエラーを読むと「プロトコルがassociated type
の要件を持っているから」とあります。
ここでIdentifiable
プロトコルのドキュメントを読んでみると、
::: tip Identifiable
associatedtype ID
A type representing the stable identity of the entity associated with an instance.
Required
:::
と書いておりIdentifiable
に準拠したことでassosiated type
が要件に加わり、そのためにコンパイルエラーが発生したことがわかります。
Swift のジェネリックなプロトコルの変数はなぜ作れないのか、コンパイル後の中間言語を見て考えたにもあるように、
::: tip 導入
Swift では通常のプロトコルは変数の型として使用することができますが、
型パラメータ(associated type)を持つジェネリックなプロトコルの変数は作れません。
:::
とあるように、プロトコルをIdentifiable
準拠にした段階でプロトコルを変数の型として利用することができなくなってしまうのです。
また、Identifiable
でなくてもassociatedtype
をプロトコル内に書いた段階で変数の型としては利用できなくなります。
Enum + CaseIterable
Identifaible
に準拠させてしまうとめんどくさいことはわかりましたが、CaseIterable
はどうでしょうか?
調べてみるとドキュメントには次のようにあります。
static var allCases: Self.AllCases
A collection of all values of this type.
Required.
associatedtype AllCases
A type that can represent a collection of all values of this type.
Required.
つまり、CaseIterable に準拠させるとassociatedtype
が設定されるので変数としてはプロトコルを指定できないことになります。
よって、アプリとライブラリのエラーを全て一括で配列にするPlatformError.allCases
は利用できないということになります。
まあでもプロトコルには適応できないというだけであって、それぞれの Enum に対してCaseIterable
準拠させれば似たようなことはできます。
// サンプルコード
import SwiftUI
protocol PlatformError: LocalizedError {
var rawValue: String { get }
}
enum APIError: String, PlatformError, CaseIterable {
case forbidden
}
enum APPError: String, PlatformError, CaseIterable {
case change
}
class ErrorTypeList {
var errors: [PlatformError]
init(errors: [PlatformError]) {
self.errors = errors
}
}
let errorType = ErrorTypeList(errors: (APPError.allCases + APIError.allCases))
for error in errorType.errors {
print(error.rawValue) // -> chnage, forbidden
}
まとめ
プロトコルにプロトコルを準拠させる時はassociatedtype
がついているか気をつけようね!!!