ストリーミングはほとんどのブラウザと
Developerアプリで視聴できます。
-
Swiftの新機能
Swiftの新機能を確認しましょう。Parameter packやマクロなどの機能でさらに拡張可能性が高まり表現豊かになったAPIについて学びましょう。また相互運用性の改善点や、Foundationからサーバでの大規模配信プログラムにいたる各所で改善された、Swiftのパフォーマンスや安全面での利点について解説します。
関連する章
- 0:39 - Swift project update
- 2:44 - Using if/else and switch statements as expressions
- 3:52 - Result builders
- 4:53 - type parameter packs
- 9:34 - Swift macros
- 19:47 - Swift foundation
- 23:25 - Ownership
- 27:59 - C++ interoperability
- 32:41 - What's new in Swift Concurrency
- 38:20 - FoundationDB: A case study
リソース
関連ビデオ
WWDC23
- パラメータパックを使ったAPIの一般化
- 構造化並行処理の基本を超えて
- Swift と C++ の同時利用
- Swiftマクロの拡張
- Swiftマクロの書き方
- SwiftDataについて
- SwiftUIの新機能
WWDC21
-
ダウンロード
♪ ♪
こんにちは What's New in Swift 5.9へ ようこそ 私はBenです 同僚のDougと Swift言語の改善点について お話しします また、Swiftのclean syntaxを使って 実装を簡略化する方法や フレームワークデベロッパが 新しいAPIsをより自然に使えるための 非常にパワフルな機能についても紹介します さらにLow Levelのコードにおいて パフォーマンスと安全性を制御する 新しい方法についてお話しします まずはSwift オープンソースプロジェクトの お話からです これはコミュニティーのみなさんによる 素晴らしいアップデートで コントリビューターやSwiftユーザーなしには できなかったことです swift.orgに集まりこの言語を進展させ 新しいイニシアチブを 支持してくれたおかげで実現しています Swiftは言語の進化において オープンプロセスを追求しています 新機能や大きな動作変化は Swift フォーラムで オープンに提案されレビューされます これらの言語提案はすべて ウェブサイトのダッシュボードで ご覧いただけます 1年前Swift Projectの管理で 大きな再構築がありました Language Steering Groupの編成が発表され Swift言語とライブラリ8点についての 主要な責任を担うことになりました それ以来 このグループは 40の新言語提案を管理し そのいくつかを今日取り上げます
しかしまれに個別の言語提案が 一つに統合されより大きなテーマとなります Swift concurrencyの追加は 10の提案で別々に紹介されたものです このような場合のために Language Steering Groupは これらの提案を結ぶ 新しい方法を導入しました それがvision documentsです そのドキュメントは言語の大きめな 変化の提案を構成します このグループが最初に受諾したのが Swift 5.9の新機能である Swift macrosのvisionでした この後でお話しします もちろん言語の発展は Swiftコミュニティの 役割の一部にすぎません 言語の成功にはそれ以上のことが必要です 素晴らしいツールや 多くのプラットフォームのサポート 豊富な資料などが挙げられます それらの発展のため language steering groupと並行して ecosystem steering groupを設立しました この新機構についてはSwift.orgの ブログポストに掲載されています この新グループの編成について さらなる発表を今後予定しています では今年度の Swiftのアップデートからはじめましょう まずコードでの表現を 向上させる方法からです Swift 5.9には 最もリクエストのあった 言語拡張が含まれています if/elseとswitch文を 式として認めることで コードを整理することができます
例えば複雑な状況において let変数を初期化したい場合 このように読みにくい 複合三項式に頼る必要があります
式であれば、より親しみがあり読みやすい if文を使えることになります
これはまた グローバル変数や 格納型プロパティの初期化に役立ちます 1つの式ならいいものの 条件が必要なら 直ちに実行するためには クロージャを使用せねばなりません
しかしif文が式となるなら 余計なものを省けてコードがスッキリします Result buildersやSwiftUIのような 機能を動かす宣言型のsyntaxは 今年 著しい発展が見られます 型検査やコード補完の最適化や エラーメッセージの改善です
この改善は特にInvalidなコードに 焦点を当てています エラーのあるResult Builderコードは 型チェッカーが あらゆるパスを探るため フェイルが返るまで時間がかかりました
Swift 5.8でInvalidコードの 型チェックは速くなり Invalidコードのエラーメッセージは より正確になりました これまでInvalidコードによっては まったく違う箇所の 紛らわしいエラーが 返されることがありました Swift 5.7では このようなエラーが返りますが 実際の間違いはここにあります
最新リリースでは より正確なコンパイラ診断で 問題を確認できます 次にジェネリックシステムの追加で 毎日使用するフレームワークが 大きく改善されます
Swiftで書く場合 たいていジェネリクスを使っています 型推論は高度な機能を理解する必要なく これらの型を使用可能にします 例えば標準ライブラリの配列型は ジェネリクスで 保存したいどのタイプのデータにも対応する 配列を提供します 配列を使うときに必要なのは要素だけです 要素型の引数を特定する必要はありません これは要素値から検出できるからです
Swiftのジェネリックシステムは natural APIsを可能にし 型情報を保存します そのため あなたのコードは 指定した明確な型でスムーズに動作します これはSwiftコンパイラの コードベースからの例です このAPIは強く型付けされた値を定義するために 要求される型を評価します つまりブール値をリクエストでき、 ブールの結果を得られるのです
APIによっては明確な型だけでなく 幾つもの引数を 抽出しようとするものもあります ですので関数は1つのリクエストに 1つの結果 2つのリクエストに2つの結果 3つのリクエストに3つの結果を返します
そのためジェネリックシステムは 幾つもの関数を扱えるメカニズムと 一緒に使用されることによって すべての型に対してそれに相対する型を 結びつけるのです Swift 5.9以前は このパターンの成立に それぞれの引数に対して APIがサポートするオーバーロードを 追加しなければなりませんでした しかしそれには制限があります 定義する引数の数に意図的な上限が 設定されてしまうため 数が多すぎると コンパイルエラーとなります この場合 7つの引数がありますが 6つ以上対応できる オーバーロードがありません このオーバーロードパターンと制限は 概念的に任意引数を扱う API全体に見られます
Swift 5.9でジェネリックシステムは 引数のジェネリック抽象化を可能にし このAPIパターンに対する すばらしいサポートが追加されます これは一緒にパックされた 幾つもの個別型パラメータを表現する 新しい言語コンセプトで可能になります この新しいコンセプトを タイプパラメータパックと呼びます パラメータパックを使い 現在固定引数に対しオーバーロードが それぞれあるAPIは 1つの関数に含められます
1つのリクエストの結果型を意味する Resultを受け入れる代わり それぞれの結果型に対する 個別のリクエストを受け入れられるのです 関数は括弧内に1つの値か 各値を含んだタプルで 各結果インスタンスを戻します これで上限なく すべての引数に対応することができます
型推論はパラメータパックの使用に 気付かぬまま APIを自然に使用できます
限りなく引数に対応できる evaluate functionを呼び出すのは 固定長オーバーロードを 呼び出すようなものです Swiftは関数の呼び方に基づき それぞれの引数の型と 合計数を検出します ジェネリックライブラリAPIについては 「Generalize APIs using parameter packs」をご視聴ください ジェネリックAPIを自然に呼び出すのは 簡潔なコードによる明確な式という Swiftの基本デザインゴールです
Swiftの高度な言語機能により 目的を簡単に伝える 美しいAPIが可能になります
これらの高度な言語機能による恩恵を Swiftで書く一行目から受けられます 配列や辞書型などジェネリクスであれ SwiftUIでのデザインでもです Swiftは段階的な開示を取り入れており 用意ができれば より高度な機能を学べるようになっています
Swift 5.9はマクロシステムを用いて ライブラリ作者に 表現豊かなAPIデザインの 新しいツールボックスを提供し さらにパワーアップさせました ではDougがお話しします マクロで言語自体の可能性を広げられ ボイラープレートを取り除き Swiftの表現豊かなパワーを引き出します では条件がtrueかチェックするときに よく使われるアサート関数を見てみましょう アサートはfalseの場合 プログラムを停止します しかしその場合 ファイルと ラインナンバーしか表示されません ロギングやデバッガを使わねば より詳細な情報を得られません これを向上させる試みはありました XCTestはassert-equal作業で 2つの値を別々にとらえるため 少なくとも2つの違う値を 確認することができます しかしどちらの値が間違いかわかりません 違うのはaかbか それともmaxの結果か この方法はアサートでのすべてのチェックに 向いていません 最初のアサーションを見ると 失敗した時にログにあるべき ソースコードの情報がたくさんあります 何のコードか? aとbとcの値は? maxは何か? カスタム機能なしでは これは向上できませんでした でもマクロで可能になります
この例では#assert構文で assertというマクロが拡張されます 見覚えがあるかもしれないのは Swiftには既に これと同じスペルのものがあるからです #fileや#selectorや#warningがその例です アサートマクロは関数版とよく似ていますが マクロであるため アサーションがフェイルすると よりリッチな体験ができます アサーションに失敗したコードが示され その結果になった各値も示されています
Swiftではマクロは 型や関数のようなAPIなので それを定義するモジュールを インポートしてアクセスします 他のAPI同様 マクロはパッケージとして配給されます このアサートマクロはGitHubにある オープンソースSwiftパッケージの PowerAssertライブラリからのものです
マクロパッケージの内部を見ると アサートのマクロ宣言があります macroのキーワードが付いていますが それ以外は関数とよく似ています 条件をチェックする無ラベルの ブールパラメータがあります このマクロが値を提示すれば その結果型は 従来のArrow Syntaxで書かれます マクロの使用はパラメータに対し 型チェックされます つまり 最大値を比べるのを 忘れるというような マクロ使用上での 間違いを犯した場合 便利なエラーメッセージが マクロの拡張前に 表示されます Swiftでのマクロの使用は 非常に素晴らしい デベロップメント体験を提供します これはマクロはよくある入力で作動し 意外性なくプログラムの 拡張コードを作成するためです たいていのマクロは externalMacroと定義され 文字列でマクロ実行の モジュールと型を明示します externalMacro型は コンパイラプラグインとして振る舞う 別のプログラムで定義されます Swiftコンパイラはマクロ使用の ソースコードをプラグインにパスします プラグインは新しいソースコードを作り Swiftプログラムに再び統合されます ここではマクロが アサーションをコードに拡張し そこで個々の値をとらえ ソースコードに表示しています マクロがあなたのために ボイラープレートを書いてくれます マクロ宣言には もう一つの情報があります ロールです このアサートマクロは freestanding expressionマクロです #構文を使用するため freestandingと呼ばれ 新コードを作るために その構文上で作動します 値を作り出す場所なら どこでも使用できるので expressionマクロです Foundation Predicate APIは expressionマクロのいい例を提供します Predicateマクロはクロージャで タイプセーフに predicatesを書くことができます その結果のpredicate値は Swiftコレクション作業の SwiftUIやSwiftDataなど 他のAPIで使用できます マクロ自体は入力型セットにおいて ジェネリックです 入力型値で作動する関数で 入力セットがマッチするか ブール結果を算出する クロージャ引数を 受け入れます そしてマクロはプログラム内で 使用できるPredicate型の インスタンスを 戻します
しかしマクロにはさらに奥があります ボイラープレートを書くのは 以前書いたコードを そこから引き出したものと 拡張する必要があるからです 例を見てみましょう 私はコード内でenumをよく使っています 相対及び絶対パスをとらえる この列挙型パスががそうです しかし特定のものを チェックすることがよくあります コレクションから絶対パスを フィルターする場合などです もちろんisAbsoluteチェックを 計算型プロパティとして書けます しかし いずれ別のものを 書かねばならなくなるでしょう これは面倒な作業になります
そこでマクロにボイラープレートを 作成させることができます Case detectionはattachedマクロで property wrappersのような カスタム属性構文で書かれています Attachedマクロは この列挙型宣言のように 宣言構文の入力として受け止め コードを作成します
このマクロ拡張コードは 通常のSwiftコードで コンパイラがプログラムに統合させます マクロが作成したコードを 点検してデバッグし さらにカスタム化したいなら コピーすることもできます
Attachedマクロには属する宣言を どう拡張するかに基づき 5つのロールがあります 今話したcase detectionマクロは member attachedマクロで 型や拡張に新しいmembersを作成します Peerマクロは属する宣言と共に 新しい宣言を追加します 非同期メソッドのcompletionHandler版を 作ったりするのがその例です
Accessorマクロは格納型プロパティを 計算型プロパティにします 特定のプロパティアクセスの実行や property wrappersより柔軟な方法で ストレージを抽象化するのです またattachedマクロは 特定のメンバーに属性及び プロトコルconformanceを追加します Attachedマクロロールの幾つかは 役にたつ効果を作るために 一緒に構成することもできます その重要な例の一つがobservationです Observationは常にSwiftUIの一部でした クラスのプロパティの変化を監視するには 型をObservableObjectとして すべてのプロパティを@Publishedとし ViewでObservedObject property wrapperを使用します ステップが多いため1つでも見逃すと インターフェイスは更新されません マクロを使ったObservationで改善ができます
Observableマクロをクラスに付け すべてのプロパティに Observationが提供されます プロパティ毎にアノテーションをつける必要はなく マクロがすべて手配するため 何の心配もする必要がなくなります
Observableマクロは 3つのマクロロール構成を処理します それらのロールがどう働くのか 見てみましょう 各マクロロールはPersonクラスが Observableマクロに拡張されるよう 特定の対応を行います メンバーロールが新しいプロパティと メソッドを導入します
メンバー属性ロールが 監視クラスの格納型プロパティに @ObservationTrackedマクロを足します そしてgettersとsettersに拡張し observation eventsを起こします 最後に適合ロールが Observableプロトコルに 適合を導入します
多くのコードに見えますが すべて普通のSwiftコードで Observableマクロの後ろに 折り込まれているのです
プログラムへの影響を理解するため どうマクロが拡張しているか 見る必要があれば いつでもXcodeで確認できます
を実行し エディタでマクロ拡張ソースコードを 確認できます マクロ作成コード内のエラーメッセージは 自動的に拡張コードを表示し デバッガでコードに 出入りすることができます
Swiftマクロはより表現豊かなAPIを実現し ボイラープレートを除く新ツールを提供し Swiftの表現力の可能性を開きます マクロは入力を型チェックし 普通のSwiftコードを作成し コードの設定された場所で統合させるため その効果は簡単に納得がいきます マクロの動作を理解したければいつでも 拡張ソースコードをエディタで確認できます これは表面的なことに過ぎません 「Expand on Swift macros」では Swiftマクロのデザインについて すべての質問にお答えできるように 説明しています そして「Write Swift macros」で 独自のマクロ開発を することが可能です みなさんがどのようなマクロを作るか 楽しみです
当初からSwiftは拡張性のある言語として デザインされました Swiftのデザインは堅苦しくなく 読み書きしやすい 明白で簡明なコードの 表現性に重点を置いています ジェネリクスや ネイティブコンカレンシーサポートなど Swiftのパワフルな機能を行使することで SwiftUIやSwiftDataのような フレームワークは 素早く 求める結果を得られ 大切なことに焦点を当てる時間を提供します
しかし高度な機能に関わらず Swiftはまた効率的です ネイティブにコンパイルし ガレージコレクションではなく 値型と参照型を使用するため 小さいメモリフットプリントを達成できます
この拡張性により これまでObjective-Cで 可能だった場所だけではなく CやC++を使わねばならない 低レベルのシステムでも Swiftを使用できるということです つまりより多くの場所で Swiftの明白なコードと安全性の担保を 活用することができます つい最近Foundationフレームワークの リライトをオープンソースにしました これによりApple及び そのほかのプラットフォームで 一つのFoundationを 共有履行できることになります またSwiftで大量のObjective-Cと Cコードをリライトすることになります macOS SonomaとiOS 17では DateやCalenderのようなessential typeや LocaleやAttributedStringのような フォーマットと国際化のエッセンシャル またJSONエンコードやデコードが Swiftに新しく実装されています そしてパフォーマンスも向上しています
Calendarが重要な日を計算する能力は 中間アロケーションを避けるため Swiftのvalue semanticsを使用して 20%以上のスピード向上を達成しています FormatStyleを使った日付のフォーマットも 標準日時のテンプレートの フォーマットベンチマークで 150%ものスピード向上を達成しています さらに新パッケージでは JSONデコードにも向上が見られます Foundationには新しく JSONデコーダとJSONEエンコーダを導入し Objective-Cコレクション型への 非効率なラウンドトリップを 取り除くことができます Codableの初期化において JSONのパースをSwiftに統合することも パフォーマンス向上につながります テストデータのパースのベンチマークでは 2~5倍のスピード改善が見られています これらの改善は古いObjective-Cから Swiftへの履行によるブリッジの減少と 新しいSwift基盤の 実行の速さによるものです
それではベンチマークの一つを 例に見てみます Venturaではブリッジを要するため enumerateDatesを呼ぶのは Objective-CがSwiftより 少し速かったのですが MacOS Sonomaでは 同じ関数を呼ぶのに Swiftの方が20%速くなっています ブリッジを省いたこともありますが 新しい関数実行自体が Objective-Cから呼ぶより 速くなったためです この日付計算は特に複雑ではないため 2つの言語間における オーバーヘッドの削減が よくわかるかと思います しかし低レベルのシステムを使用している時 必要な性能を得るために きめ細かい管理が必要になる時があります Swift 5.9では そのような管理を可能にするため 新しいオプトイン機能を追加しています これらの性能は オーナーシップに焦点を当てます つまりアプリをパスする上でコードの どの箇所が値を持つかということです
これらの機能をいつ使うべきか コードの例を見てみましょう 低レベルのシステムコールに Swiftインターフェイスを与える ファイル記述子のための シンプルなwrapperがあります しかしこのAPIでは まだ簡単にミスしてしまいます 例えばcloseしてから ファイルに書いてしまうかもしれません 型がスコープから出る前にcloseメソッドで 常に手動でクローズする必要があります でなければリソースリークになります 解決策の一つは型がスコープから出る時に 自動的にクローズするdeinitをもつ クラスにすることです
しかしこれには欠点があります 通常は問題ありませんが さらなるメモリーアロケーションが必要なため システムに限りがある場合は問題になります
またクラスには 参照セマンティクスがあります 故意なくスレッドを通して ファイル記述子型を共有してしまい 競合状態や期待しない保持に つながるかもしれません
しかし少し戻って構造体を見てみましょう
この構造体も参照型のように作用しています 真の値を参照するintegerを保持しています つまりオープンファイルです この型をコピーすると バグをもたらす可能性があります ミュータブルステートをアプリ中で 共有する可能性があります この構造体のコピーを作る可能性を 防ぐ必要があります
Swift型は構造体であれクラスであれ 既定上ではコピー可能です 通常なら正しい選択です 過度で不必要なコピーは コードのネックになりますが コンパイラがコピーに関し明確にするよう 常にうるさく言われるよりは ときどき、Instruments で ネックを探す方が楽です しかしそのようなコピーが 問題になる場合もあります 値のコピーが正当性の問題に つながる時などです ファイル記述子wrapperがその例です Swift 5.9では structとenum宣言に適用できる 新しい構文があり 型をコピーする暗黙的な機能を 抑えることができます 型がコピーできなくなると クラスの時と同様に deinitを与えて 型値がスコープ外に出た時に作動します
コピーできない型は クローズを呼ぶ問題の解決にも使用でき また、その時は他のメソッドも使えます
クローズオペレーションは consumingとも明記できます consumingメソッドや引数は 呼び出したメソッドの値の オーナーシップを放棄します この型はコピーできないので オーナーシップを放棄すると 値を使用できなくなります
既定上 Swiftのメソッドはselfを含む これらの引数を借りることになります つまりファイル記述子を借りる writeメソッドを呼び バッファーに書き出し その後 値のオーナーシップがcallerに戻り closeのような別メソッドを呼べるのです
しかしcloseは borrowingのデフォルトではなく consumingと明示されているので 最後の使用となります
つまり先にファイルをクローズし writeのような別のメソッドを呼ぶと ランタイムエラーではなくコンパイル時に エラーメッセージを受け取ります またコンパイラはconsumingされた場所を 指摘します
Non-copyable型はSwiftにおける システムレベル・プログラミングの 新機能です まだ進化が始まったばかりですが 後のSwiftリリースでジェネリックコードに 拡張する予定です もしこの検討に興味があれば Swift forumsで活発に議論されています Swiftの成功の鍵は Objective-Cとの共存機能です 当初からデベロッパは 既存のコードベースに ファイルやモジュール毎に Swiftを徐々に取り入れてきました しかしObjective-Cで書かれた コードだけでないのも確かです C++で実装されたコアビジネスロジックを 持つアプリも多くあり そのインターフェイスは 非常に困難でした しばしSwiftからObjective-Cへ そこからC++へ行き逆戻るという 余分な手動ブリッジレイヤーを 足さなければなりませんでした Swift 5.9ではC++型と関数に 直接Swiftから インタラクトする機能を導入します C++インターオペラビリティは Objective-Cインターオペラビリティと同様 C++ APIを Swiftの同等のものにマッピングし Swiftコードで直接使えるようにします C++は大きな言語で クラスやメソッドやコンテナなど 独自の観念を備え持っています Swiftコンパイラは 共通のC++イディオムを理解し 多くの型は直接使用できます 例えばこのPerson型はC++値型にある 5つの特殊メンバー関数を 定義します コピー及びムーブ・コンストラクタ 代入演算子とデストラクタです Swiftコンパイラは値型として扱い 適時に適切な特殊メンバー関数を 自動的に呼び出します さらにベクターやマップなど C++コンテナはSwiftコレクションとして アクセス可能です
これらの結果 C++関数と型を直接使用した 簡単なSwiftコードが書けるということです Personインスタンスのベクターで フィルタをかけ C++メンバー関数を呼び データメンバーに直接アクセスできます
また逆に C++からSwiftコードを使用するのは Objective-Cと同じメカニズムです Swiftコンパイラが Swift API上にC++ビューを含む generated headerを作成します しかしObjective-Cとは違い objc attribute注釈付きの Swiftクラスだけを使用するという 制限はありません C++はプロパティやメソッド イニシャライザなど ブリッジのオーバーヘッドなくして たいていのSwift型と フルAPIを直接使用できます これはC++がPoint構造体を 使用している例です generated headerを含んだあと C++はSwiftイニシャライザで Pointインスタンスを作成し mutating methodsを起動し 格納及び計算型プロパティにアクセスできます Swiftコード自身に変更はありません
SwiftのC++共存機能で Swiftを既存の C++コードベースと 統合させるのが簡単です 多くのC++イディオムが 直接Swift内で表現できます たいてい自動ですが時として 求めるセマンティックを 指摘するアノテーションが必要になります Swift APIはC++から直接アクセスでき アノテーションもコード変更も必要なく CやC++及びObjective-Cを使用する コードベースを通して Swiftを徐々に採用することが 可能になったのです
C++共存機能は C++共存機能ワークグループが ガイドを務める現在進展中のトピックです 詳しくは 「Mix Swift and C++」をご覧になるか Swift forumsにご参加ください
言語レベルでの共存機能は 非常に大切ですが コードを作成できることも大切です Swiftを始めるために 既存のXcodビルドシステムや Swift Package Managerを取り替えることは 大量のコードを書き直すのと同様に 大きなバリアです そこでCMakeと共に CMakeサポートを向上させました Swiftを言語の一つに宣言し ファイルをターゲットに置き SwiftコードをCMakeビルドに 統合できます
さらに重要なのはC++とSwiftを 1つのターゲット内にミックスでき CMakeはそれぞれ別々にコンパイルし 両言語においてサポートする すべての適切なライブラリと ランタイムをリンクします つまりファイルやターゲット毎に クラスプラットフォームC++プロジェクトを 今日からSwiftにて適用することが できるということです またbridgingやgenerated headersなど SwiftとC++/Swiftの混合ターゲットを含む CMakeプロジェクトの サンプルリポジトリを提供し 取り掛かりやすくしています
数年前 async/awaitと structured concurrencyと Actorsのビルディングブロックに基づく 新しいconcurrencyモデルを Swiftに登場させました Swiftのconcurrencyモデルは abstractモデルで 違う環境やライブラリに適用できます Abstractモデルには TasksとActorsがあります Tasksは概念的にどこでも動作する 連続的処理単位を意味します Tasksはプログラムに awaitがあると停止させられ 処理可能になれば続行します
Actorsは同期メカニズムで 孤立状態に相互排他的アクセスを提供します 外部からActorに入るには 処理を中断するため awaitが必要になります
TasksとActorsは abstract言語モデルに統合されており そのモデル内で違う環境に対応するため 違う形で実行されます Tasksはglobal concurrent poolで 実行されます global concurrent poolは環境次第で 処理スケジュールを決定します Appleのプラットフォームでは ディスパッチライブラリが システム全体に 最適化されたスケジュールを提供し プラットフォーム毎に広範囲 に適合されています より制限のある環境では マルチスレッドスケジューラの オーバーヘッドは容認されません その場合 Swiftのconcurrencyモデルは シングルスレッドcooperative queueで 履行されます Abstractモデルは多様なランタイム環境に 対応できる柔軟性があるため 同じSwiftコードが 両環境で作動します
さらにコールバック基盤ライブラリの 共存機能が Swiftのasync/awaitサポートに 当初から備わっています withCheckedContinuation演算で タスクを停止でき コールバックに応答して のちに処理を続行できます これでタスクを処理する既存ライブラリとの 統合が可能になります
Swift concurrencyランタイムでの Actorsの標準実行は actorの実行への Tasksのロックフリーキューですが 可能なのはそれだけではありません 制限された環境ではatomicsがなく スピンロックのような別のconcurrency premitiveを使うかもしれません その環境がシングルスレッドなら 同期化は必要ありませんが actorモデルはプログラムにおいて abstract concurrencyモデルを それに関わらず維持します まだ同じコードをマルチスレッドの 別の環境に持ち込めます Swift 5.9では custom actor executorsが 特定のactorに独自の同期メカニズムの 履行を許可します これによりactorsはより適応性が増し 存環境に順応的になります 例を見てみましょう ここにデータベースコネクションを処理する actorがあります Swiftはこのactorのストレージに 相互排他的アクセスを許し データベースへの同時アクセスはありません しかし同期化がなされる特定方法の制御が さらに必要ならどうでしょう? actorsがない別のコードと キューを共有するため データベースコネクションに 特定のdispatch queueを使用するなら custom actor executorsで可能です
ここでactorに serial dispatch queueと そのdispatch queueに対応する executorを作り出す unowned executor propertyの実行を 追加しました この変更によりactorインスタンスの すべての同期化は そのキューを通して起こります
actor外からpruneOldEntriesのコールを awaitすると それに対応するキューで dispatch-asyncが実行されます これでactorsによる 同期化の提供を制御でき Objective-CかC++で書かれているため まだactorsを使用していない 別のコードとactorの同期化も可能です
dispatch queuesによるactorsの同期化は dispatch queueがSerialExecutor プロトコルに従うため可能です actorsを使うため 独自の同期化メカニズムを使うことも このプロトコルに従う 新しいタイプを定義して可能です これのコア演算は限られています Executorにおいて既にコードが 実行されているかのチェックです 例えば メインスレッドで実行してるのか?
過剰な参照カウントトラフィックなしで 非所有リファレンスを executorに引き出し アクセスを許しています そしてコア演算のenqueueが executor Jobの所有権を取ります Jobはexecutorで同期的に 実行されねばならない非同期タスクです enqueueが呼ばれた時点で serial executorで 他のコードが実行されていない時に jobを実行するのが executorの責任になります 例えばdispatch queueのenqueueが そのqueueでdispatch asyncを呼んでいます
Swift Concurrencyが 使用し始められて数年になり tasksとactorsからなる抽象モデルは concurrent programming tasksの 広範囲をカバーしています 抽象モデル自体が適応性があり iPhoneやApple Watch サーバーに至るまで 様々な処理環境に適応が可能です また重要箇所でカスタマイズでき まだSwift Concurrencyに 適応されていないコードとも 相互運用が可能になります 詳細は「Behind the Scenes」と 「Beyond the basics of Structured Concurrency」をご覧ください 最後にSwiftがiOSやmacOSなど 見慣れた環境以外の 非常に異なった環境で作動する 事例研究を見てみましょう FoundationDBは分散データベースで コモディティハードウェアを運用し macOSやLinuxにWindowsなど 様々なプラットフォームをサポートする 大型のキーバリューストアに スケーラブルなソリューションを提供します FoundationDBはC++の大型コードベース オープンソース・プロジェクトです コードは主に非同期式で テスト目的で極めて重要な 確定的シミュレーション環境を 提供するactorsとランタイムを供給する 独自の方法を備えています コードベースの現代化を模索していた FoundationDBは その性能と安全性やコードの明瞭性から Swiftが最適であると決めました すべてをリライトするのは 大掛かりで危険な試みです その代わりにSwiftの 共存機能を利用して 既存のコードベースに統合させたのです これはFoundationDBのmaster data actorの C++のインプリメンテーションの一部です
非常に複雑ですし C++をすべて理解する必要はありません しかしコードの重要な部分をお見せします まずC++にはasync/awaitがないため FoundationDBにはエミュレートに プレプロセッサのような アプローチがあります
多くのC++コードベースのように 非同期タスクの管理に 独自のC++ Future型を 実行していました リクエストへのレスポンスを送る 2つの明白なメッセージがここにあります 関数からの戻りに対する返答の 慎重なペアリングに注目してください 最後にFoundationDBには メモリーを自動管理する独自の 参照カウンタースマートポインタがあります Swiftではこれらすべてを もっと手際良く実行できます
(安堵の息) 良くなりましたね Swiftではこの関数をasync関数として 直接実行できます このリクエストへの答えを提供するための 普通のreturn型とreturn文があり 同期しないことはありません ほかのSwift非同期コードと同様 停止ポイントを示すawaitがあります このSwift コードは継続を使って適応している C++ Future型と同調しています
ここには多くのC++型があります C++のMasterData型は参照カウンター スマートポインタを使用しています C++で肩に注釈をつけることで Swiftコンパイラは ほかのクラス同様にこの型を使い 自動的に参照カウントを管理します
requestやreply型などのほかの型は Swiftで直接使われるC++値型です そして相互運用性は逆でも同じです この非同期関数や Swift Concurrencyモデルにより 紹介されたすべての作業は FoundationDBの既存の 確定的ランタイムで作動します そのため 段階的な採用により 既存のC++と調和し 必要な箇所でSwiftの恩恵を 受けることができるのです
ここではいろんなトピックをカバーしました より表現的なAPIを可能にし よりよいコードを素早く書ける パラメータパックやマクロなどの 機能についてお話ししました また参照カウントオーバーヘッドなしで リソース管理を提供できる パフォーマンス過敏コードや noncopyable型に触れました SwiftでC++ APIをサポートし コードにSwiftの利点を簡単にもたらす C++の共存機能についてお話ししました
最後に並行処理を楽に安全にするため デバイスや言語を通して Swiftの柔軟並行処理モデルが さまざまな環境にどのように適応するかお話ししました 言語パラメータパックやマクロに non-copyable型と Swift 5.9でのすべての言語機能は Swift Evolution プロセスを通し オープンにデザイン・開発され それにはコミュニティーの意見が 重要な役割を果たしました Swift 5.9は活発なデザイン討論や バグレポート プルリクエストや教育コンテンツなど Swiftコミュニティの限りない貢献の 極みです Swift 5.9が最高のリリースとなり みなさんに感謝しています ♪ ♪
-
-
3:06 - Hard-to-read compound ternary expression
let bullet = isRoot && (count == 0 || !willExpand) ? "" : count == 0 ? "- " : maxDepth <= 0 ? "▹ " : "▿ "
-
3:19 - Familiar and readable chain of if statements
let bullet = if isRoot && (count == 0 || !willExpand) { "" } else if count == 0 { "- " } else if maxDepth <= 0 { "▹ " } else { "▿ " }
-
3:30 - Initializing a global variable or stored property
let attributedName = AttributedString(markdown: displayName)
-
3:46 - In 5.9, if statements can be an expression
let attributedName = if let displayName, !displayName.isEmpty { AttributedString(markdown: displayName) } else { "Untitled" }
-
4:31 - In Swift 5.7, errors may appear in a different place
struct ContentView: View { enum Destination { case one, two } var body: some View { List { NavigationLink(value: .one) { // The issue actually occurs here Text("one") } NavigationLink(value: .two) { Text("two") } }.navigationDestination(for: Destination.self) { $0.view // Error occurs here in 5.7 } } }
-
4:47 - In Swift 5.9, you now receive a more accurate compiler diagnostic
struct ContentView: View { enum Destination { case one, two } var body: some View { List { NavigationLink(value: .one) { //In 5.9, Errors provide a more accurate diagnostic Text("one") } NavigationLink(value: .two) { Text("two") } }.navigationDestination(for: Destination.self) { $0.view // Error occurs here in 5.7 } } }
-
5:47 - An API that takes a request type and evaluates it to produce a strongly typed value
struct Request<Result> { ... } struct RequestEvaluator { func evaluate<Result>(_ request: Request<Result>) -> Result } func evaluate(_ request: Request<Bool>) -> Bool { return RequestEvaluator().evaluate(request) }
-
6:03 - APIs that abstract over concrete types and varying number of arguments
let value = RequestEvaluator().evaluate(request) let (x, y) = RequestEvaluator().evaluate(r1, r2) let (x, y, z) = RequestEvaluator().evaluate(r1, r2, r3)
-
6:35 - Writing multiple overloads for the evaluate function
func evaluate<Result>(_:) -> (Result) func evaluate<R1, R2>(_:_:) -> (R1, R2) func evaluate<R1, R2, R3>(_:_:_:) -> (R1, R2, R3) func evaluate<R1, R2, R3, R4>(_:_:_:_:)-> (R1, R2, R3, R4) func evaluate<R1, R2, R3, R4, R5>(_:_:_:_:_:) -> (R1, R2, R3, R4, R5) func evaluate<R1, R2, R3, R4, R5, R6>(_:_:_:_:_:_:) -> (R1, R2, R3, R4, R5, R6)
-
6:47 - Overloads create an arbitrary upper bound for the number of arguments
//This will cause a compiler error "Extra argument in call" let results = evaluator.evaluate(r1, r2, r3, r4, r5, r6, r7)
-
7:12 - Individual type parameter
<each Result>
-
7:36 - Collapsing the same set of overloads into one single evaluate function
func evaluate<each Result>(_: repeat Request<each Result>) -> (repeat each Result)
-
8:21 - Calling updated evaluate function looks identical to calling an overload
struct Request<Result> { ... } struct RequestEvaluator { func evaluate<each Result>(_: repeat Request<each Result>) -> (repeat each Result) } let results = RequestEvaluator.evaluate(r1, r2, r3)
-
10:01 - It isn't clear why an assert function fails
assert(max(a, b) == c)
-
10:20 - XCTest provides an assert-equal operation
XCAssertEqual(max(a, b), c) //XCTAssertEqual failed: ("10") is not equal to ("17")
-
11:02 - Assert as a macro
#assert(max(a, b) == c)
-
11:42 - Macros are distributed as packages
import PowerAssert #assert(max(a, b) == c)
-
12:07 - Macro declaration for assert
public macro assert(_ condition: Bool)
-
12:26 - Uses are type checked against the parameters
import PowerAssert #assert(max(a, b)) //Type 'Int' cannot be a used as a boolean; test for '!= 0' instead
-
12:52 - A macro definition
public macro assert(_ condition: Bool) = #externalMacro( module: “PowerAssertPlugin”, type: “PowerAssertMacro" )
-
13:11 - Swift compiler passes the source code for the use of the macro
#assert(a == b)
-
13:14 - Compiler plugin produces new source code, which is integrated back into the Swift program
PowerAssert.Assertion( "#assert(a == b)" ) { $0.capture(a, column: 8) == $0.capture(b, column: 13) }
-
13:33 - Macro declarations include roles
// Freestanding macro roles @freestanding(expression) public macro assert(_ condition: Bool) = #externalMacro( module: “PowerAssertPlugin”, type: “PowerAssertMacro" )
-
13:53 - New Foundation Predicate APIs uses a `@freestanding(expression)` macro role
let pred = #Predicate<Person> { $0.favoriteColor == .blue } let blueLovers = people.filter(pred)
-
14:14 - Predicate expression macro
// Predicate expression macro @freestanding(expression) public macro Predicate<each Input>( _ body: (repeat each Input) -> Bool ) -> Predicate<repeat each Input>
-
14:48 - Example of a commonly used enum
enum Path { case relative(String) case absolute(String) }
-
15:01 - Checking a specific case, like when filtering all absolute paths
let absPaths = paths.filter { $0.isAbsolute }
-
15:09 - Write an `isAbsolute` check as a computer property...
extension Path { var isAbsolute: Bool { if case .absolute = self { true } else { false } } }
-
15:12 - ...And another for `isRelative`
extension Path { var isRelative: Bool { if case .relative = self { true } else { false } } }
-
15:17 - Augmenting the enum with an attached macro
@CaseDetection enum Path { case relative(String) case absolute(String) } let absPaths = paths.filter { $0.isAbsolute }
-
15:36 - Macro-expanded code is normal Swift code
enum Path { case relative(String) case absolute(String) //Expanded @CaseDetection macro integrated into the program. var isAbsolute: Bool { if case .absolute = self { true } else { false } } var isRelative: Bool { if case .relative = self { true } else { false } } }
-
16:57 - Observation in SwiftUI prior to 5.9
// Observation in SwiftUI final class Person: ObservableObject { @Published var name: String @Published var age: Int @Published var isFavorite: Bool } struct ContentView: View { @ObservedObject var person: Person var body: some View { Text("Hello, \(person.name)") } }
-
17:25 - Observation now
// Observation in SwiftUI @Observable final class Person { var name: String var age: Int var isFavorite: Bool } struct ContentView: View { var person: Person var body: some View { Text("Hello, \(person.name)") } }
-
17:42 - Observable macro works with 3 macro roles
@attached(member, names: ...) @attached(memberAttribute) @attached(conformance) public macro Observable() = #externalMacro(...).
-
17:52 - Unexpanded macro
@Observable final class Person { var name: String var age: Int var isFavorite: Bool }
-
18:05 - Expanded member attribute role
@Observable final class Person { var name: String var age: Int var isFavorite: Bool internal let _$observationRegistrar = ObservationRegistrar<Person>() internal func access<Member>( keyPath: KeyPath<Person, Member> ) { _$observationRegistrar.access(self, keyPath: keyPath) } internal func withMutation<Member, T>( keyPath: KeyPath<Person, Member>, _ mutation: () throws -> T ) rethrows -> T { try _$observationRegistrar.withMutation(of: self, keyPath: keyPath, mutation) } }
-
18:12 - Member attribute role adds `@ObservationTracked` to stored properties
@Observable final class Person { @ObservationTracked var name: String @ObservationTracked var age: Int @ObservationTracked var isFavorite: Bool internal let _$observationRegistrar = ObservationRegistrar<Person>() internal func access<Member>( keyPath: KeyPath<Person, Member> ) { _$observationRegistrar.access(self, keyPath: keyPath) } internal func withMutation<Member, T>( keyPath: KeyPath<Person, Member>, _ mutation: () throws -> T ) rethrows -> T { try _$observationRegistrar.withMutation(of: self, keyPath: keyPath, mutation) } }
-
18:16 - The @ObservationTracked macro adds getters and setters to stored properties
@Observable final class Person { @ObservationTracked var name: String { get { … } set { … } } @ObservationTracked var age: Int { get { … } set { … } } @ObservationTracked var isFavorite: Bool { get { … } set { … } } internal let _$observationRegistrar = ObservationRegistrar<Person>() internal func access<Member>( keyPath: KeyPath<Person, Member> ) { _$observationRegistrar.access(self, keyPath: keyPath) } internal func withMutation<Member, T>( keyPath: KeyPath<Person, Member>, _ mutation: () throws -> T ) rethrows -> T { try _$observationRegistrar.withMutation(of: self, keyPath: keyPath, mutation) } }
-
18:33 - All that Swift code is folded away in the @Observable macro
@Observable final class Person { var name: String var age: Int var isFavorite: Bool }
-
23:59 - A wrapper for a file descriptor
struct FileDescriptor { private var fd: CInt init(descriptor: CInt) { self.fd = descriptor } func write(buffer: [UInt8]) throws { let written = buffer.withUnsafeBufferPointer { Darwin.write(fd, $0.baseAddress, $0.count) } // ... } func close() { Darwin.close(fd) } }
-
24:30 - The same FileDescriptor wrapper as a class
class FileDescriptor { private var fd: CInt init(descriptor: CInt) { self.fd = descriptor } func write(buffer: [UInt8]) throws { let written = buffer.withUnsafeBufferPointer { Darwin.write(fd, $0.baseAddress, $0.count) } // ... } func close() { Darwin.close(fd) } deinit { self.close(fd) } }
-
25:05 - Going back to the struct
struct FileDescriptor { private var fd: CInt init(descriptor: CInt) { self.fd = descriptor } func write(buffer: [UInt8]) throws { let written = buffer.withUnsafeBufferPointer { Darwin.write(fd, $0.baseAddress, $0.count) } // ... } func close() { Darwin.close(fd) } }
-
26:06 - Using Copyable in the FileDescriptor struct
struct FileDescriptor: ~Copyable { private var fd: CInt init(descriptor: CInt) { self.fd = descriptor } func write(buffer: [UInt8]) throws { let written = buffer.withUnsafeBufferPointer { Darwin.write(fd, $0.baseAddress, $0.count) } // ... } func close() { Darwin.close(fd) } deinit { Darwin.close(fd) } }
-
26:35 - `close()` can also be marked as consuming
struct FileDescriptor { private var fd: CInt init(descriptor: CInt) { self.fd = descriptor } func write(buffer: [UInt8]) throws { let written = buffer.withUnsafeBufferPointer { Darwin.write(fd, $0.baseAddress, $0.count) } // ... } consuming func close() { Darwin.close(fd) } deinit { Darwin.close(fd) } }
-
26:53 - When `close()` is called, it must be the final use
let file = FileDescriptor(fd: descriptor) file.write(buffer: data) file.close()
-
27:20 - Compiler errors instead of runtime failures
let file = FileDescriptor(fd: descriptor) file.close() // Compiler will indicate where the consuming use is file.write(buffer: data) // Compiler error: 'file' used after consuming
-
28:52 - Using C++ from Swift
// Person.h struct Person { Person(const Person &); Person(Person &&); Person &operator=(const Person &); Person &operator=(Person &&); ~Person(); std::string name; unsigned getAge() const; }; std::vector<Person> everyone(); // Client.swift func greetAdults() { for person in everyone().filter { $0.getAge() >= 18 } { print("Hello, \(person.name)!") } }
-
29:51 - Using Swift from C++
// Geometry.swift struct LabeledPoint { var x = 0.0, y = 0.0 var label: String = “origin” mutating func moveBy(x deltaX: Double, y deltaY: Double) { … } var magnitude: Double { … } } // C++ client #include <Geometry-Swift.h> void test() { Point origin = Point() Point unit = Point::init(1.0, 1.0, “unit”) unit.moveBy(2, -2) std::cout << unit.label << “ moved to “ << unit.magnitude() << std::endl; }
-
35:30 - An actor that manages a database connection
// Custom actor executors actor MyConnection { private var database: UnsafeMutablePointer<sqlite3> init(filename: String) throws { … } func pruneOldEntries() { … } func fetchEntry<Entry>(named: String, type: Entry.Type) -> Entry? { … } } await connection.pruneOldEntries()
-
35:58 - MyConnection with a serial dispatch queue and a custom executor
actor MyConnection { private var database: UnsafeMutablePointer<sqlite3> private let queue: DispatchSerialQueue nonisolated var unownedExecutor: UnownedSerialExecutor { queue.asUnownedSerialExecutor() } init(filename: String, queue: DispatchSerialQueue) throws { … } func pruneOldEntries() { … } func fetchEntry<Entry>(named: String, type: Entry.Type) -> Entry? { … } } await connection.pruneOldEntries()
-
36:44 - Dispatch queues conform to SerialExecutor protocol
// Executor protocols protocol Executor: AnyObject, Sendable { func enqueue(_ job: consuming ExecutorJob) } protocol SerialExecutor: Executor { func asUnownedSerialExecutor() -> UnownedSerialExecutor func isSameExclusiveExecutionContext(other executor: Self) -> Bool } extension DispatchSerialQueue: SerialExecutor { … }
-
39:22 - C++ implementation of FoundationDB's "master data" actor
// C++ implementation of FoundationDB’s “master data” actor ACTOR Future<Void> getVersion(Reference<MasterData> self, GetCommitVersionRequest req) { state std::map<UID, CommitProxyVersionReplies>::iterator proxyItr = self->lastCommitProxyVersionReplies.find(req.requestingProxy); ++self->getCommitVersionRequests; if (proxyItr == self->lastCommitProxyVersionReplies.end()) { req.reply.send(Never()); return Void(); } wait(proxyItr->second.latestRequestNum.whenAtLeast(req.requestNum - 1)); auto itr = proxyItr->second.replies.find(req.requestNum); if (itr != proxyItr->second.replies.end()) { req.reply.send(itr->second); return Void(); } // ... }
-
40:18 - Swift implementation of FoundationDB's "master data" actor
// Swift implementation of FoundationDB’s “master data” actor func getVersion( myself: MasterData, req: GetCommitVersionRequest ) async -> GetCommitVersionReply? { myself.getCommitVersionRequests += 1 guard let lastVersionReplies = lastCommitProxyVersionReplies[req.requestingProxy] else { return nil } // ... var latestRequestNum = try await lastVersionReplies.latestRequestNum .atLeast(VersionMetricHandle.ValueType(req.requestNum - UInt64(1))) if let lastReply = lastVersionReplies.replies[req.requestNum] { return lastReply } }
-
-
特定のトピックをお探しの場合は、上にトピックを入力すると、関連するトピックにすばやく移動できます。
クエリの送信中にエラーが発生しました。インターネット接続を確認して、もう一度お試しください。