Page icon

第277回

2023/07/21
Empty
時事的な話題
Empty
Empty
Empty
Empty
Empty
12 more properties
今回も引き続き、個人的にお気に入りな技術ブログ「その Swift コード、こう書き換えてみないか」にある 
型拡張
 まわりの小技的な話題を見ていきますね。前回はその基本的なところやプロトコル適合についてのあたりを眺めていきましたけれど、今回はそこからもう少し細かく型拡張が作用するところをおさらいしていく予定です。よろしくお願いしますね。
———————————————————————————————————————————— 熊谷さんのやさしい Swift 勉強会 #277
00:00 開始 00:50 API の宣言だけを確認する方法 05:00 デリゲートに準拠するときにも型拡張は便利 07:22 本体と型拡張では、プロトコル適用時の独立性に違いあり 13:02 中断する関数は、中断しない関数で代用可能 15:21 型拡張で添えた機能には Actor 独立性が暗黙継承 18:55 型拡張全体に Actor 独立を指定可能 19:50 型拡張と Actor 独立性のおさらい 23:51 Swift UI に見る、プロトコルに連動して Actor 独立性を持つ事例 25:05 View も型拡張で独立して適用するべき? 27:19 型拡張ならではの特徴、ほかにある? 27:42 条件付きプロトコル準拠 30:28 型拡張とイニシャライザーの自動実装 32:22 直付けか、後付けか 33:11 クロージング ————————————————————————————————————————————
Transcription & Summarize : 熊谷さんのやさしい Swift 勉強会 #277
はい、では始めますね。今日も引き続き、Swiftコードを書き換えるという内容で進めていきます。今回は前回の続きで、アクションをプロトコルごとに作り、その準拠のための実装は出来る限りその中のみで行うという話です。ポイントは、できる限りその中のみで行うという点にあります。
最初に、アクションが増えすぎて大丈夫かという質問が上がりました。基本的には大丈夫だと思いますが、プロトコル準拠が分散し過ぎるのが気になります。そこで、このインターフェースの出し方を探したところ、便利な方法が見つかりました。それを紹介します。
まず、「プレイグラウンド」ではなく、普通のソースコードのキーワードを使う方が良いようです。コード上で右端のナビゲータリリティで「デメリティインターフェイス」などをクリックすると、ソースコードのインターフェイスだけが表示されます。プロトコル準拠やインターフェイスだけの情報が見やすくなるので便利です。ただし、コメントが多いと見やすさが損なわれることもあります。
例えば、
つぶそう
というメソッドがあって、それをクリックすると、一番端っこのナビゲータリリティで「デメリティインターフェイス」を選ぶと、インターフェイスだけが表示されます。コメントが多いと見にくいですが、余計な実装を気にせずにインターフェイスだけを見ることができます。コメントフォルティングを調整すると、見やすくなる場合もあります。
また、オプションキーを押しながらクリックすると、横にインターフェイスを確認しながらコードを書くことができます。この機能を使うと、コードとインターフェイスの確認が同時にできるので便利です。
これでプロトコルごとにエクステンションを作ると、後から見やすくなると思います。次回もこのテクニックを活用していきましょう。 デリゲートを搭載する際には、結構効果的です。例えば、
NSObjec
tから派生したクラスでエクステンションとして実装するといった場合です。具体的には、
NSTableViewDelegate
のようなものです。このような場合、要求される実装を一つのところに混ぜていくと、メリハリがなくなりがちです。
そのため、デリゲートに関するコードを分けてあげて、「ここがデリゲート関連の部分ですよ」と明示する方が良いです。デリゲート関係のコードが多くなりすぎた場合や、何らかの事情で別のオブジェクトにデリゲートの実装を移したい場合、必要な部分だけを切り替えて持っていけるのは便利です。
確かに、デリゲートを切り出す利点はありますが、それだけでなく、デリゲートがどのように応答するのかを読むのにも役立ちます。これだけでなく、他にもメリットがあるので、プロポーザルSE-0316の例を見てみましょう。このプロポーザルは何かを確認しておきましょう。プロポーザルSE-0316は「プロパーティアクター」についてのもので、さらに「メインアクター」を自分で作成するという話になります。 この中にコード例があるらしい「プロトコルP」があって、この中にメインアクターのファンクション(メインスレッド)が存在します。ファンクションがあり、「
X
」というデータがある場合、それを「
P
」に準拠させると、ファンクション「
F
」は暗黙的にメインアクターに準拠します。他方、「
Y
」を「
P
」に準拠させてエクステンションを作成し、ファンクション「
F
」を実装する場合、このとき「F」はメインアクターに暗黙的に準拠しません。
通常、このような書き方で処理しているため、あまり意識していませんでしたが、試してみましょう。ここで「メイン」ではなく「プロトコルP」を使います。このように実行するとエラーになるでしょう。「ファンクションF」を呼び出すために、コード内で例えば「
Y
」を実行しましょう。「
let y = Y()
」とし、「
y.F()
」で実行します。「X」はそのまま「X」で、これが正常に動作する場合、メインアクターで動くようにはなっていないということです。
このコードがプレイグラウンドで動作しているかどうかコンパイラに確認してみます。ビルドしてみると、「プロトコルP」に準拠していないように見えます。コンパイルオプションの設定を確認し、プロジェクトのビルドセッティングで「コンカレンシー」オプションが出てくるか調べます。「ミニマル」ではなく「コンプリート」に設定するとどうなるでしょうか。その上で再度ビルドをかけてみます。
ビルドが通りません。メインアクターを使用しているためかと思われますが、「
async
」を付けたコードが適切に動作していないようです。コード内で「メインアクター」を一度削除して確認してみます。すると、ビルドが通るようになりました。
目的としては「X」と「Y」のどちらもプロトコル準拠の動作を確認するためです。「Y」でコンパイルオプションを取り、実行すると動作しました。プロトコルで非同期処理を要求していてもコンパイルできる場合、それで問題ありません。メインアクターが必要な状況において、それでも動作するなら、それで構わないのです。
アウェイトやタスクの使用はここで重要です。「タスク」を使用し、一度バックグラウンドに処理を渡す場合、コード内の「async」や「タスクawait」が正しく機能するか確認します。コード内で「プロトコル準拠」が正しく動作しているかチェックし、「await」が必要な場面でエラーが出ることを確認します。
このように、メインアクターに暗黙的に準拠している場合と、していない場合の動作の違いが見えてきます。メインアクターに準拠しない場合でも、インターフェースが問題なく機能することが確認できました。これは特定の場面で用意されている機能や特徴がどのように動作するかを確認するための一つの例です。 それでは、続けて解説します。まず、関数
F
がメインアクターの中で実行されるためには、
F
を定義する構造体やクラスがメインアクターとして指定されている必要があります。しかし、
F
が他のプロトコルに準拠している場合、そのプロトコルがメインアクターを要求していないと、
F
は必ずしもメインアクターの環境で実行されない可能性があります。
例えば、プロトコル
P
に準拠した構造体
X
があり、その中でメソッド
F
を定義するとします。この場合、
F
の実行環境について明示的な指定がなければ、
F
がメインアクターのコンテキストで実行される保証はありません。
結果的に、以下のようなコードがあるとします:
protocol P { func F() } struct X: P { // `F` の実装 func F() { print("Executed in X") } } // エクステンションを利用して `P` に準拠させる場合 extension P { func F() { print("Executed in P extension") } }
このとき、構造体
X
の中で
F
を実行しても、エクステンションで実装された
F
が呼び出されるとは限りません。構造体
Y
においても同様のアプローチを取ると、同じように扱われます。これらの構造体がどのアクターコンテキストで実行されるかを理解することは重要です。
また、
SwiftUI
View
プロトコルにおいても、同様の概念が適用されます。
body
メソッドが
MainActor
上で実装される場合、
View
を適用した際に問題なく動作しますが、
MainActor
の外で実装すると、動作が異なる可能性があります。
例えば、以下のようになります:
struct ContentView: View { var body: some View { Text("Hello, world!") .padding() } } @MainActor extension ContentView { var body: some View { Text("Hello, MainActor!") .padding() } }
このように
MainActor
コンテキストを明示することで、必要なコンテキストで実行されることが保証されます。
最後に、
View
の準拠についても
extension
を利用して外部で定義することは可能です。これにより、適切なアクターコンテキストを確保しつつ、コードの分離を図ることができます。個人的な感覚やプロジェクトのスタイルによって、どちらの方法が最適かを判断することが求められるでしょう。
以上、プロトコルとエクステンションに関連する
MainActor
の扱い方について解説しました。このような知識を活用して、より堅牢で明確なコードを書けるよう努めてください。 これについては、普通に「これはメインアクターだよ」という対処策もあります。どの方法が良いかはどれでも良いかなという気がしますが、まとめるなり何かの事情で分離するのであれば、こういう理由があるかもしれません。内容はいろいろありますね。OKです。
これで一応、エクステンションのお話は終わりました。エクステンション特有の特徴みたいなものってありますかね。プロトコルボットと書いてありますが、それだけではなくても良いです。アクターとアクセスコントロール、型準拠、条件付き型準拠は当然エクステンションを使いますよね。
例えば、このビュー、コンテンツビューがエクステンションとして
extension ContentView
これは例えば、ある具体的なケースに準拠する場合ですが、例えば、ボディが何か特定の条件を満たすときなどに適用されます。これを条件付きプロトコル準拠(コンディショナルコンフォーマンス)と言います。
ここで、ジェネリックなデータ型を使用する場合、例えば、
Body
がジェネリックである必要があります。非ジェネリックな部分は、条件を満たすときにのみ実装されます。オブジェクトの中で
Value
が何らかの型に準拠している時には、こうした書き方が自然です。
条件付きプロトコル準拠により、エクステンションが一定のグループを作っているということがわかります。エクステンション特有の動きとして、プロトコル準拠とは関係ありませんが、トラックや値型があって、普通に実装すると、構造体の特徴としてメンバーワイズイニシャライザーが自動で搭載されるというのがあります。
構造体の初期値がしっかり決まっている場合にはデフォルトイニシャライザーも標準で搭載されます。ただし、独自の指定イニシャライザーを取った場合には、これらのイニシャライザーは存在しなくなります。しかし、エクステンションの場合は全く問題ありません。そのため、エクステンションを使っても基本的に問題なくコンパイルが通りますし、イニシャライザーも追加されます。
エクステンションと本体の定義は若干異なり、後から追加されるものや制約が異なります。それを見るとエクステンションの面白さが感じられるかもしれません。制約がいろいろ出てくるのは主にクラスですが、構造体を使っている場合、エクステンションを積極的に使っても問題ないでしょう。
時間になったので、この辺で終わりにしましょう。お疲れ様でした。ありがとうございました。