Page icon

第63回

2022/01/05
A Swift Tour
Empty
Empty
Empty
Empty
12 more properties
今回は、前回に見てきた A Swift Tour の プロトコルと拡張 の続きを眺めていきます。
前回の話の中で出てきたところにもフォーカスした内容になっていそうですけれど、だいぶ日が空いて忘れているところもありそうなので、改めてゆっくりじっくり勘を取り戻すみたいな気持ちで見ていってみようと思ってます。どうぞよろしくお願いしますね。
——————————————————————————————————————————————————————— 熊谷さんのやさしい Swift 勉強会 #63
00:00 開始 00:46 前回のおさらい 02:52 プロトコルにおける mutating の扱い 03:23 mutating 04:44 プロトコルと構造体とクラスにおける mutating の違い 07:07 mutating を使ってみる 07:34 プロトコルの基本 09:02 mutating が必要な箇所 11:22 プロトコルにおける mutating の必要性 14:52 プロトコル拡張における mutating の存在意義 18:05 参照型専用のプロトコル 19:34 プロトコルを
class
から継承する場面 19:48 クラス専用のプロトコルでは mutating が不要になる 21:15 プロトコルにおける弱参照の扱い 23:36 デリゲート 26:06 プロトコルは参照の仕方に関与しない 27:39 弱参照をプロトコルの中で語りたい場面は? 31:27 弱参照であることより、それが nil になる可能性にフォーカス 32:01 プロトコルが参照方法に関与していたとすると? 32:28 カプセル化 32:59 計算型プロパティーと weak 34:07 isKnownUniquelyReferenced 36:41 計算型プロパティーで保存型プロパティーを隠蔽した際に外側へ参照性を伝える目的 38:40 保持されずに即消滅する場面での警告に期待 41:25 内部では外側より特化した型として扱いたい場面 45:08 即解放されることの注意喚起と、コーディングミスによる意図しない強参照を招く可能性との、トレードオフ 49:03 クロージング ———————————————————————————————————————————————————————
Transcription & Summarize : 熊谷さんのやさしい Swift 勉強会 #63
はい、じゃあ行きますね。今日はプロトコルと拡張についてお話します。前回も同じ内容について話しましたが、どれくらい経ったんでしたっけ? 10回以上は開いたのかな。少なくとも自分はすっかり前に話したことを忘れていて、どこまで話したのかあまり覚えていません。なので、ざっくりと感覚を取り戻しつつ振り返りながら、新しい内容へ進んでいきましょう。
前回確認したところですが、Swiftのプロトコルについての基本的な概念をおさらいしましょう。プロトコルは
protocol
というキーワードで宣言され、クラス、列挙型、構造体に適用されます。プロトコルで義務付けたインターフェースを、その適用先が実装する義務が生じるのがプロトコルの一番シンプルな機能です。おさらいはこのくらいにして、次は今日の新しいお話に入っていきましょう。
今日の話題は「ミューテーティング(mutating)」というキーワードについて詳しく見ていきます。このキーワードについては前回もさっと紹介しましたが、ミューテーティングが何を意味するか、実際に適用するときにどのように書くのかについて詳しく見ていきます。練習問題もやりましたね。では、今日の新しいところに進みましょう。
プロトコルの基本はシンプルですが、このシンプルさから広がる世界を理解することが、Swiftを使いこなすためのステップアップになるかどうかの境目かもしれません。まずはプロトコルにおけるミューテーティングの扱いについてゆっくり見ていきましょう。
まずはプロトコルの定義からざっくりとおさらいしておきます。この
mutating func
という宣言を見てみましょう。今回は
adjust
というメソッドがあります。これはどういう意味かというと、構造体の場合を思い出すとわかりやすいですが、このインターフェースは自分自身の値を書き換えることを示しています。そのために
mutating
が付いています。構造体の場合、
let
で受けたインスタンスでは
mutating
が付けられた機能を使えず、
var
で受けたインスタンスに限って
mutating
の機能が使えるようになります。プロトコルでも同様に、
mutating
が付けられたメソッドは構造体の場合のみ適用されます。
ここで注目すべきは、プロトコルにおける
mutating
の宣言によって、そのプロトコルが適用される型が構造体である場合、
adjust
メソッドを実装するときに
mutating
キーワードが必須となることです。逆にクラスの場合は、自分自身のプロパティを書き換えることが、
let
で受けようと
var
で受けようと自由に可能です。そのため、クラスのメソッドでは
mutating
キーワードは必要ありませんし、Swiftの仕様としてクラスのメソッドには
mutating
キーワードが存在しません。
まとめると、
mutating
は値型(特に構造体)におけるメソッドが自身の値を書き換えることを示すキーワードであり、クラスでは自動的に自分自身を書き換えることが可能なため、
mutating
キーワードは必要ないということです。 なので、プロトコルの方でミューテーティングという名義がされていたとしても、クラスの方ではそういったものをわざわざ付けなくても定義可能で、その中でプロトコルが要求しているとおり、自分自身を書き換えるという振る舞いを問題なく遂行できます。
言葉で説明するだけだと分かりにくいと思うので、このミューテーティング周りをプレイグラウンドでいろいろ見ていってみましょうか。
まず基本的なところとしてプロトコル。何がいいかな...何でもいいんですけど、自分自身を書き換える
modify
みたいな関数を規定したとします。このままだとこれが自分自身を書き換えるのかどうかが分かりません。分からないとなると、大きい方を取りますよね。つまり、読み取り専用ではなくて読み書き可能を取ります。ただ、これは汎用的に考えただけの話でしたね。安全性を考えると、読み書きするよりも読みだけを取った方がいい、という感じですから、ちょっと間違えましたね。小さい方を取っていくっていう感じです。それで、安全性を高めていくという感覚でいるといいのかな、とりあえず適応していきますかね。
ただ、これだけだと自分自身の値を書き換えるという性質は示せません。例えば、プロパティが何かあって、これを
modify
で書き換えるとします。このままだとSwiftのイミュータブル性(不変性)保護機能が働いてできませんから、
mutating
を付ける必要があります。
mutating func modify() { // プロパティの書き換え }
クラスの場合は、例えば以下のように定義できます。
class SomeClass { var property: Int = 0 func modify() { self.property = 1 } }
これで特に何も意識しなくても、代入ができて問題ありません。これによって、
modify
が自分自身を書き換えているということになります。クラスの場合は
mutating
というキーワードなしでもプロトコルが求める自分自身を書き換える
modify
メソッドを実装できたということになります。これはクラスと構造体における
mutating
が指定されたメソッドやAPIの準拠の特徴の違いを示しています。
わざわざ
mutating
があってもなくてもいいんじゃないかとも感じるかもしれませんが、例えばプロトコルにおいてそのAPIが自分自身を書き換えるか否かに関与しないという発想もあるかもしれません。しかし、Swiftではそういうやり方をとっていません。そして、もしこのプロトコルを具体的な型として扱うとき、プロトコルにミューテーティングとノンミューテーティングの区別がなかったとすると
let
で受けようが
var
で受けようが、そのミュータブル性の制御ができなくなります。
例えば以下のように:
protocol SomeProtocol { mutating func modify() } struct SomeStruct: SomeProtocol { var property: Int = 0 mutating func modify() { property = 1 } }
let
で受けたときに
modify
が呼び出せるか、
var
で受けたときに
modify
が呼び出せるか。このように制御がきかない場合、読み書きもできないし読みもできるようにするしかなくなります。
しかし、プロトコルに対しても構造体のように
mutating
という区別を設計しておけば、
var
で受けたときには
mutating
関数が呼べますし、
let
で受けたときには自分自身を書き換えない、つまり内容を保護するために呼べなくなる。このような区別ができるわけです。
存在型で表現しましたが、これジェネリクスについても同じことが言えます。 とりあえずPに準拠していると、その値を受け取ることができます。このときに、Pの中のミューテーティングメソッドが呼べるか否かが影響してきます。このプロトコルがミューテーティングという性質を持つか否かは、ジェネリックプログラミングにおいて非常に重要です。この概念はジェネリックスだけでなく、プロトコル指向プログラミングにも関連します。プロトコル指向という言葉は広範囲にわたるため、いくつかの異なるイメージを持つかもしれませんが、その中でもプロトコルエクステンションについて考えると、ミューテーティングという価値観があることによって、自分自身を書き換えるかどうかの制御が必要になります。
たとえば、モディファイメソッドの実装を行うとき、ミューテーティングがあれば自分自身の何かを書き換える状態になります。プロトコルのインターフェースに、例えば値型として
Int
型があり、読み書き可能なプロパティー(ゲットとセット)が宣言されている場合、自分自身が持っている値を更新することが可能になります。これにより、プロトコルという抽象的な世界の中でも、このようなプログラミングが可能になります。
一方、ミューテーティングを持たないモディファイメソッドがあった場合、それを否定の実装として追加すると、当然のように書き換えることができなくなります。この場合、値に対して何かを入れようとするとエラーになります。この点は、構造体でプログラミングを行っている場合と同じです。構造体と同様に、ミュータブル性とイミュータブル性の制御がプロトコルだけでも完結するというのは大きな特徴です。こういった特徴がプロトコルに備わっているのです。
さて、プロトコルには他にもいろいろな価値観が用意されています。以前にもお話した内容ですが、プロトコルが参照型限定なのか、それとも参照型と値型の両方に適用できるものなのかを表現する方法として、
AnyObject
というプロトコルがあります。
AnyObject
はタイプエイリアスとして隠ぺいされており、具体的にどの型であるかは書いてありません。ただし、
AnyObject
をコンクリートタイプとして使うと、Objective-Cのメソッドがいろいろと実行できます。また、プロトコルとして使うときには、クラスだけに適用できるプロトコルとなります。かつては
class
という表記も可能でしたが、現在は警告が出ます。
AnyObject
を使うことで、プロトコルPはクラスに適用されることになります。これによって面白いエラーが発生する場合がありますが、クラスになるとミューテーティングが不要になるため、エラーが示すとおりミューテーティングを取り除く必要があります。この制約の中で期待どおりに動作します。例えば、プロトコルエクステンションのモディファイメソッドが自分自身を書き換えることも可能になります。このように、この制約の中でクラスに適用するプロトコルも動作することが確認できます。 プロトコルが変幻自在なところとクラスと構造体の特徴の違いについて説明します。この部分、Swiftにおけるプロトコルの柔軟性とクラスと構造体の特性の違いについて話しているのかなと思います。
まず、参照系プロトコルで強参照と弱参照の違いについて考えてみましょう。例えばプロトコル
P
を作成して、プロパティを
var p: Int { get }
として定義したとします。この時点ではごく普通のプロトコルですが、ここでプロパティをオプショナルにして
weak var
を付けるとどうなるか見てみます。
protocol P { var p: Int? { get } }
しかし、プロトコルの宣言内でプロパティに
weak
をつけることはできないとされています。具体的には、このような弱参照をプロトコルのプロパティで宣言することはできません。強参照なのか弱参照なのかを指定する部分は、プロトコルそのものではなくプロトコルを適用するクラスや構造体で決定されるのです。
例えば、次のようにクラスにプロトコルを適用するとします。
class MyClass: P { weak var delegate: P? init(delegate: P?) { self.delegate = delegate } }
このように、
weak
プロパティはプロトコルではなく、そのプロトコルを実装する具体的なクラスで指定しています。このため、プロトコル自体は強参照なのか弱参照なのかに関与しません。
また、ミューテート(変更)に関連する部分はプロトコルを使う際に明確に扱われる一方で、
weak
のようなプロパティの参照タイプに関する部分はプロトコルでは直接的に扱われない点が興味深いです。
例えば、UITableViewにUITableViewDelegateやUITableViewDataSourceがあるのと同じように、あるクラスに
weak
参照でデリゲートを設定するといったシチュエーションでは、プロトコル自体は関与しません。Swiftの標準ライブラリでも、デリゲートプロパティは通常
weak
として定義されますが、それは実装クラスの役割です。
プロトコルを使用する際の
weak
などの弱参照の設定はプロトコルに組み込むのではなく、実際のクラスで管理するという設計原則は、プロトコルの柔軟性とシンプルさを保つための工夫といえるでしょう。
このようなデリゲートを使用するときに、プロトコルが強参照か弱参照かについては実装の詳細に依存し、プロトコル自体はその参照タイプに関与しないという設計が一般的です。このため、プロトコルを使って開発する際には、その柔軟性を活かしつつ、必要に応じて実装クラスで適切な参照タイプを指定することが重要です。
最後に、
weak
なプロパティは参照がなくなるとnilになるため、オプショナル型である必要があります。この点も考慮に入れて設計することが大切です。 個人的には、ウィークは循環参照を防ぐためのものなので、プロトコルがウィークに対応していないのは、参照の持ち方に関与しないということを表しているのかもしれないですね。ただ、ウィークを付けない場合でも、オプショナルを返すようにすればいいこともあると思います。デリゲートの場合でも、必ずしも強参照で持つ必要はなく、デリゲートコントローラーが強参照を持つことが設計によってはあり得ます。プロトコルは、あくまでゲッターセッターやメソッドを定義するもので、参照の持ち方は分離しましょうという考え方だと思います。
たとえば、プロトコル
P
に準拠したコントローラーが、バーバリューのゲッターセッターを持つ場合、ウィークを付けても問題ありません。持ち方は任せるけど、ゲッターセッターはちゃんと返してくださいね、ということです。しかし、ウィークを付けた場合、インスタンスはオプショナルじゃないと無理です。なので、その考え方としては分離するのが良いかもしれません。
実際の実装の際には、ウィークは必要に応じて適用できます。プロトコルとしては、ストアドプロパティに対してウィークを付けるかどうかは開発者に任せるという立場ですね。
具体的には、プロトコルにストアドプロパティでもコンピューテッドプロパティでも良いからゲットできるようにしたい。しかし、ストアドプロパティに対してウィークを付けるかどうかは開発者の裁量に任せる、という感じです。
ところで、K3型プロパティーまでスフィアに入ってくると、設計が広がっていきますね。重要なのは、参照形態よりも、それがミューテートできるかどうかという点にフォーカスすることです。
他に弱参照や強参照がプロトコルにある場合、何が起こるかを考えてみますが、基本的にはウィークや強参照がプロトコルに大きく影響することはなさそうです。K3型もありますので、必要に応じて隠蔽する場合もあります。
また、ストアドプロパティではなくても良い、つまりオブジェクトとして返す必要はない場合もあるということですね。K3型プロパティーでカプセル化することも可能です。
確かにK3型プロパティーにウィークを付ける必要はないでしょう。リードオンリーの場合、意味がありませんし、ウィークオプショナルが必要になるかもしれません。
全体として、プロトコルは参照の持ち方に関与しないという設計上の考え方があるということですね。 じゃあ、ちょっと待ってください。そうですね、意味がないってことですね。これは何ですか。この24行目と25行目の瞬間あたりでnilになる可能性があるってことですか。三条が解放されたとしたら、どうなんですかね。どうですかね。そうか、可能性はありますね。
いや、でも違いますよ。ここで
newValue
を取っていますよね。そうですね。オブジェクトは16行目なので関係なくて、フンッとする側がどう思っているかを知りたい。では、例えば
newValue
が取っていて、ここでプリントで
newValue
の…あ、違う違う、アンメリックリ独立する、アンメリックリ独立する、なんで書けんのか。んーと、兄弟と…。あ、ここでもう
RED
で取っちゃってるのか。イミュータブルで取っていますね。うん、ああ、じゃあ尚更わかったじゃないですか。
セッターに値が届いた時点で元のインスタンスが一回
RED
のプロパティーに受けられて入ってきてるから、カウンターは1増えてるはずですね。そうすると、このセットの中で突然消滅するっていうことはなさそうですね。そうすると意味がないっていう結論になってくる気がするんです。そうですね。
これに意味を見いだせる方っていらっしゃるんですかね。一般的にはプロパティーに
weak
があるっていう普通のケースはないですよね。あ、ですね。Stack Overflowで何か書いている人がいますね。リンクを…あー、今のところか。まあ、最終的な行が結論で、あまり意味はないけど、カプセル化とかで隠蔽したときに内部的には
weak
で持っている場合、それを外部に伝えるセッターとしては
weak
があったほうが外部に伝えられるからいいよね、みたいな話な気がしますね。
なるほどね。内部的なプライベート
weak var
でアンダーバー
delegate
で持っているのであれば、
delegate
プロパティーも
weak
をつけたほうが分かりやすいよね、みたいな。ただ意味はないよねっていう。なるほど、なるほど。うーん、納得し難いような気はしますね。そうですね、とりあえず意味がちょっと分からないですね。
コンパイラ的には生成されるコードは同じっぽくて、ただ意味的に言えば、
delegate
を隠蔽したい、アンダーバー
delegate
を隠蔽したいときに、明示的につけてもいいかもしれませんね。なるほどね。なんか推しの弱い意見に感じますね。個人的には、無くても良くない?有効にしている理由が、言語仕様として許している理由がそれだけではない気がしますね。
公式見解はどうなっているんだろうな。ゆっくり探してみるか。
公式見解はどうなっているんだろうな…。ゆっくり探してみる価値はありそうですね。たとえば、
delegate
をカスタム
delegate
として
weak
プロパティーに割り当てたときに、参照が保持されないよってコンパイラが警告を出してくれる。なるほど、こういう警告を出してくれるんですね。これは価値がありますね。
若干このためのマーク付けか…。なるほど、この観点では確かに価値がありますね。まあ、まあ価値はあるかもしれませんけど、どうでしょう。個人的にはあまり意味がない気がしますが…。
上の書き方は認められないんですけど、逆ならまだありそうですね。外側に提供する方法としては、
delegate
の型を変えたい場合とか…。まあ、そうですね。内部用の
delegate
、それも使うかもしれませんね。
delegate
って大体参照型、つまりクラスを使うことになるので、サブタイピングを使えばいいですよね。 そうでもないのかな。そうでもないのかな。プロトコル
AnyObject
を継承したプロトコルでもいいですけど、いずれにせよ隠蔽したいときには何とかなりそうな気がします。
個人的にはプロパティをわざわざ分けなくても良いとは思いますが、隠蔽されたタイプをそのまま中で使いたい場合、そのタイプのままである必要がありますので、結局こういう感じになるのかなと思います。
なるほど、なるほど。そういうケースはなかなか出ないと思いますが、他にも方法がありますね。例えばクラスとデリゲートがあって、それで「ベースデリゲート」にしようと考えました。ベースデリゲートがあって......クラス。いや、ベースデリゲートじゃなくて良いですね。デリゲートとアクチュアルデリゲート。デリゲートみたいな。こういうふうに、実際のアクチュアル(実際の)ほうが良いですね。
アクチュアル。これが
AnyObject
とあったときに、別に継承しなくても良かったんですけど、難しい話になっちゃいました。コントローラーがあって、継承していないのを使っちゃいます。デリゲートがあって
AnyObject
があって、これを実際にはアクチュアルデリゲートとして使いたい場合に、今の例だと内部的な保存型プロパティとして、これをアクチュアルデリゲートとして持たせておいて、それでこれを計算型プロパティのゲッターやセッターも同様にして、デリゲートを返すみたいな書き方が一つあります。
もう一つの方法として、安全性が若干失落する気もしますが、デリゲート自体を
AnyObject
として持たせておいて、内部で使うプロパティをプライベートにします。プライベート、ウィークなんかいらなくてバリアブルにして、ここでアクチュアルデリゲートをダイナミックキャストしてアクチュアルデリゲートを返すみたいな書き方もできます。だから、デリゲートをアズアクチュアルデリゲート、こんな感じという方法もありますね。内部ではアクチュアルデリゲートを使っていくと。これで型安全に何かをすることができる、という感じの書き方もなくはないです。
こういう感じですね。隠蔽したいときに、ウィークに直接代入で警告が出るよ、と。ここは一つの説得力がそれなりにある発想ではあります。個人的にすごくこのコードを見て心配に思うのが、ここウィークバーとかになっていて、ここウィークがなかったら恐怖だなとか思うんですけど、どう思いますかね。そんなことじゃないかな。
それが許されているところがなんとなく怖いなという気がします。後々うっかりこのウィークを削ってしまったとき、そうですね、だからやるべきじゃないのかなと。運用ルールに任されてウィークを人間が頑張ってつけないといけない部分でつけたいときですね。隠蔽しなくていいんじゃないかという。
もしこれがコンパイラーが警告を出してくれて、ウィークバーで受けたのをウィークじゃないので受けたら警告とかが出てくれると話は変わるんですけど、どうなんでしょう。これがSomeClassデリゲートになってしまってるから、
AnyObject
に返させてもらって、これで良いですか。ここはコンパイル通るでしょうか。通りますね。通りましたね。ここウィークなくなったとき警告が出てくれたら急に見方が変わりますけど、それはないですね、たぶん。
とにかく、人為的なミス。今このプレイグラウンドで書いてるようなコードを生んでしまう可能性のほうが高い気がします。自分はうっかりインスタンス化されてどこからも保持されていないインスタンスを直接代入して、いきなり空になってしまうというバグよりも、今コードで見てるこちらのバグのほうがすごく怖い気がして、いまいちさっきの理由だけでは使いたくないなという印象です。
ないほうが良いんじゃないかなっていう。この計算型プロパティにウィークはないという環境の中で作業するという方が安全ではないかという感じがなんとなくしますが、なんとも言えないですね。
この問題について、一応プロパティラッパーにすれば解消できそうですけど、なるほど、あーしょうがなさそう。プロパティラッパーも絡んでくるとまた好奇心がそそられますね。プロパティラッパー付きのものにウィークを付けた場合。すごく楽しそうですね。考えたことなかったな。解決もできるけど同様の問題も起こりそうです。
ウィークが付いてるのに確保されてるよみたいな。面白いけど怖いな。ちょっと十分に注意して使わないと、ウィークとプロパティラッパーのセットはもしかすると怖いかもしれないですね。使ったことないですけど、今のところは。なるほど、面白いですね。
はい、じゃあ今日はこれくらいで終わりにしようと思います。お疲れ様でした。ありがとうございました。