👀

Swift 5.3で引数ラベルを省略したtrailing closureで表示される警告への対処方と原因

2021/02/07に公開

概要

Swift5.3以降で、closure引数を複数受けるメソッドの呼び出し時、全引数をtrailing closureで指定可能になった。

https://docs.swift.org/swift-book/LanguageGuide/Closures.html

その際のルールは次の通り

  • クロージャーが1つの場合は引数ラベルなしでtrailing closureを利用可能 (従来どおり)
  • クロージャーが複数ある場合は 最初のクロージャーは引数ラベルが省略可能、2番目以降のクロージャーには引数ラベルが必須

例えば次のようなメソッドで(Swift Language Guideより引用)

sample method
func loadPicture(
    from server: Server,
    completion: (Picture) -> Void, // 1番目のクロージャー
    onFailure: () -> Void          // 2番目のクロージャー
) {
    if let picture = download("photo.jpg", from: server) {
        completion(picture)
    } else {
        onFailure()
    }
}

// [補足] 動かすなら適当な型へtypealiasでも張って下さい
typealias Server = String
typealias Picture = Int
func download(_ photo: String, from: Server) -> Picture? {
    photo.count + from.count // ここは型を合わせるための適当な処理です
}

これを呼び出す場合に、Swift 5.2以前は以下のような書き方で呼び出していた。

Swift 5.2
loadPicture(
    from: someServer,
    completion: { picture in
        someView.currentPicture = picture
    }) { // 最後のクロージャーのみtrailing closureが利用可能
        print("Couldn't download the next picture.")
    }

一方Swift 5.3からは次のような記法が利用できるようになった。

Swift 5.3
loadPicture(from: someServer)
    { picture in // 1番目のクロージャー: 引数ラベルなしのtrailing closure
        someView.currentPicture = picture
    } onFailure: { // 2番目以降のクロージャー: 引数ラベルありのtrailing closure
        print("Couldn't download the next picture.")
    }

メソッドの末尾がより簡潔になり、可読性が向上していると個人的には思う。

片方の引数にデフォルト値を設定する場合

上記の例では、いすれのクロージャー引数にもデフォルト引数が設定されていないので省略できない。
ここで片方にデフォルト引数を設定しラベルなしでtrailing closureを利用すると、デフォルト引数を指定しなかった方のクロージャーとして認識される。

default completion
func loadPicture(
    from server: Server,
    completion: (Picture) -> Void = { _ in},
    onFailure: () -> Void
) { /* 略 */ }

// trailing closureはonFaliure
loadPicture(from: "my server") {
    print("called onFaliure")
}
default onFailure
func loadPicture(
    from server: Server,
    completion: (Picture) -> Void,
    onFailure: () -> Void = {}
) { /* 略 */ }

// trailing closureはcompletion
loadPicture(from: "my server") { _ in
    print("called completion")
}

両方の引数にデフォルト値が設定された場合

では両方に対してデフォルト引数を設定したらどうなるか。
もちろんこの場合は、2つのクロージャー引数うちいずれかまたは両方を省略しても呼び出しが可能である。

default onFailure
func loadPicture(
    from server: Server,
    completion: (Picture) -> Void = { _ in } ,
    onFailure: () -> Void = {}
) { /* 略 */ }

// 以下のいずれも文法的には利用可能だが
loadPicture(from: "my server")
loadPicture(from: "my server") { _ in print("called completion") }
loadPicture(from: "my server") { print("called onFailure") } // この呼び出しはwarningが発生する

しかし最後の呼び出しのみ、以下のような警告が表示される。

Backward matching of the unlabeled trailing closure is deprecated; label the argument with 'completion' to suppress this warning.

これを回避するには、サジェストに従って下記のように引数ラベル付きで呼ぶか

loadPicture(from: "my server", onFailure: { print("called onFailure") })

completion を含まないメソッドをオーバーロードする必要がある。

func loadPicture(
    from server: Server,
    onFailure onFailure: () -> Void = {}
) {
    loadPicture(from: server, completion: { _ in }, onFailure: onFailure)
}

Swift5.3からクロージャー引数のマッチが前方からの一致に変更

こうなった背景には、クロージャーのコンパイル時のマッチルールがSwift5.3で変わったことが影響していると思われる。

https://github.com/apple/swift-evolution/blob/master/proposals/0286-forward-scan-trailing-closures.md

単一のtrailing closureのみが許容されていたSwift5.2以前は、コンパイラがクロージャー引数の型のマッチをするときには、後ろから前に向かって引数のパターンを見てマッチするメソッドの探索を行っていたようである。
例えば下記の場合、コンパイラがtrailing closureとなっている引数を onFailure として解釈しており、引数ラベルなしでは completion として解釈してくれなかった。

func loadPicture(
    from server: Server,
    completion: (Picture) -> Void,
    onFailure: () -> Void = {}
) { /* 略 */ }
// Swift 5.3ではOKだが5.2以前では "error: missing argument for parameter 'completion' in call" が発生
loadPicture(from: "my server") { _ in } // Swift 5.2では `onFailure` に相当する引数として解釈されてしまう

これにより onFailure を省略しようとして completion に対応するクロージャーを渡そうとしても、それが onFailure として認識されてしまうということが起きていた。

そこでSwift 5.3からは前方から順に引数をマッチするようになったため、クロージャー引数を completion として解釈してくれるようになった。
その代わりにSwift 5.2の逆のパターンとして、trailing closureが前方の引数にマッチしなかった場合にはエラーとなってしまう。
前述の「両方の引数にデフォルト値が設定された場合」のようにtrailing closureで後方のクロージャー引数を指定するには、本来なら呼び出し時に引数ラベルを明示的に指定しなければならない。
しかしこれは破壊的変更なのでどうやらSwift 5.3ではエラーにはできず、以前の後方一致による判定ロジックもwarning付きで残されているようである。

image
(AppCode 2020.3.2でのUI)

これが今回の「backward matchingに関する警告」を起こしている要因だったようだ。
今後のSwift(おそらくv6.0以降)では、このような後方一致に依存したtraling closureの指定が使えなくなる可能性もある。
今のうちから引数ラベルを付与して対応しておくのが良さそうである。

参考

Discussion