※この記事はSwift by Sundellの内容を日本語に翻訳したものです。
ジェネリック型制約を使用して、特定のAPIが使用される具象型に一連の要件を課すことができます。これにより、ジェネリックコード内でそれらの型について特定の仮定を行うことができます。
たとえば、私たちが取り組んでいるアプリがNetworkRequest
プロトコルを使用して、さまざまなリクエストを、それぞれResponse
が期待する種類を宣言するさまざまなタイプとして定義できるようにしているとします。
protocol NetworkRequest {
associatedtype Response: Codable
func makeURLRequest() -> URLRequest
}
このようなリクエストを実行するためのAPIを定義すると、次のような結果になる可能性があります。ジェネリック型制約を使用して、そのT
型が実際に上記のプロトコルに準拠していることを確認する関数です。
func perform<T: NetworkRequest>(
_ request: T,
then handler: @escaping (Result<T.Response, Error>) -> Void
) {
...
}
ここで、NetworkRequestQueue
クラスを使用してリクエストをキューに入れるためのサポートも追加したいとします。これも、NetworkRequest
プロトコルに制約されたジェネリック型を使用します。
class NetworkRequestQueue<Request: NetworkRequest> {
...
}
上記のクラスのインスタンスをperform
関数に挿入するためのパラメーターを追加する場合、最初は次のようにパラメーターのリストに追加するだけです。
func perform<T: NetworkRequest>(
_ request: T,
on queue: NetworkRequestQueue<T>,
then handler: @escaping (Result<T.Response, Error>) -> Void
) {
...
}
ただし、NetworkRequestQueue
クラスでは、NetworkRequest
プロトコルに準拠するために汎用のRequest
タイプがすでに必要であり、コンパイラは、そのクラスを特殊化するために使用されるタイプがリクエストパラメータに使用されるタイプと同じであることを認識しているため、実際には省略できます。次のように、perform
関数からの型制約を完全に実行します。
func perform<T>(
_ request: T,
on queue: NetworkRequestQueue<T>,
then handler: @escaping (Result<T.Response, Error>) -> Void
) {
...
}
上記は細かいことのように思えるかもしれませんが、おそらくそれは物事の壮大なスキームにあります—しかし、ジェネリックコードは非常に冗長になる傾向があるため、特に型制約を使用する場合、そのような冗長性を減らすためにできることは間違いなく良いことです。
追加のボーナスとして、上記のhandler
パラメーターのクロージャー型も少し単純にしましょう。最初に、NetworkRequest
プロトコルの拡張機能内で型エイリアスに変換します。
extension NetworkRequest {
typealias ResponseHandler = (Result<Response, Error>) -> Void
}
上記を実行すると、perform
関数のシグネチャをさらに読みやすくすることができます—次のようになります。
func perform<T>(
_ request: T,
on queue: NetworkRequestQueue<T>,
then handler: @escaping T.ResponseHandler
) {
...
}
Swiftの構文は、ジェネリック型と関数の定義に関しては最初はかなり冗長に見えるかもしれませんが、読みやすさを犠牲にすることなく、そのような定義をはるかにコンパクトにするために使用できる特定のトリックとテクニックがよくあります。
元の記事
Inferred generic type constraints Swift by Sundell