Page icon

第62回

2021/12/24
A Swift Tour
Empty
Empty
12 more properties
今回からは 
A Swift Tour
 の 
プロトコルと拡張
 について眺めていきます。プロトコルの定義の仕方から、それを適用したりといった、基本的なところを確認していく回になりそうです。どうぞよろしくお願いしますね。
————————————————————————————————— 熊谷さんのやさしい Swift 勉強会 #62
00:00 開始 00:41 拡張 01:30 前回の補足 02:15 構造体の値のアドレス 03:19 配列のアドレス 04:41 C 言語との相互運用 06:16 Copy-On-Write の内部バッファーを観察する 08:15 プロトコルと拡張 08:36 プロトコルの定義 10:02 インターフェイス 12:20 練習問題 14:57 プロトコルから連想した余談 15:56 プロトコルに要求を追加する 19:23 必須イニシャライザー 25:22 実装を任意にする方法 26:36 既定の実装 30:12 カスタマイズポイント 35:05 ジェネリクスとカスタマイズポイント 37:37 内部からカスタマイズポイントを呼ぶ場合 41:58 @objc protocol 44:53 オプショナルなメソッド 46:44 メッセージパッシング方式 48:26 オプショナルな機能を持っているかを判定する —————————————————————————————————
Transcription & Summarize : 熊谷さんのやさしい Swift 勉強会 #62
それでは始めていきましょう。今日は新しい単元、「プロトコルとエクステンション」のお話をしていこうと思います。Swift の勉強会では「何 & 何」というタイトルが好きなようですね。今回もエクステンションがプロトコルに特化した機能ではないのですが、「プロトコルとエクステンション」という括りで紹介されています。
まず、エクステンションについて補足しておきます。エクステンションというのは、型を拡張する機能です。クラスや構造体など、名前付きの型を拡張できる機能になっており、Swift で非常に重要な機能です。エニー型(
Any
)も含めるかどうかの議論がありましたが、とりあえず名前付きの型を拡張する機能と覚えておいてください。
前回の勉強会では、構造体の「コピーオンライト」について検証しました。その中で、構造体もアドレス(ポインター)に変換できるという話があったと思います。しかし、その説明のために意図していたコードと異なったコードを書いてしまい、それがたまたま正しく動いていました。そのため、その点について補足しておこうと思います。
例えば、ある構造体をアドレスに変換するための仕組みとして、次のような例を考えます。インスタンスがあったとき、それを関数で受け取る場合です。次の関数を考えてみましょう:
func printAddress<T>(of value: inout T) { withUnsafePointer(to: &value) { print($0) } }
このようにして、構造体のアドレスを取得することができます。これは Swift ではなかなか珍しい機能の一つです。
さて、この仕組みを配列で使ったときの話に移りましょう。通常の発想では、配列のアドレスをポインターとして取得することになります。次のようなコードを考えてみます:
var array: [Int] = [1, 2, 3]
これをポインターとして受け取りたい場合、仮に次のようにしてみます:
func printPointer(of array: inout [Int]) { array.withUnsafeBufferPointer { print($0.baseAddress) } }
このようにすれば、配列の要素の先頭アドレスを取得することができます。しかし、私が前回勘違いして書いたコードは、変数を単純に
Int
型のポインターとして受け取る形になっていました。それでもコードが通り、アドレスを正しく取得できたことに驚きました。
この理由を調べたところ、Swift にはC言語とのブリッジを円滑に行うための特別ルールがありました。具体的には、配列を
Int
型のポインターに変換する特別な解釈があり、次のような互換性を提供しています:
void someFunction(int *pointer) { // C言語のコード }
これに対して Swift では次のように書くことができ、自然にブリッジが行われます:
var array: [Int] = [1, 2, 3] someFunction(&array)
このように、Swift と C言語のインターオペラビリティ(相互運用性)を高めるための仕組みが存在しています。これにより、C言語との円滑な連携が可能となっています。
今回はプロトコルとエクステンションについての話が主題でしたが、前回の補足も含めてお話ししました。次回はさらに具体的なコード例を交えながら説明を進めていきたいと思います。 なので、自然に配列を
Int
のポインターに変換して渡すことができます。これがなかなか面白い仕組みだなと思いました。また、このときに思いがけず見つかったことがあります。それは、内部のバッファーを
UnsafePointer<Int>
として暗黙的に渡しているみたいです。この仕組みはコピーオンライトのように重要になります。
話が進む中で、「inout」という用語が出てきましたが、これは配列をポインターに変換するときにエレメントに対してポインターを作るという方法です。また、内部バッファーを直接渡してくれるので、コピーオンライトが働く前なら共有しているバッファーが渡りますし、コピーオンライトが発動した後なら新しいバッファーが渡されるようになります。この裏事情もたまたま知ることができて、なかなか面白いケースだなと思って勉強会が終わった後に感心していました。
このような特別な仕組みは、知っておかないと扱いが難しいので、今回の発見は良い機会になりました。これを把握しておくことで、Swiftの相互運用性だけでなく、Swift単体でコードを書く際にも良いコードを書くきっかけになりそうです。面白い発見だったなと思いました。
では、振り返りはこのぐらいにして、今日の本題に入りたいと思います。Swiftツアーなので、そんなに難しい話は出てこないですが、まず基本的な定義のところから見ていきます。
プロトコルについてですが、プロトコルは
protocol
というキーワードを使って宣言できます。定義したプロトコルはクラスや列挙型、構造体に適用することができます。他に適用できる型は現在のところありません。最近ではアクターという新しい型が追加されましたが、Swiftプログラミング言語の公式ドキュメントにはまだ完全には反映されていません。一応、アクターにもプロトコルは適用できるようですが、いろいろと制約がありそうです。これについてはまだ把握しきれていないので、調べた上で理解できたらお話ししようと思います。
とりあえず、クラス、列挙型、構造体にプロトコルを適用できると捉えておけば、今の段階では問題ないでしょう。
では、そもそもプロトコルとは何かというと、他の言語を経験している人ならインターフェースと捉えるとイメージしやすいかもしれません。C++をやっている人にとっては抽象クラスと捉えてもいいでしょう。若干、自由度や仕組み上の違いはありますが、それと捉えても構いません。基本的には、プロトコルは実装を持ちませんが、デフォルト実装を持つことも可能です。 ただ、純粋なプロトコルキーワードで宣言するものはインターフェースだけに限られます。そして、宣言したプロパティやメソッドというものは、プロトコルを適用した方で実装することが義務付けられるのがプロトコルの基本的な約束ごとです。これによって、そのプロトコルに準拠している型は、そのプロトコルが規定する機能、要は性質を持つということが約束されます。
このため、どんな型であってもそのプロトコルに準拠しているのであれば、「こういう操作ができるでしょう」という形でジェネリクスの発想につながっていきます。これはとても大事な機能です。したがって、ジェネリクスの自由度を高める上でプロトコルはとても大事な存在になってきます。ここが、プロトコルの本質的なポイントです。
では、プロトコルの宣言にいく前に練習問題を見てみましょう。とても初歩的な練習問題が用意されています。さっきの
Example Protocol
に新たな要求を追加し、それを準拠するクラスにどんな変更が必要になるかを見てみましょうという例題です。
まずプロトコルの宣言を全部コードで持ってきてみましょう。こんな感じになります。
protocol ExampleProtocol { var simpleDescription: String { get } mutating func adjust() }
このプロトコル
ExampleProtocol
simpleDescription
adjust
という関数を要求しています。これを適用して、実際に
simpleDescription
adjust
の機能を実装します。他にプロトコルに規定されていない機能も自由に追加して良いのですが、基本的には以下のような感じに用意します。
class SimpleClass: ExampleProtocol { var simpleDescription: String = "A simple class." func adjust() { simpleDescription += " Now adjusted." } }
こうするとインスタンスを作ったときに、
simpleDescription
を使えることが保証されます。例えば、ジェネリクスとパラメーターと
getDescription
を使った形式になります。あまり Swift でこういうメソッドを作らない気もしますが、型は何でもいいです。
func getDescription<P: ExampleProtocol>(_ instance: P) -> String { return instance.simpleDescription }
このように、プロトコルに準拠したものをパラメーターとして取り、プロトコルで規定されている機能を使うことができます。これを前提に機能を組み上げていけるのがプロトコルの役割です。インターフェースとして共通認識を持たせる役割もあります。
話がずれて研修の話をすると、研修課題を用意するというのはプロトコルを規定するのと似ているかもしれません。プロトコルで規定して、研修をクリアしたというプロトコルをその人に適用すると、そのプロトコルで規定されている機能をその人が持っているということになります。資格試験なども同様で、その資格を持つ人は最低の知識を満たしていると規定されることと同じです。
さて、練習問題に戻りますが、このプロトコルに新しい要求を追加してみましょう。どんな要求を持たせるか考えることがポイントです。例えば、イニシャライザーをプロトコルで要求してみます。
protocol ExampleProtocol { var simpleDescription: String { get } mutating func adjust() init(simpleDescription: String) }
これで、
ExampleProtocol
はイニシャライザーとして
simpleDescription
を取るものも用意しなければなりません。現在の
SimpleClass
はこの要求を満たしていないので、コンパイルエラーが発生します。それを修正してみましょう。
class SimpleClass: ExampleProtocol { var simpleDescription: String required init(simpleDescription: String) { self.simpleDescription = simpleDescription } func adjust() { simpleDescription += " Now adjusted." } }
こうすることで、プロトコルの要求を満たし、コンパイルも正常に通るようになります。最初はうまくいかないこともありますが、試行錯誤しながら進めることが大切です。 とりあえず定通りのものを備えてあげれば、これでちゃんと機能するよ、という感じです。これでオッケーですね。ちょっと変なところを訂正しましたけど、要はシンプルに適切なインターフェースを搭載してあげれば、プロトコルの要求を満たせた、という感じです。そのイニシャライザーを使ってプログラムを組んでいける、みたいな感じです。
ここから話がクラス継承の話になってくるので、クラス継承のときに話した内容を思い出して心の準備をしてもらえば、より分かりやすく聞けるかなと思います。プロトコルでイニシャライザーを要求した場合、その要求にクラスの実装で応える、という話ですね。
これがクラス継承になったとき、
final
じゃなくクラス継承を加味しないといけない、
final
じゃないクラスの場合、プロトコルでイニシャライザーが規定されているときに、そのクラスでプロトコルに準拠させようとすると、イニシャライザーには
required
を付けないといけない、というエラーが出ます。
required
って「要求する」みたいな意味の言葉ですよね。プロトコルで要求するものを宣言するのと、
required
というキーワードが似ているなと思って、ちょっと紹介した程度のものです。
この
required
って何なんだろうな、と思ったときには、プロトコルみたいにそのイニシャライザーがあることを約束しないといけないから
required
が付いている、という発想です。例えば、サブクラスを規定してサンプル、あ、エキザンプルじゃなくてシンプルなクラスを提唱したときに、このシンプルクラスはエキザンプルプロトコルに準拠しているから、必ずこのイニシャライザーを持たなければならない。
しかし、クラスのイニシャライザーは以前の説明の通り、プレゼンティブイニシャライザーをサブクラスで提示してしまうと、親クラスのイニシャライザーを隠蔽するという性質がありましたよね。その性質によって、シンプルディスクリプションを取るイニシャライザーが隠蔽される可能性が出てきます。それを避けるために
required
イニシャライザーという機能があり、サブクラスでも必ずこの要求を満たさなければいけない、というものです。
具体的な例で言うと、シンプルクラスが例のプロトコルに準拠していて、このイニシャライザーを必ず提供しなければならない、といった場合に、その性格がサブクラスで失われてしまうことを避けるために
required
イニシャライザーがあります。これはプロトコルに限った話ではなく、クラスの中でも同様です。書かれた
required init
は提唱先のサブクラスでもそのインターフェースを必ず備えなければなりません。それによって、仮にサブクラスが親クラスのイニシャライザーを隠蔽する行為をしたとしても、
required init
については必ず用意しないといけません。
プロトコルでイニシャライザーを宣言することと、実質同じことを
required init
で行っています。この辺りを頭の中で関連付けておくと、
required init
という言葉が出てきたときに混乱せずに済むかなと思います。
ついでにややこしい話をしましたが、そんな感じです。
スライドを1個前に戻ってみましょう。プロトコルはプロトコルを使って宣言するプロトコルを適用可能です。ここまでで話した通り、プロトコルの中で規定したものはそれを適用したクラスで実装することを義務付ける、という話をしました。でも、それを必ず実装しなくてもいいようにする手法がSwiftには2つ用意されています。その辺りを紹介しておこうかなと思います。
まず、例えばイニシャライザーですね。2つのプロトコルが規定されていて、例えば
adjust
という機能がプロトコルに規定されているとします。この中で、特に
adjust
が必要ない場合、実装しなくてもいい、みたいな考え方です。こういったときに考えられる方法としては、規定しないというシンプルな方法もありますが、共通認識として例のプロトコルに準拠したものに
adjust
が含まれているということを示すために、エクステンションを使って
Example Protocol
に対して
func adjust
という規定の実装を添えてあげる方法があります。この中で、たとえば
adjust
であまり意味のあることは書かないですがシンプルディスクリプションに空文字を設定する、みたいなことが考えられます。
なお、プロパティの読み取り専用ではなく、書き込みもできるようにしないといけない場合もありますが、これはアジャストの内部実装に寄ります。例えばこんな感じです。 とりあえず、
adjust
というものを付けて、それをセルフで代入して、ミューテーションが必要になります。こういうふうに自分自身を書き換えるためには、
adjust
が呼ばれたら
simpleDescription
を空にするというような、何らかの調整のための機能を実装する必要があります。
こうすると、わざわざ
SimpleClass
のほうで
adjust
というものを用意しなくても、プロトコルに準拠しているとみなされます。というか、そもそも要求は
simpleDescription
のみですが、
adjust
という振る舞いもできるようになります。
たとえば、要求通りに
simpleDescription
に準拠したインスタンスが、プロトコルに準拠した形でインスタンス化できる。このようにインスタンスが自然と
adjust
という振る舞いもできるようになるのです。ただし、ミューテーションが必要なので
var
にしないといけません。このようにして、要求はしないが
adjust
みたいな機能を持つ、実質的にオプショナルに近い動きをするものをプロトコルエクステンションで実現できます。
もう少しオプショナルっぽくするには、要求としても
mutating
な関数として
adjust
を要求し、エクステンションでそのインターフェースに対する実装を提供します。これにより、要求はしていますが、その要求に応えなければ規定の実装を採用するという形になります。これによって、プロトコルに準拠したクラスや構造体は
adjust
を実装しなくても、ちゃんとプロトコル準拠したとみなされ、実際に
adjust
を使うことができるようになります。
また、こうすることでオプショナルっぽい動きを示すことができます。要求を明記した場合、Swiftの世界ではカスタマイズポイントを提供したという意味合いになります。カスタマイズポイントが提供されれば、その実装が使われますし、そうでなければ規定の実装が使われるという発想です。
例えば、
adjust
を読んだときに、
protocol
という文字列を設定するのであれば、
SimpleClass
のほうでは
simpleDescription
adjust
の中で自分でカスタマイズした場合、
simpleDescription
"Protocol"
となります。このように、
adjust
をした後のインスタンスの
simpleDescription
がその状況に合わせて変わります。
このような考え方に基づいて、Swiftでのプロトコルエクステンションの使用例を実際に見ていくことで、オプショナルな機能の実装方法を理解することができます。 例えば、
ExampleProtocol
型にシンプルクラスのインスタンスを入れたとします。このとき、出力が「プラス」と表示されるのです。これがプロトコルにカスタマイズポイントとして規定されていなかった場合、表示が「プロトコル」に変わります。そして、
ExampleProtocol
型ではなくシンプルクラス型だった場合には、出力が「クラス」と表示されます。
このような動きはややこしいかもしれませんが、カスタマイズポイントを搭載することによって、シンプルクラスのときに「プラス」と表示されていたものが、
ExampleProtocol
型でも「プラス」と表示されます。この動きがカスタマイズポイントの重要なポイントとなります。つまり、カスタマイズポイントがあることによって、汎用的な型で包んだときに、しっかりとカスタマイズした実装が呼ばれるのです。実装していなかったときには規定の実装が呼ばれ、実装していたときにはカスタマイズした実装が呼ばれるというわけです。
汎用的な型として扱うためには、カスタマイズポイントをしっかりと記述することが重要です。この感覚はとても大事です。
存在型でこのテーマを少し触れましたが、ジェネリクスでも同様です。任意の型に対して
ExampleProtocol
を適用する場合、以下のような実装にするとしましょう。
func someFunction<V: ExampleProtocol>(value: V) -> String { return value.simpleDescription }
このとき、カスタマイズポイントが存在する状態、つまり、3行目にカスタマイズポイントがある状態でインスタンスを作り、そのインスタンスに対してアクションを行うと、カスタマイズされた内容が出力されます。しかし、カスタマイズポイントを外した場合、出力が「プロトコル」になるでしょう。このように、カスタマイズポイントの有無によって動作が大きく異なるのです。
プロトコル型はあまり使うことがないかもしれませんが、ジェネリクスはよく使います。そのため、カスタマイズポイントの有無によって呼ばれるメソッドが異なる点に注意が必要です。
もう一つお話ししたいのは、プロトコルのエクステンションに別の関数がある場合の話です。例えば、プロキシメソッドを用意し、その中で自分自身のアジャストを呼ぶとします。このプロキシメソッドを用意し、以下のようにします。
extension ExampleProtocol { func proxyMethod() { self.adjust() } }
これを使うときも、同じ状況が発生するはずです。ジェネリクスを通さずに、
ExampleProtocol
型としてエンティティを扱い、そのインスタンスに対してプロキシを呼び出し、シンプルディスクリプションを表示するようにすると、カスタマイズポイントが無い状態では「プロトコル」と表示されるはずです。
カスタマイズポイントがある場合、プロキシを介してアジャストを呼んだときにも、ちゃんとカスタマイズされた実装が呼ばれます。これは非常に重要です。なぜなら、規定の実装をふんだんに使用したプロトコルを作成したときに、自分自身の規定の実装を使って別の実装を作成することが普通に行われます。この場合、カスタマイズポイントがあるかどうかによって、実際に動作する機能が差し替わるためです。
カスタマイズポイントが無い状態で
ExampleProtocol
ではなくシンプルクラス型を使った場合、プロキシを呼び出し、シンプルディスクリプションが「プラス」になるでしょうか。それとも「プロトコル」になるでしょうか。
実行すると、出力は「プロトコル」となります。シンプルクラスでは、アジャストをカスタマイズして「プラス」を表示するようにしているにもかかわらず、プロキシを通して一度「プラス」から「プロトコル」の世界に行き、そこからアジャストを呼び出してカスタマイズが反映されないという動きを見せます。これが非常に重要なポイントです。 このカスタマイズポイント、ちょっと脱線しましたが、オプショナル的な機能を搭載するときには、規定の実装だけではなくて、カスタマイズポイントをしっかり搭載することが重要だと思います。
もう一つのオプショナル的な機能を搭載する方法としては、Foundationの機能、特にObjective-Cの機能を使う方法があります。
import Foundation
を行った上で、
@objc
プロトコルをつけると、
mutating
が使えなくなりますが、これは要は参照型を想定したプロトコルになるからです。構造体に対してObjective-Cプロトコルを準拠させようとすると、構造体だからできないと言われます。つまり、
@objc
をつけた時点で
AnyObject
に対するプロトコルになります。
@objc
をつけることで、オプショナルな機能を指定することができるようになります。その場合、特に実装を用意しなくてもプロトコルに準拠することができたと認識されます。これが純粋なオプショナル的な機能要求です。具体例として、オプションにしたメソッドがどうなるのか気になってくると思いますが、この場合、メソッドがオプショナルであるため、オプショナルチェイニングを使ってそのメソッドがある場合にのみそれを使うという呼び方ができます。
この時、例えば
adjust?()
のように、メソッドがオプショナルであることを示すために
?
を使用します。この方法で、メソッドが実際に存在するかどうかを確認できます。実際にそのメソッドが存在する場合は実行され、存在しない場合は
nil
が返ってきます。これにより、メソッドのオプショナルな存在を柔軟に扱うことができます。
さらに、
@objc
をつけると、戻り値がオプショナルで包まれて返ってきます。つまり、オプショナルで定義したメソッドやプロパティの戻り値は、実装されていれば実行され、実装されていなければ
nil
が返ってきます。これはまさしくメッセージパッシング方式のオブジェクト指向的なメソッド呼び出しとなります。
このオプショナルな性格により、実行できたかどうかをランタイム上で
!= nil
で判定することができます。Cocoaフレームワークなどで積極的に活用されているのが、このオプショナルメソッドの機能です。特にデリゲートパターンでは頻繁に用いられます。
いろいろとお話しましたが、時間が参りましたので、今日の勉強会はこれで終わりにします。次回は年をまたいでの開催となりますので、よろしくお願いいたします。ご視聴ありがとうございました。