Swift & Objective-C

[Swift哲学 #1] Safe・Fast・Expressive

Swiftでコードを書いていると、ふと疑問に思う瞬間があります。なぜオプショナルはこれほど厳格なのか、なぜ配列はコピーされるのか、なぜguardがあるのか。こうした疑問を掘り下げていくと、Swiftが最初から掲げていた3つの目標、Safe(安全)、Fast(高速)、Expressive(表現力)に行き着きます。

読了 10 分
[Swift哲学 #1] Safe・Fast・Expressiveのカバー画像

Swiftでコードを書いていると、ふと疑問に思う瞬間があります。なぜオプショナルはこれほど厳格なのか、なぜ配列はコピーされるのか、なぜguardがあるのか。こうした疑問を掘り下げていくと、Swiftが最初から掲げていた3つの目標、Safe(安全)、Fast(高速)、Expressive(表現力)に行き着きます。

Swift公式サイト(swift.org)は、言語紹介の冒頭でこの3つを明示しています。単なるマーケティング文句ではありません。過去10年余りにSwiftへ追加されたほぼすべての機能が、この3つの基準で評価されてきました。本稿では、それぞれの哲学がどの言語機能として実装され、衝突したときSwiftがどちらを選んだのかをまとめます。

この記事はSwift哲学シリーズの第1回です。Swiftが生まれた背景、つまりChris LattnerがなぜObjective-Cを捨てたのかは別の記事で扱ったため、ここでは「生まれた後、どの原則で成長したのか」に焦点を当てます。

Safe — コンパイラがバグを未然に防ぐ

Swiftの設計で最も優先度が高い価値は安全性です。ここでいう安全とは「間違いにくくする」ことです。プログラマーの善意や集中力に頼らず、言語レベルでミスへの道を塞ぎます。

代表例がオプショナルです。Objective-Cではどのポインタもnilになり得て、nilにメッセージを送っても静かに無視されました。クラッシュはしませんが、バグの起点を追うのは困難でした。C系言語ではそのままクラッシュします。null参照を「10億ドルの過ち」と呼んだTony Hoareの話は有名です。

Swiftはこの問題を型システムに組み込みました。値が存在しない可能性のある変数は、型に疑問符を付けてString?のように宣言する必要があり、疑問符のない型は決してnilになりません。nilになり得る値を使うには、if letやguard letでアンラップする手続きをコンパイラが強制します。

var name: String? = fetchUserName()

// コンパイルエラー — オプショナルはそのまま使えない
// print(name.count)

if let name {
    print(name.count) // ここでは安全性が保証される
}

「nilチェックを忘れた」という実行時バグが、「オプショナルをアンラップしていない」というコンパイルエラーに変わったのです。ユーザーに届く前、ビルド段階でバグを止められます。

安全性の哲学は、オプショナル以外にも随所に現れています。

  • 使用前の変数初期化を強制: 未初期化メモリを読むバグを根本から防ぎます。
  • 配列の範囲チェック: インデックスが範囲外なら、異常なメモリを読む代わりに即座に停止します。
  • 整数オーバーフロー検出: Cでは値が静かに折り返しますが、Swiftは通常の演算でトラップします。
  • 型推論はあるが暗黙変換はない: IntDoubleをそのまま足すことはできません。不便ですが、Cの暗黙的な型変換が生む微妙なバグを考えれば得策です。

注目すべき点が1つあります。Swiftの安全性は「クラッシュしない」よりも「未定義動作がない」に近いのです。配列の範囲外アクセスでは、Swiftは意図的にクラッシュします。奇妙な値で実行を続けるより、問題箇所で確実に停止するほうが安全だと判断しているのです。

Swiftの安全性は実行時バグをコンパイルゲートで未然に防ぐ
Swiftの安全性は実行時バグをコンパイルゲートで未然に防ぐ

Fast — 安全性のために性能を犠牲にしない

安全な言語は多くあります。問題は、安全装置には通常コストがかかることです。ガベージコレクション、実行時型チェック、インタープリタ。スクリプト言語は安全性と便利さの代わりに速度を差し出してきました。

Swiftの野心は、この取引自体を拒むことです。目標は明確で、C系言語に匹敵する性能を出しながら、上記の安全装置をすべて維持すること。そのために複数の仕組みが重ねられています。

第一に、可能な限りコンパイル時に決定します。Swiftは静的型付け言語であり、コンパイラは型を把握できるため、メソッド呼び出しをコンパイル時に直接アドレスへ解決できます(静的ディスパッチ)。すべてのメソッド呼び出しを実行時にobjc_msgSendで検索するObjective-Cとは対照的です。クラスにfinalを付けるとより積極的に最適化されるのも同じ原理です。

第二に、ガベージコレクションではなくARC(Automatic Reference Counting、自動参照カウント)を使います。参照カウントではコンパイル時にretain/releaseコードが挿入されるため、GC(ガベージコレクション)のように実行時にプログラムを停止してメモリを掃除する一時停止がありません。メモリ解放時点を予測できるのも利点です。

第三に、値型とプロトコル中心の設計です。structはヒープ割り当てや参照カウントのコストなしにスタックへ置け、ジェネリックは特殊化を経て型ごとの専用コードにコンパイルされます。抽象化のために実行時コストを払うのではなく、コンパイラが抽象化を剥がして具体的なコードを生成します。これをゼロコスト抽象化と呼びます。

もちろん現実は目標より複雑です。プロトコル型を格納するexistential containerや、クラスの参照カウントオーバーヘッドなど、Swiftにも性能コストが潜む場所があります。したがって「Swiftは常に速い」ではなく、「速くなれる道を言語が用意しており、そこから外れるとコストを払う」が正確です。その道の詳細は発展編で扱います。

Expressive — 意図がそのままコードに現れる

3つの中で最も捉えにくい価値が表現力です。つまり、コードを読めば作者の意図がそのまま伝わり、したいことを冗長な儀式なしに書けるべきだということです。

Objective-Cと比べると、その違いをはっきり実感できます。

// Objective-C
NSArray *names = @[@"Kim", @"Lee", @"Park"];
NSMutableArray *upper = [NSMutableArray array];
for (NSString *name in names) {
    [upper addObject:[name uppercaseString]];
}
// Swift
let names = ["Kim", "Lee", "Park"]
let upper = names.map { $0.uppercased() }

行数が減ることも重要ですが、より大切なのは意図の密度です。mapという1語が「各要素を変換して新しい配列を作る」という意図全体を伝えます。forループでは、読み手がその意図を再構成しなければなりません。

表現力を支える仕組みをいくつか挙げると、次のとおりです。

  • 型推論: let names = ["Kim", "Lee"]と書けば、それが[String]だとコンパイラが判断します。型安全性を保ちながら、型表記のノイズだけを減らします。
  • enumと関連値: 「成功ならデータ、失敗ならエラー」という状態を、case success(Data)case failure(Error)としてそのままモデル化します。状態とデータが分離しません。
  • 末尾クロージャ、サブスクリプト、演算子定義: ライブラリが言語構文のように読めるAPIを作れるようにします。
  • resultBuilder: SwiftUIの宣言的な構文はこれで作られています。UIの構造とコードの構造が一致します。

注意点もあります。表現力は「短さ」と同義ではありません。Swift API Design Guidelinesの第一原則は「使用箇所での明瞭さ(clarity at the point of use)」であり、簡潔さより明瞭さが優先されます。remove(at: 3)のように引数ラベルをあえて付ける構文があるのはそのためです。remove(3)のほうが短いものの、「3番目の位置を削除するのか、値3を削除するのか」で読み手が迷うからです。

衝突すれば安全性が勝ち、性能と表現力はコンパイラが取り戻す
衝突すれば安全性が勝ち、性能と表現力はコンパイラが取り戻す

3つが衝突したら、どれが勝つのか

哲学が3つあれば、必ず衝突する瞬間があります。Swiftの本質は、その判定記録に表れます。

安全性 vs 表現力: オプショナルのアンラップは確かにコードを騒がしくします。Pythonのようにそのまま使えれば短くなるでしょう。Swiftは安全性を選びました。その代わり、if letの省略構文、オプショナルチェーン(user?.name)、nil合体(??)など、安全性を保ちながらノイズを減らす構文を追加して表現力を補っています。

安全性 vs 性能: 配列の範囲チェックは、アクセスのたびに比較演算が加わることを意味します。Swiftはデフォルトで安全性を選びつつ、範囲外になり得ないとコンパイラが証明できる場合はチェックを削除して性能を取り戻します。本当に必要な人のために、withUnsafeBufferPointerのような逃げ道も用意されています。名前にunsafeを含めることで、リスクを受け入れた事実がコードに残ります。

性能 vs 表現力: 高階関数やジェネリックのような抽象化は表現力の核ですが、素朴に実装すると遅くなります。Swiftはインライン化とジェネリック特殊化によって、「抽象化を使っても手書きコードと同じ機械語を出す」ことにコンパイラの力を注ぎました。

パターンが見えますか。デフォルトは常に安全性で、性能と表現力はコンパイラ最適化と明示的な逃げ道で取り戻す。この公式でSwiftのほぼすべての設計判断を説明できます。

この哲学が実務で持つ意味

哲学の話は雲をつかむように感じるかもしれませんが、実務に直結する点があります。

第一に、コンパイラと戦わず、味方になるほうが得です。Swiftのコンパイルエラーの多くは「将来の実行時バグを今捕まえた」というサインです。オプショナルが面倒だからと!を乱用するのは、言語が築いた防御線を自分で崩す行為です。

第二に、API設計の基準が得られます。自作関数が誤用しやすい形なら、Swiftらしくありません。誤った使い方がコンパイルエラーになるよう型を設計することがSwiftスタイルの核心であり、以降のシリーズでも繰り返し登場します。

第三に、新機能が理解しやすくなります。async/awaitはコールバック地獄という表現力の問題と、データ競合という安全性の問題を同時に狙った機能です。Swift 6のstrict concurrencyは「並行処理のバグもコンパイル時に捕まえる」という安全性の延長です。マクロはボイラープレートという表現力の問題をコンパイル時に解決します。3つの哲学を知れば、新機能がどの価値をどう押し進めたのか見えるようになります。

まとめ

  • Swiftの設計はすべて、Safe、Fast、Expressiveという3つの価値のバランスから生まれています。
  • Safe: オプショナル、初期化の強制、範囲チェックにより、ミスを実行時バグではなくコンパイルエラーにします。
  • Fast: 静的ディスパッチ、ARC、値型、ジェネリック特殊化により、安全装置を維持しながらC級の性能を目指します。
  • Expressive: 型推論、enum、クロージャの目標は短さではなく、意図の明瞭さです。
  • 衝突時のデフォルトは安全性であり、性能と表現力はコンパイラ最適化と明示的な逃げ道で取り戻します。

次回は、この哲学が学習曲線にどう反映されているかを扱います。1行のprintスクリプトとジェネリックライブラリが同じ言語に共存できる秘密、Progressive Disclosureです。

あわせて読みたい