🍣

guard ~ else return 撲滅委員会

2021/10/23に公開

嘘です。撲滅はしません。
ただし、期待する前提条件を満たさないからと言って安易にreturnやreturn nilしてはいけません。
guard ~ else return は思慮深く使用しなければなりません。

無駄nilチェックreturn

以下のコードではguardでURLの生成失敗をチェックし、失敗の場合return nilします。

func example() -> String? {
    guard let url = URL(string: "https://example.com/") else {
        return nil
    }
    var result: String = /* なんか処理する */
    return result
}

URLのinitializerに渡すのがリテラルならば、これは絶対に失敗しないことがわかりますから、guardは不要です。ましてやこのためだけに戻り値をOptionalにするのはデメリットでしかありません。

func example() -> String {
    let url = URL(string: "https://example.com/")!
    var result: String = /* なんか処理する */
    return result
}

素直に!を使えば良いです。
ただし、URL文字列をプログラム外部から入力する場合は必ずnilチェックが必要です。
UIImage(named: String)なども同様の扱いで良いでしょう。

引数チェックreturn

以下のコードはguardで引数をチェックし期待する条件を満たさなければreturnしています。

func example(num: Int?, urlString: String) {
    guard let num = num, let url = URL(string: urlString) else {
        return
    }
    // 何か処理する
}

まず大きな問題は、処理の失敗をどこにも伝えていないところです。これは気付きにくいタイプのバグの温床となります。

戻り値のあるメソッドの場合はreturn nilされることも多いと思います。この場合呼び出し側は失敗を検知することが可能となりますが、戻り値をOptionalにするのはできる限り回避するべきです。

もちろん、このパターンは引数の型の条件を厳しくして、呼び出し側にチェックさせることで簡単に改善することができます。

func example(num: Int, url: URL) {
    // 何か処理する
}

場合によってはこのために独自のstructを定義するのも有効でしょう。

内部状態チェックreturn

次のコードではexecuteWorkに必要なstateをチェックし、nilだったらreturnします。

class Example {
    private var state: Int?
    
    private func executeWork() {
	guard let state = state else {
	    return
	}
	// 何か処理する
    }
    
    func otherMethod() {
        self.state = /* state準備 */
	executeWork()
    }
    ...
}

このパターンでは、まずexecuteWorkはstateがnilの場合を正常な状態として受理するのか、不正な状態として扱うのか明確にするべきです。
次に、nilが不正な状態であるならば、呼び出し側にstateが存在する保証を要求するべきです。

その方法のひとつは、非Optionalの引数を要求することです。うまくいけば、ついでにインスタンスからmutableな状態を削減できます。

class Example {
    private func executeWork(state: Int) {
	// 何か処理する
    }
    
    func otherMethod() {
        let state = /* state準備 */
	executeWork(state: state)
    }
    ...
}

上のような綺麗な方法が難しい場合、理想的ではないですが、preconditionを使ってstateがnilの状況で呼び出してはならないメソッドであることを明示的に主張する方法があります。

class Example {
    private var state: Int?
    
    private func executeWork() {
	precondition(state != nil)
	// 何か処理する
    }
    
    func otherMethod() {
        self.state = /* state準備 */
	executeWork()
    }
    ...
}

実行時エラーチェックreturn

次のコードではファイルから文字列をロードし、失敗したらreturnします。

func example(fileURL: URL) {
    guard let content = try? String(contentsOf: fileURL, encoding: .utf8) else {
        return
    }
    // なんか処理する
}

ファイルやネットワークなど実行時のエラーを完全に排除できないケースでは、失敗時の分岐そのものは排除できません。上のケースはおそらくそのままthrowしてしまうのが良いでしょう。

func example(fileURL: URL) throws {
    let content = try String(contentsOf: fileURL, encoding: .utf8)
    // なんか処理する
}

失敗時にthrowではなくnilを返してくるメソッドを使用する場合でも、guard ~ else throwに変更することにより意図がより明確になる場合があり、検討の価値があります。

guard ~ else returnするとき

ここまでいかにguard ~ else returnを避けるか考えてきましたが、もちろんguard ~ else returnを使用するべき時もあります。
ただし、それが安易なreturnではなく思慮深く選択された結果であることをコメントで主張した方が読む人に優しいです。

func example() {
    guard let state = self.state else {
        // この場合は○○のため何もしない
        return
    }
    // 他の処理
}

// ○○を行う。○○の場合はnilを返す
func example2() -> String? {
    guard let state = self.state else {
        return nil
    }
    // 他の処理
}

まとめ

重要なポイントはメソッドが引数やインスタンスの状態を処理対象として受け入れるか否かをはっきりさせることです。
処理対象として受け入れるなら、すべての分岐について不正な状態が発生しないよう注意深く設計する必要があります。逆に、受け入れないなら型やpreconditionなどを使用して明示的に拒絶するべきです。
コード上受け入れているのに処理できないので適当にreturnするのは避けましょう。

Discussion