Swift Digest
SE-0284 | Swift Evolution

Allow Multiple Variadic Parameters in Functions, Subscripts, and Initializers

Proposal
SE-0284
Authors
Owen Voorhees
Review Manager
Saleem Abdulrasool
Status
Implemented (Swift 5.4)

01 何が問題だったのか

Swift では可変長引数(variadic parameter)を使うことで、コンパイル時に個数は決まっていなくても配列リテラルの括弧を書かずに複数の値を渡せる、簡潔な API を設計できます。標準ライブラリの print 関数がその代表例です。

しかし可変長引数には次の二つの制約がありました。

  • パラメータリストに可変長引数を 1 つしか 置けない
  • 可変長引数の 次に来るパラメータには必ずラベルが必要

1 つ目の制約は、2 つ以上のリストを自然に受け取りたい API にとっては大きな不便でした。たとえば swift-driver プロジェクトにある次のようなヘルパーを考えてみます。

func assertArgs(
      _ args: String...,
      parseTo driverKind: DriverKind,
      leaving remainingArgs: ArraySlice<String>,
      file: StaticString = #file, line: UInt = #line
    ) throws { /* Implementation Omitted */ }

try assertArgs("swift", "-foo", "-bar", parseTo: .interactive, leaving: ["-foo", "-bar"])

先頭の引数列は可変長引数なので括弧なしで渡せるのに、leaving: に渡すリストは(前に可変長引数があるせいで可変長引数にできず)配列リテラルとして [...] で囲まなければなりません。同じ「文字列の並び」を受け取りたいだけなのに、呼び出し側の見た目がちぐはぐになってしまいます。

DSL 風の軽量な API でも同様の問題が起きます。たとえば複数のビューと複数の制約をまとめて受け取りたい次のようなメソッドは、可変長引数を 1 つしか置けないために素直に書けませんでした。

extension UIView {
  func addSubviews(_ views: UIView..., constraints: NSLayoutConstraint...) {
    // ...
  }
}

また、この制約を素朴に実装しているために、コンパイラには長年のバグが残っていました。サブスクリプトやクロージャでは、「可変長引数の次にラベルなしのパラメータ」という本来呼び出し不可能な宣言が受理されてしまっており、書けはするものの実際には呼び出せない(あるいは第 2 引数を明示的に指定できない)デッドコードが許容されていました。

02 どのように解決されるのか

「1 つのパラメータリストに可変長引数は 1 つまで」という制約を撤廃し、関数・サブスクリプト・イニシャライザに 複数の可変長引数 を書けるようにします。「可変長引数の次に来るパラメータにはラベルが必要」というルールはそのまま維持されるため、呼び出し側は常に可変長引数の切れ目を見分けられます。

関数・イニシャライザでの書き方

次のように、ラベルで区切りさえ付ければ 2 つ以上の可変長引数を並べられます。

// 2 つ目のパラメータはラベルが必須(可変長引数の直後に来るため)
func twoVarargs(_ a: Int..., b: Int...) { }
twoVarargs(1, 2, 3, b: 4, 5, 6)

// 可変長引数はデフォルトで [] なので省略可能
twoVarargs(1, 2, 3)
twoVarargs(b: 4, 5, 6)
twoVarargs()

可変長引数の間に非可変長のパラメータを挟むこともできます。間に挟まるパラメータが可変長引数でなければ、その に来るパラメータにはラベル必須ルールが適用されません。

// 3 つ目のパラメータは、直前の b が可変長引数でないためラベル不要
func splitVarargs(a: Int..., b: Int, _ c: Int...) { }
splitVarargs(a: 1, 2, 3, b: 4, 5, 6, 7)
// a is [1, 2, 3], b is 4, c is [5, 6, 7]
splitVarargs(b: 4)
// a is [], b is 4, c is []

間に挟むパラメータにデフォルト値があっても同様です。ただしこの場合、3 つ目の可変長引数に値を渡すときは、間のデフォルト引数にも必ず値を与える必要があります。

func varargsSplitByDefaultedParam(_ a: Int..., b: Int = 42, _ c: Int...) { }
varargsSplitByDefaultedParam(1, 2, 3, b: 4, 5, 6, 7)
// a is [1, 2, 3], b is 4, c is [5, 6, 7]
varargsSplitByDefaultedParam(b: 4, 5, 6, 7)
// a is [], b is 4, c is [5, 6, 7]
varargsSplitByDefaultedParam(1, 2, 3)
// a is [1, 2, 3], b is 42, c is []

サブスクリプトでの書き方

サブスクリプトは宣言時に外部ラベルを明示しないとラベルが付かない点に注意が必要です。可変長引数の次に来るパラメータには、b b: のように外部ラベルを明示的に書く必要があります。

struct HasSubscript {
    // NG: 2 つ目のパラメータに外部ラベルがない
    subscript(a: Int..., b: Int...) -> [Int] { a + b }

    // OK
    subscript(a: Int..., b b: Int...) -> [Int] { a + b }
}

クロージャでの扱い

クロージャのパラメータリストには外部ラベルがそもそも存在しないため、可変長引数を複数並べることはできません。次のような、従来は受理されていたものの呼び出し不可能だった記述はコンパイルエラーになります。

// コンパイルエラー
let closure = {(a: Int..., b: Int) in}

長年のバグの修正

本 Proposal では、これまでサブスクリプトやクロージャで誤って受理されていた「可変長引数の次にラベルなしのパラメータ」を コンパイルエラー に変更します。対象となる宣言は基本的に呼び出し不可能なものばかりで、わずかにデフォルト引数が絡むサブスクリプトのみ「宣言はできるが第 2 引数を明示指定できない」という形で実害がありえますが、いずれにせよ実用的な利用はほぼ存在しない箇所の修正です。