Swift & Objective-C

[Swift深掘り #7] Swiftマクロ総まとめ、@Observableの正体

Swift 5.9のマクロは、コンパイラに組み込まれるコード生成プラグインです。構文木を受け取ってコードを返す仕組み、freestanding(#)とattached(@)の2種類、自作時のコストをまとめます。

読了 8 分
[Swift深掘り #7] Swiftマクロ総まとめ、@Observableの正体のカバー画像

プロパティラッパー編の終わりに、「@が付いていてもすべてがラッパーとは限らない」と伏線を張りました。その主役が@Observableです。

前回の記事Swift深掘り #6から続く内容です。

見た目はラッパーですが、正体はマクロです。Xcodeで#Previewを使うときも、SwiftDataの@Modelを使うときも、私たちはすでにマクロの利用者です。

今回の深掘りでは、Swift 5.9で導入されたマクロとは何か、どんな問題を解決し、どんなコストが発生するのかを整理します。SE-0382: Expression Macros

マクロが解決する問題 — ボイラープレート最後の砦

Swiftは繰り返しコードを減らす仕組みを着実に追加してきました。プロトコルのデフォルト実装、Codableの自動合成、プロパティラッパーです。

それでも手を付けられない領域が残っていました。型の構造を読み取り、それに合ったコードを生成することです。

Observationはその好例です。プロパティの変更を監視者に通知するには、すべての保存プロパティに追跡コードが必要です。

@Publishedのようなラッパーを使うと、プロパティごとに@を付ける必要があり、付け忘れたプロパティは静かに監視対象から外れました。

必要なのは、「このクラスのすべての保存プロパティに、それぞれの名前に合ったコードを生成して」です。これは型の構造を読む必要があるため、ラッパーの能力を超えています。

従来、この領域は2つの方法が担っていました。ランタイムコストと不透明さを伴うObjective-Cランタイムの動的機構か、ビルドパイプライン外で別途管理するSourceryのような外部コード生成ツールです。

マクロはこの処理を言語の中へ、しかもコンパイル時へ持ち込む仕組みです。

仕組み — コンパイラに組み込まれるコード生成プラグイン

Swiftマクロの正体はコンパイラプラグインです。Cプリプロセッサのテキスト置換とは根本的に異なります。処理の流れは次のとおりです。

  1. コンパイラがソース中でマクロの使用(#または@)に出会うと、そのコードの構文木(AST)をマクロ実装に渡します。
  2. マクロ実装は別プロセスで実行されるSwiftプログラムです。SwiftSyntaxライブラリで木を解析し、新しいコード片を作って返します。
  3. コンパイラは結果を元の位置に組み込み、その後は通常どおりコンパイルします。

この構造から、重要な性質が生まれます。

1つ目は、結果が検査可能な本物のSwiftコードであることです。Xcodeでマクロを右クリックしてExpand Macroを選べば、生成コードをそのまま展開できます。

いつでも魔法の幕を上げられるという点は、Progressive Disclosure編で見た「隠すが、曖昧にはしない」のマクロ版です。

2つ目は、衛生的(hygienic)であることです。マクロが作る変数は周囲のコードの名前と衝突しないよう管理され、宣言された役割の範囲外のコードを変更することもできません。

Cマクロで悪名高い副作用を、設計段階で防いでいるのです。

3つ目はサンドボックスです。マクロプロセスはファイルやネットワークにアクセスできないため、コンパイル時に任意コードを実行するセキュリティホールにはなりません。

構文木をサンドボックス化されたプラグインに渡し、生成コードを受け取る4段階の図
構文木を受け取ってコードを返し、結果はExpand Macroで検査できます

2つの系統 — freestanding(#)とattached(@)

マクロは、付加される方法によって2系統に分かれます。

freestandingマクロ(#)は、その場所にコードを生成します。 #Preview { MyView() }はプレビュー登録の構造を作ります。

#URL("https://apple.com")のようなマクロは、コンパイル時に文字列を検証してアンラップ済みのURLを生成します。式を置く場所に現れるマクロです。

**attachedマクロ(@)は、付加された宣言を拡張します。**役割はさらに細分化されています。

memberはメンバーを追加し、accessorはプロパティにアクセサーを付け、extensionはプロトコル準拠を追加します。1つのマクロが複数の役割を兼ねることもあります。

@Observableはまさにこの組み合わせです。クラスの各保存プロパティに監視追跡用アクセサーを追加し(accessor)、登録用ストレージメンバーを追加し(member)、さらにObservableプロトコルへの準拠を追加します(extension)。

プロパティごとに@を付けていた@Published時代の不便が、型に@を1つ付けるだけになった理由です。

この区別が分かると、ライブラリのドキュメントが読みやすくなります。SwiftDataの@Model、テストフレームワークの#expect(Swift Testing編で扱ったマクロ)、Composable Architectureの@Reducerを思い出してください。

最近のエコシステムを代表するAPIは、すべてこの2系統のどちらかです。

コストの請求書 — 作るのか、使うだけなのか

マクロには明確なコストがあり、利用者と作り手では負担の内容が異なります。

**利用者側のコストは主にビルド時間です。**マクロ実装はSwiftSyntaxに依存しますが、これは大きなライブラリです。

そのため、マクロを使うパッケージを初めてビルドすると、SwiftSyntaxのコンパイルが丸ごと加わります。

CIのクリーンビルドが数分単位で延びるケースは珍しくなく、コミュニティではプリビルドバイナリの配布などの緩和策が改良され続けています。

それでも利用者にとっては、多くの場合、受け入れられるコストです。Expand Macroで検査でき、ランタイムコストもないからです。

**作り手側のコストははるかに大きくなります。**マクロ作成は「コードを扱うコード」なので、難易度が一段上がります。

SwiftSyntax APIの学習、入力コードと期待する出力コードを比較するマクロ専用テスト、診断メッセージの設計まで必要です。

実務では保守的な基準を採用すべきです。プロトコルのデフォルト実装、ジェネリクス、ラッパーで解決できる問題なら、まずそちらを使います。

「型の構造を読まなければならない繰り返し」がチームのコードベース全体に広く存在する場合だけ、自作マクロが候補になります。YAGNI(You Aren’t Gonna Need It — 必要になるまで作らない原則)のマクロ版です。

多くのチームにとって、マクロは作るための道具ではなく、フレームワークが提供するものを正しく理解して使うための道具です。

#はその場所にコードを生成し、@は宣言を拡張する、2系統のマクロを描いたイラスト
#はその場所に生成し、@は付加された宣言を拡張します

まとめ

  • マクロは、コンパイル時に構文木を受け取ってコードを生成するコンパイラプラグインです(Swift 5.9)。テキスト置換を行うCマクロとは異なり、型の構造を読み取り、衛生的かつサンドボックス内で動作します。
  • freestanding(#)はその場所にコードを生成し(#Preview)、attached(@)は付加された宣言を拡張します(@Observable、@Model)。
  • 生成結果はExpand Macroでいつでも検査できる本物のSwiftコードです。@が付いていてもすべてがラッパーではないので、これからはドキュメント中のmacroという単語を区別して読めます。
  • コストは、利用者側ではSwiftSyntaxによるビルド時間、作り手側では高い実装難易度です。自作は「型の構造を読まなければならない広範な繰り返し」が確認できた場合だけにしましょう。SE-0389: Attached Macros

次回は、SwiftがRustの領域から取り入れた概念を扱います。コピーが禁止された型~Copyableと、borrowingとconsumingが開く所有権の世界です。

出典と確認基準

  • SE-0389: Attached Macros — Swift Evolution · 標準・仕様の原文 · 確認 2026-08-17 · 根拠:attached macroの役割と生成可能な宣言
  • SE-0382: Expression Macros — Swift Evolution · 標準・仕様の原文 · 確認 2026-08-17 · 根拠:Swift 5.9のexpression macroモデルとコンパイラプラグイン

あわせて読みたい