メンバーワイズイニシャライザからprivateな初期化済みプロパティを除外する
Exclude private initialized properties from memberwise initializer
このダイジェストはClaude Opus 4.7 / 4.8によって生成されたものです(License)。原文はこちら↗。
01 何が問題だったのか
struct に対して自動合成されるmemberwise initializerは、すべてのstored propertyを引数にとってひとつずつ初期化してくれる便利な仕組みです。たとえば次のコードでは init(x: Int, y: Int) が合成されます。
struct S {
var x: Int
var y: Int
}
let s = S(x: 1, y: 2)
しかしこの合成には落とし穴がありました。private や fileprivate で初期値を持つpropertyをひとつでも追加すると、そのpropertyもmemberwise initializerに含まれてしまい、initializer全体のアクセスレベルがそのpropertyに合わせて下がってしまいます。
struct S {
var x: Int
var y: Int
private var z = 0
}
let s = S(x: 1, y: 2) // error: 'S' initializer is inaccessible due to 'private' protection level
ここでは z に初期値があるため、本来は S(x:y:) という形で呼び出せれば十分なのに、z がinitializerの引数として含まれたうえでinitializer自体が private になってしまい、型の外から使えません。結果として、利用者は自分でmemberwise initializerを手書きで定義し直す羽目になりがちです。
この挙動はとくにproperty wrapper相当のふるまいを提供しようとする attached macro にとって厄介です。組み込みのproperty wrapperでは、コンパイラが private な backing property をmemberwise initializerから除外してくれますし、property自体も初期値を持ち fileprivate か private ならば除外されます。
@propertyWrapper
struct Wrapper<T> {
var wrappedValue: T
}
struct S {
@Wrapper private var x = 1
var y: Int
}
let s = S(y: 2) // Okay
同じことをmacroで再現しようとすると、macro展開で private な backing property を追加する必要があるため、現状のルールではmemberwise initializerが private に格下げされてしまい、ソース互換なまま置き換えることができません。
02 どのように解決されるのか
memberwise initializerに含めるpropertyの判定ルールを変更し、「initializer全体のアクセスレベルよりも低く、かつ初期値を持つproperty」は自動的に除外するようにします。あわせてソース互換のために旧シグネチャのinitializerも互換用オーバーロードとして合成されます。
新しい判定ルール
memberwise initializerのアクセスレベルは、memberwise initializableなpropertyのうち最も高いものに合わせて決まり、ただし上限は internal です。明示的なアクセス修飾子を持たないpropertyは、囲む型のアクセスレベル((file)private な型の場合は事実上 fileprivate)を持つものとして扱われます。
そのうえで、次のようなpropertyだけがmemberwise initializerの引数から外れます。
- そのinitializerの最大アクセスレベルより低いアクセスレベルを持ち、かつ
- 初期値を持っている
ここでいう「初期値を持つ」とは、private var x = 0 のように明示的な初期値がある場合だけでなく、private var x: Int? のように Optional などでデフォルト初期化される場合も含みます。init accessorを持つcomputed propertyでも、初期値があれば同様に対象になります。
このルール変更により、冒頭の例は次のようにそのまま使えるようになります。
struct S {
var x: Int
var y: Int
private var z = 0
}
let s = S(x: 1, y: 2) // OK
x と y の最大アクセスレベルが internal なので、memberwise initializerも internal になり、それより低い private の z は初期値を持つため除外されます。z が fileprivate でも同じ結果になります。
ルールが働かないケース
すべてのpropertyが private など同じアクセスレベルのときは、initializer自体の最大アクセスレベルもそこまで下がるので、除外は起こりません。
struct S {
private var x = 0
private var y: Int?
}
// init(x: Int = 0, y: Int? = nil) は private のまま、両方とも含まれる
y を fileprivate に変えれば、最大アクセスレベルが fileprivate に上がり、x が除外されて fileprivate init(y: Int? = nil) になります。
また、初期値を持たないpropertyは引き続きinitializerの引数として必須になるため、除外されません。従来と同様にinitializerのアクセスレベルがpropertyに引きずられます。
struct S {
var x: Int
private var y: Int
}
let s = S(x: 0, y: 1) // error: 'S' initializer is inaccessible due to 'private' protection level
型自体が (file)private のときは、無指定のpropertyも実質 fileprivate なので、最大アクセスレベルは fileprivate になり、どちらのpropertyも含まれます。
fileprivate struct S {
var x: Int?
fileprivate var y: Int?
}
let s = S(x: 0, y: 1) // OK
memberwise initializerの上限は internal なので、public propertyがあっても挙動は変わりません。次の例の合成initializerは internal init(x: Int = 0, y: Int = 0) のままです。
public struct S {
public var x: Int = 0
var y: Int = 0
}
互換用オーバーロード
ルール変更だけでは、既存コードが同一ファイル内で private propertyをmemberwise initializerから初期化していた場合にソース互換が壊れます。そこでコンパイラは、従来と同じ引数を取る互換用オーバーロードも引き続き合成します。新旧どちらの呼び出し方も同じファイル内からは動作するため、同一モジュール内の利用はほとんど壊れません(memberwise initializerはもともと最大でも internal なので、影響範囲は定義元のモジュールに限られます)。
それでも次の2つのケースは互換用オーバーロードでは救えません。
let fn = S.initのようにコンテキスト型を持たないベース名参照では、デフォルト引数が少ない新しいinitializerが選ばれるため、下流で引数不足のエラーになりうる- 同じ型のextensionで、新しいmemberwise initializerと同じシグネチャのinitializerがすでに定義されている場合、合成の抑制が働かないため redeclaration エラーになる
いずれも実運用ではごく稀なケースです。
03 今後の見通し
互換用オーバーロードの段階的廃止
将来の言語モードでは、互換用オーバーロードを取り除くことが検討されています。まずは互換用オーバーロードを利用している箇所に対してwarningとfix-itで明示的なinitializerへの置き換えを促し、そのうえで新しい言語モードでは互換用オーバーロードの合成自体をやめる、という方向性です。あわせて、互換用オーバーロードなしの新しい挙動を先行して試すためのupcoming feature flagを用意する案にも触れられています。あくまで将来の方向性として示されているものであり、実現を約束するものではありません。
memberwise initializerのフルカスタマイズ
どのpropertyをmemberwise initializerに含めるか、またそのアクセスレベルをどうするかをユーザーが明示的に指定できる仕組みも検討対象として挙げられています。propertyに付ける属性として実現する案や、次のような #memberInit(...) 風のmacroライクな構文、あるいは過去のSE-0018で検討された memberwise キーワードを使う案などが示されています。
struct S {
private var x = 0
var y: Int
var z = "hello"
public #memberInit(x, y)
}
ただし、型注釈のないpropertyのinitializerを型チェックする必要があるため、現行のmacro機能の範囲では実現できないとされています。デフォルトのmemberwise initializerの改善(本Proposalの内容)をブロックするものではないと位置づけられており、こちらも将来の可能性の議論にとどまっています。