Swift Digest
SE-0242 | Swift Evolution

Synthesize default values for the memberwise initializer

Proposal
SE-0242
Authors
Alejandro Alonso
Review Manager
Ted Kremenek
Status
Implemented (Swift 5.1)

01 何が問題だったのか

構造体には、それぞれのプロパティを引数に取るイニシャライザがコンパイラによって自動合成されます。これを memberwise initializer と呼びます。たとえば次の構造体を考えます。

struct Dog {
    var age: Int
    var name: String
}

このとき、次のような memberwise initializer が合成されます。

init(age: Int, name: String)

ところが、プロパティに初期値を与えた場合、その初期値は memberwise initializer の引数のデフォルト値としては反映されていませんでした。

struct Dog {
    var age: Int = 0
    var name: String
}

このような宣言をした利用者は、age を省略できることを期待して次のように書きがちです。

// name だけ指定したい。sparky は生まれたばかりなので age は 0 のまま
let sparky = Dog(name: "Sparky")

しかし従来はこれがコンパイルエラーになっていました(missing argument for parameter 'age' in call)。プロパティ宣言で = 0 と書いておきながら、イニシャライザ呼び出しでは age を明示的に渡す必要があるという、直感に反する挙動でした。

この問題を避けるためには、結局利用者側で次のようにイニシャライザを自分で書き直す必要があり、memberwise initializer によるボイラープレート削減の恩恵が打ち消されてしまっていました。

struct Dog {
    var age: Int = 0
    var name: String

    // プロパティの初期値をイニシャライザのデフォルト値に反映するために手書きする必要があった
    init(age: Int = 0, name: String) {
        self.age = age
        self.name = name
    }
}

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

memberwise initializer の合成ルールが拡張され、初期値を持つ stored property については、その初期値が引数のデフォルト値として反映されるようになりました。memberwise initializer が合成される条件自体は従来と変わらず、合成できる場合に限り、そこへデフォルト値の情報も追加されます。

基本的な挙動

プロパティに初期値を書くだけで、それが呼び出し側でも省略可能になります。

struct Dog {
    var age: Int = 0
    var name: String

    // 合成される memberwise initializer:
    // init(age: Int = 0, name: String)
}

let sparky = Dog(name: "Sparky") // Dog(age: 0, name: "Sparky") と同じ

より複雑な例として、varlet、初期値の有無を混在させた場合の挙動は次の通りです。

struct Alphabet {
    var a: Int = 97
    let b: String
    var c: String = "c"
    let d: Bool = true
    var e: Double = Double.random(in: 0 ... .pi)

    // 合成される memberwise initializer:
    // init(
    //     a: Int = 97,
    //     b: String,
    //     c: String = "c",
    //     e: Double = Double.random(in: 0 ... .pi)
    // )
}

ここでのポイントは次の通りです。

  • a, c, e は初期値を持つ var なので、memberwise initializer にも同じデフォルト値が付きます。e のように初期値が実行時に評価される式(Double.random(in:))でも問題なく扱われ、イニシャライザ呼び出し時に毎回評価されます。
  • b は初期値を持たない let なので、従来通り引数として要求されます。
  • d は初期値を持つ let であり、値が宣言時点で確定しています。d はそもそも memberwise initializer の引数に含まれません(これは従来からの挙動です)。

このように、デフォルト値が合成されるのは 初期値を持つ var(ミュータブルな stored property) に限られます。let は初期化後に変更できないため、memberwise initializer から値を上書きする余地がないからです。

複数プロパティをまとめて初期化している場合

タプルパターンで複数プロパティを同時に初期化している場合には、個々のプロパティにデフォルト値を振り分けられないため、デフォルト値は合成されません。

struct Person {
    var (firstName, lastName) = ("First", "Last")

    // 合成される memberwise initializer:
    // init(firstName: String, lastName: String)
}

この場合でも memberwise initializer 自体は従来通り合成されますが、firstNamelastName はいずれも引数として明示的に渡す必要があります。