Swift API の設計に @autoclosure を使用する#
この記事は翻訳です#
オリジナル記事:Using @autoclosure when designing Swift APIs
Swift の @autoclosure 属性は、クロージャ内で「包まれた」引数を定義するために使用されます。これは、必要な時にのみ実行される(潜在的に時間がかかり、リソースを消費する)コードを遅延実行するために主に使用されます。
Swift の標準ライブラリには、Assert 関数の使用例があります。assert はデバッグモードでのみトリガされるため、リリースモードではコードを実行する必要はありません。そのため、@autoclosure が使用されます:
func assert(_ expression: @autoclosure() -> Bool,
_ message: @autoclosure() -> String) {
guard isDebug else {
return
}
// assert内では、expressionを通常のクロージャとして参照できます
if !expression() {
assertionFailure(message())
}
}
上記は私が assert の実装を理解したものであり、assert の実際の実装はこちらです
@autoclosure の利点の 1 つは、呼び出しの場所に影響を与えないことです。assert が「通常」のクロージャの形式で実装されている場合、使用方法は次のようになります:
assert({ someConfition() }, { "Hey, it failed!" })
しかし、今では、クロージャ引数なしで関数を呼び出すことができます:
assert(someCondition(), "Hey it failed!")
今週は、@autoclosure を自分のコードでどのように使用し、エレガントな API を設計するかを見ていきます。
インライン関数#
@autoclosure の 1 つの用途は、関数呼び出しで式をインライン化することです。これにより、指定した式を引数として使用できます。以下の例を見てみましょう:
iOS では、通常、次の API を使用してビューアニメーションを実装します:
UIView.animate(withDuration: 0.25) {
view.frame.origin.y = 100
}
@autoclosure を使用すると、animate関数を作成し、アニメーションのクロージャを自分で作成して実行できます。次のようになります:
func animate(_ animation: @autoclosure @escaping () -> Void,
duration: TimeInterval = 0.25) {
UIView.animate(withDuration: duration, animations: animation)
}
その後、animate関数を単純な関数呼び出しで呼び出すだけで、余分な {} がなくなります:
animate(view.frame.origin.y = 100)
上記の方法により、冗長なアニメーションコードが減少し、可読性が損なわれません。
エラーを式として渡す#
@autoclosure のもう 1 つの非常に便利な用途は、エラー処理のユーティリティクラスを作成する場合です。例えば、Optional をアンラップするために throwing API を使用したい場合、次のように Optional に非 null の値があるか、エラーを throw するかを判断したいとします:
extension Optional {
func unwrapThrow(_ errorExpression: @autoclosure() -> Error) {
guard let value = self else {
throw errorExpression()
}
return value
}
}
assert の実装と似ていますが、エラー式の評価は必要な時のみ行われ、Optional をアンラップするたびに null チェックする必要はありません。これで、unwrapOrThrow API を次のように使用できます:
let name = try argument(at: 1).unwrapOrThrow(ArgumentError.missingName)
デフォルト値を使用した型推論#
@autoclosure の最後の使用シナリオは、dictionary、database、UserDefaults から null 可能な値を取得する場合です。
通常、不確定な型の dictionary からプロパティを取得し、デフォルト値を提供するには、次のようなコードを書く必要があります:
let coins = (dictionay["numberOfCoins"] as? Int) ?? 100
上記のコードは読みにくく、型推論と??演算子が必要です。@autoclosure を使用して、同じ表現を実現する API を定義できます:
let coins = dictionary.value(forKey: "numberOfCoins", defaultValue: 100)
上記のように、defaultValue は値が欠落した場合のデフォルト値として使用されるだけでなく、型推論にも使用されるため、特に型を宣言する必要はありません。シンプルで明快です👍
次に、このような API を定義する方法を見てみましょう:
extension Dictionary where Value = Any {
func value<T>(forKey key: Key, defaultValue: @autoclosure () -> T) -> T {
guard let value = self[key] as? T else {
return defaultValue()
}
return value
}
}
同様に、defaultValue の判定をメソッドの実行ごとに避けるために、@autoclosure を使用しています。
結論#
冗長なコードを減らすことは慎重に考える必要があります。私たちの目標は、自己説明的で読みやすいコードを書くことですので、低冗長な API を設計する際には、重要な情報が呼び出しから削除されていないことを確認する必要があります。
私は、@autoclosure を適切に使用することが、上記の目標を達成するための素晴らしいツールだと考えています。式を処理することで、冗長なコードを減らすだけでなく、パフォーマンスも向上させることができます。
@autoclosure には他の使用シナリオがあると思いますか?または、質問、コメント、フィードバックがある場合は、お気軽にお問い合わせください。Twitter: @johnsundell.
お読みいただきありがとうございました!🚀