Swift & Objective-C

[Swift基礎 #4] Swiftのguardを正しく使う:破滅のピラミッドを倒す早期脱出

Swiftのguardは失敗条件を先に処理し、正常な処理の流れを平坦に保ちます。ifとの違いであるコンパイラの保証、オプショナルバインディングの有効範囲、return・throw・continueの使い分けを実行例で確認します。

読了 7 分
[Swift基礎 #4] Swiftのguardを正しく使う:破滅のピラミッドを倒す早期脱出のカバー画像

コードレビューでよく出る指摘の一つに「これ、guardに変えましょう」があります。ifでも同じように動くコードなのに、なぜわざわざ変えるのでしょうか。好みの問題に見えますが、実際にはguardは特定のコードスタイルを言語レベルで推奨するためにSwiftが用意した構文です。早期脱出と、左端に揃ったハッピーパスというスタイルです。

Swift基礎シリーズの第4回です。オプショナル編ではguard letをアンラップの道具として少し紹介しました。今回はguardそのものを掘り下げます。ifとの違い、コンパイラが保証すること、そしてguardではなくifを選ぶべき場面まで扱います。

値がない状態から振り返りたい場合は、先にSwiftのオプショナルの正体とアンラップの判断基準を読むとよいでしょう。

問題の風景 — 破滅のピラミッド

まずはguardがなかった頃のコードを見てみましょう。会員登録処理の関数だとすると、確認項目がたくさんあります。入力がnilでないか、形式が正しいか、利用規約に同意したかどうかです。

func signUp(email: String?, password: String?, agreed: Bool) {
    if let email = email {
        if isValidEmail(email) {
            if let password = password {
                if password.count >= 8 {
                    if agreed {
                        // 本当にやりたかったこと
                        createAccount(email, password)
                    } else {
                        showError("利用規約への同意が必要です")
                    }
                } else {
                    showError("パスワードが短すぎます")
                }
            } else {
                showError("パスワードを入力してください")
            }
        } else {
            showError("メールアドレスの形式ではありません")
        }
    } else {
        showError("メールアドレスを入力してください")
    }
}

インデントが5段重なっています。この形は破滅のピラミッドと呼ばれますが、見た目が悪いだけでなく、実際に読解コストがかかります。関数の本題(createAccount)が最も深い場所に埋もれ、各チェックの失敗処理(else)も条件から画面上遠く離れています。「パスワードが短い場合はどうなるのか」に答えるには、スクロールしながら目で括弧の対応を追わなければなりません。

guardの答え — 失敗を先に、本題は平地に

同じ関数をguardで書き直すと、次のようになります。

func signUp(email: String?, password: String?, agreed: Bool) {
    guard let email, isValidEmail(email) else {
        return showError("メールアドレスを確認してください")
    }
    guard let password, password.count >= 8 else {
        return showError("パスワードを確認してください (8文字以上)")
    }
    guard agreed else {
        return showError("利用規約への同意が必要です")
    }

    createAccount(email, password)
}

構造が逆転しました。各条件と失敗処理が一つにまとまり、すべての検問を通過した本題は、インデントなしで関数の底に平らに置かれます。読む人の視線は上から下へ一度流すだけで済みます。「前提条件、そして本題」という関数の論理構造が、コードの視覚構造と一致したのです。

このスタイルの利点は、ハッピーパスを左端に揃えることだとよく表現されます。正常な流れは常にインデント0の位置にあり、例外的な状況だけがブロックの中に入るというルールです。関数が長くなっても、「左端だけ追えば正常なシナリオが見える」という一貫性が生まれます。

検問を通過すると、本題はインデントのない平地に置かれます
検問を通過すると、本題はインデントのない平地に置かれます

ifとの本当の違い — コンパイラが強制する2つのこと

「ifでも早期脱出できるのでは?」という疑問が出てくる頃です。その通りです。if email == nil { return }のように書けます。ただしguardはifを反転しただけではなく、コンパイラが強制する2つの保証があります。

1つ目は、elseブロックが必ずスコープを抜けなければならないことです。 guardのelse内では、return、throw、continue、break、fatalErrorのいずれかで現在のスコープを脱出する必要があり、そうしなければコンパイルエラーになります。ifでチェックを書くと「条件を確認しただけでreturnを忘れる」ことが起こり得ますが、guardならそのミスをコンパイル時に検出できます。「この地点を通過したなら条件は真である」という文が、慣習ではなく保証になるのです。

2つ目は、アンラップした値がguard以降の全体で有効になることです。 if letのバインディングはifブロック内だけで有効ですが、guard letのバインディングはguard文以降の残りのスコープ全体で使えます。検問を通した値を関数の最後まで保持できるため、「アンラップする場所」と「使う場所」が分かれる不自然さがありません。

この2つの保証が合わさることで、guardはドキュメントの役割を果たします。関数冒頭のguardのまとまりは、「この関数の前提条件一覧」を宣言する場所です。シグネチャだけでは表現しきれない契約を、本文の最初の行から読み取れるようになります。

guardではなくifを選ぶべき場面

では、すべてのifをguardに変えるべきでしょうか。いいえ。基準は明確です。guardは「これでなければ先に進めない」という前提条件に使い、ifは「この場合はこう、別の場合はそうする」という分岐に使います。

// ififを使う場面 — どちらの経路も正常な流れ
if user.isPremium {
    showPremiumBadge()
} else {
    showUpgradeButton()
}

プレミアムでないことは失敗ではありません。両方の分岐がそのまま進む正常なシナリオならifです。これをguardで書くと、「プレミアムでなければ異常」という誤った印象を与えてしまいます。

反対に、チェックに失敗したら関数を終了する種類のものなら、項目が1つだけでもguardが適しています。構文の選択そのものが意味を伝えるわけです。guardを見れば「前提条件だ」と、ifを見れば「分岐だ」と読者は期待するため、その期待に合わせることが可読性の実体になります。

もう一つ、アンチパターンにも触れておきます。guard elseブロックにロジックを詰め込むケースです。else内で復旧を試みたり、状態を変更したり、長い処理を始めたりすると、guardの「失敗は短く終える」という約束が崩れます。elseが3行を超えたら、設計を見直すサインと考えるのがよいでしょう。失敗処理がそれほど複雑なら、それは前提条件ではなく別の分岐か、エラーを投げて呼び出し元に渡すべき処理です。

前提条件ならguard、両方が正常な経路ならif
前提条件ならguard、両方が正常な経路ならif

ループと非同期コードでのguard

guardは関数の外でも使います。ループ内ではcontinueと組み合わせ、「この項目はスキップする」と表現します。

for item in items {
    guard item.isValid else { continue }
    guard let url = item.downloadURL else { continue }
    process(url)
}

フィルタリング条件が複数あるとき、for本体を平坦に保てるのが利点です。もちろん条件が単純ならfor item in items where item.isValidやcompactMapのほうが簡潔です。where句1つで済むならwhere、アンラップが混ざり条件が複数段階にわたるならguardが便利です。

非同期コードでは、クロージャ側で見た[weak self]との組み合わせが、事実上の慣用句になっています。

fetchData { [weak self] data in
    guard let self else { return }
    self.update(with: data)
}

「selfがすでに解放されていたら何もしない」という前提条件を1行目で処理し、残りのコードはselfが確保された平地で進める構造です。guardの早期脱出という考え方が、メモリ管理と交わる場所です。

実際に実行して確認した結果

Apple Swift 6.3.3でString?入力を受け取る関数を作り、nilはguardのelseで拒否し、値があればバインドされた文字列を次の行で使いました。

guard=rejected,accepted:devpaw

コードレビューで私がguardを勧める基準も、この出力と同じです。入力に失敗したら現在の経路を終え、通過した値だけを本文で使い続けるならguardが適しています。真偽のどちらも正常な業務フローなら、平坦に見えるという理由だけでguardに変えず、ifを維持します。

まとめ

  • guardは早期脱出を言語レベルで支援する構文であり、破滅のピラミッドを「前提条件の列挙+平坦な本題」という構造にひっくり返します。
  • ifとの違いはコンパイラの保証です。elseは必ずスコープを抜ける必要があり、バインディングは後続のスコープ全体で有効です。
  • 選択基準:失敗すると進めない前提条件にはguard、両方が正常な分岐にはif。構文の選択自体が意味を伝えます。
  • elseブロックが長くなるなら、guardを誤って使っているサインです。失敗処理が複雑なら、分岐またはエラーを投げる形で解決します。

最後の文に「エラーを投げる」という言葉が出てきました。次回はまさにその話です。throws、do-catchとtryの3つの変形、そしてResultまで、Swiftのエラー処理を一望します。

あわせて読みたい

出典と確認基準