今是昨非

今是昨非

日出江花红胜火,春来江水绿如蓝

Swiftでは、フローを制御するためにerrorを使用します。

この記事は翻訳です#

元の記事のリンク:Using errors as control flow in Swift

アプリやプロジェクトでの制御フローの管理は、コードの実行速度やデバッグの複雑さに大きな影響を与えます。制御フローは、本質的には関数と宣言の実行順序、およびコードの実行パスです。

Swift は、if、else、while、オプションなど、制御フローを定義するための多くのツールを提供していますが、今週は、Swift のコンパイル時エラーを使用してモデルをスローおよび処理する方法を見てみましょう。これにより、制御フローの管理が容易になります。

オプション値のスロー#

オプション値は、Swift の重要な機能として、空のデータを処理する際に無視することができます。また、制御フロー内で関数の元のテンプレートとして使用されることもよくあります。

以下は、アプリからバンドルをロードし、画像を調整するためのメソッドを再作成したものです。各ステップでオプションの画像が返されるため、関数内のどこで終了するかを指示するために、複数の guard ステートメントを記述する必要があります:

func loadImage(named name: String,
               tintedWith color: UIColor,
               resizedTo size: CGSize) -> UIImage? {
    guard let baseImage = UIImage(named: name) else {
        return nil
    }

    guard let tintedImage = tint(baseImage, with: color) else {
        return nil
    }

    return resize(tintedImage, to: size)
}

上記のコードの問題は、ランタイムエラーに対処するために nil を使用していることです - これにより、結果を解析する必要があり、エラーが発生した根本的な原因が隠されます。

次に、上記の問題を解決するために、関数とエラーをスローすることによって制御フローを再構築する方法を見てみましょう。まず、画像の処理中に発生する可能性のあるすべてのエラーを含む enum を定義します:

enum ImageError: Error {
    case missing
    case failedToCreateContext
    case failedToRenderImage
    ...
}

次に、関数が失敗した場合に上記で定義したエラーをスローするように変更し、nil を返さないようにします。たとえば、loadImage (named:) メソッドを変更して、非 null の image を返すか、ImageError.missing をスローするようにします:

private func loadImage(named name: String) throws -> UIImage {
    guard let image = UIImage(named: name) else {
        throw ImageError.missing
    }
    return image
}

他の画像処理メソッドも同様に変更すると、トップレベルの他の関数も同様に変更できます - オプションをすべて削除し、操作中に確定した画像を返すか、エラーをスローします:

func loadImage(named name: String,
               tintedWith color: UIColor,
               resizedTo size: CGSize) throws -> UIImage {
    var image = try loadImage(named: name)
    image = try tint(image, with: color)
    return try resize(image, to: size)
}

上記の変更により、関数の本体がより簡潔になり、エラーが発生した場合には明確なエラーが得られるため、デバッグが容易になります - nil が返されるステップを特定する必要がなくなります。

ただし、すべての場所でエラーを処理する必要はないため、do、try、catch モードを強制する必要はありません。また、do、try、catch を乱用すると、避けるために余分なテンプレートコードが発生します - 使用する場所を注意深く区別する必要があります。

幸いなことに、スローするメソッドを呼び出す場合でも、いつでもオプション値を使用できます。必要なのは、try? キーワードを使用してスローするメソッドを呼び出すだけです。

let optionalImage = try? loadImage {
    named: "Decoration",
    tintedWith: .brandColor,
    resizedTo: decorationSize
}

try? を使用する最大の利点は、両方の方法を組み合わせることができることです。呼び出し中にオプション値を取得できるだけでなく、スローとエラーを使用して制御フローを管理することもできます。

入力の検証#

次に、入力の検証時にエラーを使用すると、制御フローが向上する方法を見てみましょう。Swift の高度で強力な型システムがあるにもかかわらず、関数が有効な入力を受け取ることを保証するわけではありません - 時にはランタイムチェックが唯一の方法です。

次に、ユーザーが登録する際に、ユーザーが選択した資格を検証する例を見てみましょう。前述の例と同様に、コードは guard ステートメントを使用して各検証ルールを確認し、エラーメッセージを表示します:

func signUpIfPossible(with credentials: Credentials) {
    guard credentials.username.count >= 3 else {
        errorLabel.text = "Username must contain min 3 characters"
        return
    }

    guard credentials.password.count >= 7 else {
        errorLabel.text = "Password must contain min 7 charaters"
        return
    }

    // Additional validation
    ...
    service.signUp(with: credentials) { result in
        ...
    }
}

上記のコードは、2 つの条件のみを検証していますが、検証ロジックが予想を超えるほど成長する可能性があります。このようなロジックが UI に存在する場合(特にビューコントローラ内の場合)、テストがより困難になります - したがって、コードの制御フローを改善し、解耦する方法を見てみましょう。

理想的には、コードが自己完結していることを望みます。これにより、テスト中に隔離され、コード内で使用できるようになります。これを実現するために、すべての検証ロジックに対して指定された型を作成します。Validator という名前の構造体で、指定された値の検証クロージャが含まれています:

struct Validator<Value> {
    let closure: (Value) throws -> Void
}

上記のコードを使用して、値が検証に合格しない場合にエラーをスローする validators を構築できます。ただし、すべての検証プロセスに新しいエラータイプを定義することは、不要なテンプレートを生成することになります(特にこれらのエラーをユーザーに表示したい場合) - そのため、条件とユーザーに表示するエラーメッセージのみを受け取る関数を定義します:

struct ValidationError: LocalizedError {
    let message: String
    var errorDescription: String? { return message }
}

func validate { _ condition: @autoclosure () -> Bool, errorMessage messageExpression: @autoclosure () -> String } throws {
    guard condition() else {
        let message = messageExpression()
        throw ValidationError(message: message)
    }
}

上記では、再び @autoclosure を使用して、クロージャ内で自動的に解決される式を使用しています。詳細については、"Using @autoclosure when designing Swift APIs".

上記が完了したら、整理された検証ロジックコードを書くための指定されたタイプを作成できます - Validator タイプの静的計算プロパティです。以下は、パスワードバリデータの実装例です:

extension Validator where Value == String {
    static var password: Validator {
        return Validator { string in
            try validate(string.count >= 7, errorMessage: "Password must contain min 7 characters")

            try validate(string.lowercased() != string, errorMessage: "Password must contain an uppercased character")

            try validate(string.uppercased() != string, errorMessage: "Password must contain a lowercased character")
        }
    }
}

完全にするために、新しい validate 関数をオーバーロードし、引数として検証する値と使用するバリデータを渡すシンタックスシュガーを作成します:

func validate<T>(_ value: T, using validator: Validator<T>) throws {
    try validator.closure(value)
}

準備が整ったので、新しい検証システムを呼び出しで使用してコードを更新します。上記のコードのエレガントな部分は、追加の型や設定が必要ですが、入力を検証する必要があるコードがより整理されることです。

func signUpIfPossible(with credentials: Credentials) throws {
    try validate(credentials.username, using: .username)
    try validate(credentials.password, using: .password)

    service.signUp(with: credentials) { result in
        ...
    }
}
読み込み中...
文章は、創作者によって署名され、ブロックチェーンに安全に保存されています。