Swift Digest
SE-0018 | Swift Evolution

柔軟なメンバーワイズ初期化

Flexible Memberwise Initialization

Proposal
SE-0018
Authors
Matthew Johnson
Review Manager
Chris Lattner
Status
Returned for revision

このダイジェストはClaude Opus 4.7 / 4.8によって生成されたものです(License)。原文はこちら

01 何が問題だったのか

Swiftの struct には、コンパイラが自動生成するmemberwise initializerがあります。stored propertyを宣言順に並べた引数を受け取り、それぞれを self に代入してくれるイニシャライザで、以下のように自動で用意されます。

struct Point {
    let x: Double
    let y: Double
    // 以下のイニシャライザが自動生成される
    // init(x: Double, y: Double) { self.x = x; self.y = y }
}

この仕組みは便利ですが、当時のSwiftにはいくつかの不便さがありました。

「all or nothing」でしか使えない

自動生成されるmemberwise initializerは次のような制約を抱えていました。

  • struct でカスタムの init を一つでも書いた時点でmemberwise initializerが消え、復活させる手段がない。
  • アクセス制御と相性が悪く、private なpropertyや private(set)var が含まれると、外部向けのmemberwise initializerを使いたくても結局自前で書き直す羽目になる。
  • class にはそもそもmemberwise initializerが生成されない。
  • 初期値のある var propertyであっても、生成されるパラメータにデフォルト値が付かない。
  • lazy var のpropertyに対しては不適切にアクセスしてしまう問題があった。

さらに、生成されるのはイニシャライザ全体であり、「一部のpropertyだけmemberwise風に初期化し、残りは自分で書く」といった部分的な利用ができません。struct を使ったUIコードのように「公開する多数の設定propertyと、実装詳細として隠したい内部状態」が混在する型では、この「all or nothing」の性質が特に痛手でした。

イニシャライザのボイラープレートが M × N で膨らむ

stored propertyが M 個、イニシャライザが N 本あるとき、各イニシャライザが各propertyを個別に代入していくと、記述量は M × N のオーダーで増えていきます。この負担を避けるために、本来は let にしたいpropertyを var にして後から設定したり、妥当な初期値のない var に仮の初期値を与えて「後で設定する」スタイルにしたりと、設計を歪めた回避策が横行していました。結果として、短時間でも意図に反する状態の値が出回ったり、イミュータビリティが弱められたりといった安全性・表現力の面での損失が生じていました。

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

この提案は Returned for revision(差し戻し) のまま再提出されず、最終的にはdeferredなProposal群をまとめて処理する一環として終結しました。そのため、ここで提案されていた memberwise 修飾子も ... プレースホルダも、現在のSwiftには存在しません。現行Swiftにおけるmemberwise initializerの扱いは、提案前からある「struct に対してのみ自動生成される暗黙のイニシャライザ」のまま据え置かれています(その後、var propertyの初期値がパラメータのデフォルト値として反映されるなど、細かな改善は別途行われています)。

以下は、仮に採択されていた場合に導入される予定だった内容の要約です。

memberwise 修飾子と ... プレースホルダ

designated initializerに memberwise 修飾子を付け、パラメータリストに ... プレースホルダを書くと、コンパイラがそのイニシャライザに対して「memberwise化したパラメータ」と「それらを self に代入する処理」を合成する、という仕組みでした。オプトイン方式なので、明示的に書いたイニシャライザだけがこの恩恵を受けます。

struct S {
    let s: String
    let i: Int

    // ユーザーの記述
    memberwise init(...) {}
    // コンパイラが合成するイニシャライザ
    // init(s: String, i: Int) {
    //     self.s = s
    //     self.i = i
    // }
}

初期値のある var propertyについては、対応するパラメータにもその値がデフォルト値として引き継がれる予定でした。

struct S {
    var s: String = "hello"
    var i: Int = 42

    memberwise init(...) {}
    // 合成結果:
    // init(s: String = "hello", i: Int = 42) { ... }
}

propertyの対象範囲

... の位置に合成されるパラメータは、次の条件をすべて満たすstored propertyに対して生成されます。

  1. propertyのアクセスレベルがイニシャライザ以上に公開されている(var propertyではsetterのアクセスレベルで判定)。
  2. memberwise初期化と両立しないbehavior(例: lazy)を持たない。
  3. let propertyの場合は初期値を持たない。

パラメータの並び順は「デフォルト値のないパラメータ → デフォルト値のあるパラメータ」の順で、各グループ内はproperty宣言順です。

手書きパラメータとの併用・部分的な初期化

... の前後に手書きのパラメータを並べて、「公開propertyはmemberwiseで初期化し、非公開のpropertyは本体で手動初期化する」といった部分的な利用ができるよう設計されていました。

struct S {
    let s: String
    private let i: Int

    // ユーザーの記述
    memberwise init(anInt: Int, anotherInt: Int, ...) {
        i = anInt > anotherInt ? anInt : anotherInt
    }
    // 合成結果(i は可視性が低いため memberwise の対象外):
    // init(anInt: Int, anotherInt: Int, s: String) {
    //     self.s = s
    //     i = anInt > anotherInt ? anInt : anotherInt
    // }
}

lazy のように対象外となるpropertyは、memberwiseパラメータから除外され、イニシャライザ本体や初期値の側で面倒を見ることになります。

暗黙のmemberwise initializerの拡張

既存の struct の暗黙のmemberwise initializerも、この提案の仕組みで説明し直される想定でした。struct または適切な class(root classや、引数なしのdesignated initializerを持つスーパークラスを継承したclass)が自前のイニシャライザを宣言していない場合、memberwise init(...) {} に相当する暗黙のイニシャライザが合成されます。可視性は「全stored propertyがmemberwiseの対象になる最大のアクセスレベル」とされ、internal を上限に、private setterなどが混じる場合は private になる、という扱いでした。

利用者として知っておくべきこと

  • 現在のSwiftに memberwise init(...) のような構文はありません。struct の暗黙のmemberwise initializerを使うか、自分で init を書くのが引き続き基本です。
  • 「カスタム init を書いた瞬間に暗黙のmemberwise initializerが消える」問題や、アクセス制御との相性の悪さは、提案が差し戻された後も未解決のまま残りました。現行のSwiftでも、公開したいpropertyと隠したい内部状態が混在する型では、自前でイニシャライザを書くか、暗黙の初期化子を尊重するために型を分けるなどの工夫が必要になります。
  • ただし、初期値のある var propertyについては、その後の言語・コンパイラの進化により、暗黙のmemberwise initializerのパラメータとしてデフォルト値扱いされるようになっています。呼び出し側から引数を省略できるため、当時指摘された不便さの一部は解消しています。

03 今後の見通し

採択時には、コア機能を補強する次のような拡張が後続の提案として検討されると述べられていました。いずれもあくまで方向性として示されたもので、実現を約束するものではなく、本提案の差し戻しによりそのまま実現していません。現行Swiftのmemberwise initializerを考える上での参考情報として触れるに留めます。

@default 属性

let propertyについては、本提案の枠組みでは合成パラメータにデフォルト値を与えることができません。これを補うために、let propertyに対してパラメータ用のデフォルト値を指定できる @default 属性を導入する案が示されていました。@default を修飾子として書く案と、属性引数で値を渡す案の2つの構文が検討されていました。

struct S {
    @default let s: String = "hello"
    @default let i: Int = 42

    memberwise init(...) {}
    // 合成結果:
    // init(s: String = "hello", i: Int = 42) { ... }
}

propertyへの memberwise 修飾子

アクセス制御に基づく自動判定では意図に合わないケースがあるため、property側に memberwise 修飾子を付けて、memberwise化の対象となるpropertyを明示的に指定できるようにする案も検討されていました。これにより、private propertyを public イニシャライザのmemberwiseパラメータに含めたり、逆に public propertyをmemberwiseの対象から外したりといった、より細かな制御が可能になる想定でした。

イニシャライザ専用のアクセスレベル指定

setterのアクセスレベルを別指定する構文と同じスタイルで、memberwise初期化に対するアクセスレベルだけを別指定できるようにする案も挙げられていました。例えば private internal(init) let s: String のように書けば、property自体は private のまま、memberwise初期化の文脈では internal として扱う、といった調整ができる想定でした。

@nomemberwise 属性

特定のpropertyやイニシャライザをmemberwise化の対象から外すための @nomemberwise 属性も構想されていました。propertyに付ければそのpropertyを常にmemberwiseの対象外にでき、イニシャライザに @nomemberwise(prop1, prop2) のように書けばそのイニシャライザに限ってpropertyをmemberwiseから外せる、という仕組みです。

convenience / delegating initializerへのパラメータ転送

convenience initializerやdelegating initializerからdesignated initializerへ、memberwiseパラメータをそのまま転送できる仕組みも望まれていましたが、これは独立した「parameter forwarding」提案で扱うべき汎用的な課題として挙げられていました。

Objective-Cクラスインポートへの対応

Objective-Cのpropertyやイニシャライザに MEMBERWISE 属性を付けてSwiftにインポートし、Cocoaクラスに対してもmemberwise初期化構文を使えるようにする案も示されていました。Objective-Cでは初期化後にsetterを呼び出す形で実現する必要があるため、Swift側のmemberwise初期化とは実装方式が異なる点が前提になっていました。