Flexible Memberwise Initialization
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が生成されない。- 初期値のある
varpropertyであっても、生成されるパラメータにデフォルト値が付かない。 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に対して生成されます。
- propertyのアクセスレベルがイニシャライザ以上に公開されている(
varpropertyではsetterのアクセスレベルで判定)。 - memberwise初期化と両立しないbehavior(例:
lazy)を持たない。 letpropertyの場合は初期値を持たない。
パラメータの並び順は「デフォルト値のないパラメータ → デフォルト値のあるパラメータ」の順で、各グループ内は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 になる、という扱いでした。
今後の方向性(Future Directions)
採択時には、次のような拡張が後続の提案として検討されると述べられていましたが、いずれも本提案の差し戻しにより実現していません。現行Swiftのmemberwise initializerを考える上でも参考情報として触れるに留めます。
letpropertyにデフォルト値を与えるための@default属性。- propertyに
memberwise修飾子を付けて、アクセス制御と独立にmemberwise化の対象を明示できるようにする案。 - イニシャライザ専用のアクセスレベル指定(例:
private internal(init))。 - 特定のpropertyやイニシャライザをmemberwise化の対象から外す
@nomemberwise属性。 - convenience / delegating initializerへのパラメータ転送。
- Objective-Cクラスインポート時のmemberwise対応。
利用者として知っておくべきこと
- 現在のSwiftに
memberwise init(...)のような構文はありません。structの暗黙のmemberwise initializerを使うか、自分でinitを書くのが引き続き基本です。 - 「カスタム
initを書いた瞬間に暗黙のmemberwise initializerが消える」問題や、アクセス制御との相性の悪さは、提案が差し戻された後も未解決のまま残りました。現行のSwiftでも、公開したいpropertyと隠したい内部状態が混在する型では、自前でイニシャライザを書くか、暗黙の初期化子を尊重するために型を分けるなどの工夫が必要になります。 - ただし、初期値のある
varpropertyについては、その後の言語・コンパイラの進化により、暗黙のmemberwise initializerのパラメータとしてデフォルト値扱いされるようになっています。呼び出し側から引数を省略できるため、当時指摘された不便さの一部は解消しています。