一个 Swift 宏,用于增强自动成员初始化器,大大减少手动样板代码
在显式开发者提示的指导下,MemberwiseInit 可以更频繁地自动提供您期望的成员初始化器 init
,同时保持与 Swift 成员初始化器一致的安全默认标准。
重要提示
@MemberwiseInit
是一个 Swift 宏,需要 swift-tools-version: 5.9 或更高版本(Xcode 15 或更高版本)。
使用 MemberwiseInit
安装
在 Xcode 中,使用以下方式添加 MemberwiseInit:File
→ Add Package Dependencies…
并输入包 URL
https://github.com/gohanlon/swift-memberwise-init-macro
或者,对于基于 SPM 的项目,将其添加到您的包依赖项中
dependencies: [
.package(url: "https://github.com/gohanlon/swift-memberwise-init-macro", from: "0.5.1")
]
然后将产品添加到所有使用 MemberwiseInit 的目标中
.product(name: "MemberwiseInit", package: "swift-memberwise-init-macro"),
导入 & 基本用法
导入 MemberwiseInit 后,在您的结构体定义之前添加 @MemberwiseInit(.public)
。 这将提供具有公共访问权限的初始化器,或者,如果任何属性的限制性更强,则宏将无法编译并会发出错误诊断。 在这里,age
是私有的会导致宏发出错误
@MemberwiseInit(.public)
public struct Person {
public let name: String
private var age: Int? = nil
//┬──────
//╰─ 🛑 @MemberwiseInit(.public) would leak access to 'private' property
// ✏️ Add '@Init(.public)'
// ✏️ Replace 'private' access with 'public'
// ✏️ Add '@Init(.ignore)'
}
使用 @Init(.ignore)
告诉 MemberwiseInit 忽略 age
@MemberwiseInit(.public)
public struct Person {
public let name: String
@Init(.ignore) private var age: Int? = nil
}
或者,您可以使用 @Init(.public)
将 age
包含在 init
中并公开。
@MemberwiseInit(.public)
public struct Person {
public let name: String
@Init(.public) private var age: Int? = nil
}
MemberwiseInit 包括三个宏
附加到结构体以自动为其提供成员初始化器。
@MemberwiseInit
提供一个内部成员初始化器 init
。
@MemberwiseInit(.public)
以提供的访问级别提供成员初始化器 init
。 有效的访问级别:.private
、.fileprivate
、.internal
、.package
、.public
、.open
。
附加到 @MemberwiseInit
正在为其提供 init
的结构体的属性声明。
@Init
包含否则会被忽略的属性,例如 SwiftUI 的 @State
属性等带属性的属性。
@Init(.ignore)
忽略该成员属性。 忽略属性的访问级别不会导致宏失败,并且该属性不会包含在 init
中。 注意:忽略的属性必须在其他地方初始化。
@Init(.public)
对于提供的 init
,将该属性视为具有与其声明的访问级别不同的访问级别。 有效的访问级别:.private
、.fileprivate
、.internal
、.package
、.public
、.open
。
@Init(default: 42)
为属性的 init
参数指定默认参数值,这对于默认 let
属性是必需的。
@Init(escaping: true)
当属性的 init
参数无法自动为 @escaped
时,为了避免编译器错误,例如,当属性的类型使用表示闭包的类型别名时。
@Init(label: String)
在提供的 init
中分配自定义参数标签。
@Init(label: "_")
使 init
参数无标签。init
中包含的属性之间的命名冲突。 (忽略的属性不会导致冲突。)@Init(.public, default: { true }, escaping: true, label: "where")
所有参数都可以组合使用。
@InitWrapper(type: Binding<String>)
将此属性应用于由属性包装器包装并需要使用属性包装器的类型直接初始化的属性。
@MemberwiseInit
struct CounterView: View {
@InitWrapper(type: Binding<Bool>)
@Binding var isOn: Bool
var body: some View { … }
}
注意 上面的
@InitWrapper
在功能上等效于以下@InitRaw
配置
@InitRaw(assignee: "self._isOn", type: Binding<Bool>)
.
@InitRaw
附加到属性声明以直接配置 MemberwiseInit。
public macro InitRaw(
_ accessLevel: AccessLevelConfig? = nil,
assignee: String? = nil,
default: Any? = nil,
escaping: Bool? = nil,
label: String? = nil,
type: Any.Type? = nil
)
@MemberwiseInit(_optionalsDefaultNil: true)
(实验性)
当设置为 true
时,为所有可选属性提供一个默认的 init
参数值 nil
。 对于非公共初始化器,可选的 var
属性默认为 nil
,除非此参数显式设置为 false
。
@MemberwiseInit(_deunderscoreParameters: true)
(实验性)
从生成的 init
参数名称中删除下划线前缀,除非这样做会导致命名冲突。 忽略的属性不会导致冲突,并且可以使用 @Init(label:)
覆盖。
actor
、class
上的 @MemberwiseInit
(实验性)
可以附加到 actor 和 class。
@_UncheckedMemberwiseInit
(实验性)
为所有属性生成一个成员初始化器,无论访问级别如何,并减少编译时安全检查(与 @MemberwiseInit
相比)。
要控制所提供初始化器中参数的命名,请使用 @Init(label: String)
。 提示:对于无标签/通配符参数,请使用 @Init(label: "_")
。
使用 @Init(label: String)
自定义您的初始化器参数标签
无标签/通配符参数
@MemberwiseInit
struct Point2D {
@Init(label: "_") let x: Int
@Init(label: "_") let y: Int
}
产生
init(
_ x: Int,
_ y: Int
) {
self.x = x
self.y = y
}
自定义参数标签
@MemberwiseInit
struct Receipt {
@Init(label: "for") let item: String
}
产生
init(
for item: String // 👈
) {
self.item = item
}
当属性使用其语法暗示类型信息的表达式(例如,大多数 Swift 字面量)初始化时,不需要显式类型注释
@MemberwiseInit
struct Example {
var count = 0 // 👈 `Int` is inferred
}
显式类型规范可能会让人觉得多余。 有帮助的是,Swift 的成员初始化器会从任意表达式推断类型。
MemberwiseInit 作为 Swift 宏,在语法级别运行,并且本身不了解类型信息。 尽管如此,支持许多仅从其语法暗示类型的表达式,包括以下所有表达式
@MemberwiseInit
public struct Example<T: CaseIterable> {
var string = "", int = 0
var boolTrue = true
var mixedDivide = 8.0 / 4 // Double
var halfOpenRange = 1.0..<5 // Range<Double>
var arrayTypeInit = [T]()
var arrayIntLiteral = [1, 2, 3]
var arrayPromoted = [1, 2.0] // [Double]
var nestedArray = [[1, 2], [20, 30]] // [[Int]]
var dictionaryTypeInit = [String: T]()
var dictionaryLiteral = ["key1": 1, "key2": 2]
var dictionaryPromoted = [1: 2.0, 3.0: 4] // [Double: Double]
var nestedDictionary = ["key1": ["subkey1": 10], "key2": ["subkey2": 20]] // [String: [String: Int]]
var tuple = (1, ("Hello", true))
var value = T.allCases.first as T?
var nestedMixed = ((1 + 2) * 3) >= (4 / 2) && ((true || false) && !(false)) // Bool
var bitwiseAnd = 0b1010 & 0b0101
var leftShift = 1 << 2
var bitwiseNotInt = ~0b0011
var intBinary = 0b01010101
var intOctal = 0o21
var intHex = 0x1A
var floatExponential = 1.25e2 // Double
var floatHex = 0xC.3p0 // Double
var arrayAs = [1, "foo", 3] as [Any]
var dictionaryAs = ["foo": 1, 3: "bar"] as [AnyHashable: Any]
}
使用 @Init(default: Any)
在初始化器中设置默认参数值。 这对于 let
属性特别有用,否则无法在声明后对其进行默认设置。 对于 var
属性,考虑使用声明初始化器(例如,var number = 0
)作为最佳实践。
与 Swift 一样,MemberwiseInit 利用变量初始化器来为 var
属性分配默认值
@MemberwiseInit
struct UserSettings {
var theme = "Light"
var notificationsEnabled = true
}
这产生
internal init(
theme: String = "Light",
notificationsEnabled: Bool = true
) {
self.theme = theme
self.notificationsEnabled = notificationsEnabled
}
对于 let
属性,@Init(default:)
允许在初始化器中设置默认值
@MemberwiseInit
struct ButtonStyle {
@Init(default: Color.blue) let backgroundColor: Color
@Init(default: Font.system(size: 16)) let font: Font
}
这产生
internal init(
backgroundColor: Color = Color.blue,
font: Font = Font.system(size: 16)
) {
self.backgroundColor = backgroundColor
self.font = font
}
使用 @Init(.ignore)
将属性从 MemberwiseInit 的初始化器中排除; 确保忽略的属性以其他方式初始化以避免编译器错误。
@Init(.ignore)
属性将属性从初始化器中排除,从而可能允许 MemberwiseInit 为其余属性生成更易于访问的初始化器。
例如
@MemberwiseInit(.public)
public struct Person {
public let name: String
@Init(.ignore) private var age: Int? = nil // 👈 Ignored and given a default value
}
通过将 age
标记为忽略,MemberwiseInit 创建了一个没有 age
参数的公共初始化器
public init(
name: String
) {
self.name = name
}
如果未将 age
标记为忽略,则 MemberwiseInit 将无法编译并提供诊断信息。
注意 与 Swift 的成员初始化器一致,MemberwiseInit 会自动忽略具有已分配默认值的
let
属性,因为在初始化器中重新分配此类属性将无效。
如果 MemberwiseInit 忽略了一个带属性的属性并导致编译器错误,您有两种直接的补救方法
@Init
注释显式地将该属性包含在初始化器中。与编译器的默认行为不同,MemberwiseInit 在处理附加了属性的成员属性时采取了更为谨慎的方法。
对于基于 SwiftUI 的说明,让我们看一下没有 MemberwiseInit 的视图
import SwiftUI
struct MyView: View {
@State var isOn: Bool
var body: some View { … }
}
Swift 提供了以下内部成员初始化器 init
internal init(
isOn: Bool
) {
self.isOn = isOn
}
然而,以这种方式初始化 @State
属性是 SwiftUI 中的一个常见陷阱。 isOn
状态仅在视图的初始渲染时分配,并且此分配不会在后续渲染时发生。 为了防止这种情况,MemberwiseInit 默认忽略带属性的属性
import SwiftUI
@MemberwiseInit(.internal) // 👈
struct MyView: View {
@State var isOn: Bool
var body: some View { … }
}
这导致 MemberwiseInit 提供了以下初始化器
internal init() {
} // 🛑 Compiler error:↵
// Return from initializer without initializing all stored properties
从这里,您有两种选择
分配一个默认值
将属性默认为一个值使提供的 init
有效,因为提供的 init
不再需要初始化该属性。
import SwiftUI
@MemberwiseInit(. internal)
struct MyView: View {
@State var isOn: Bool = false // 👈 Default value provided
var body: some View { … }
}
生成的 init
是
internal init() {
} // 🎉 No error, all stored properties are initialized
使用 @Init
注释
如果您了解属性赋予的行为,则可以使用 @Init
显式标记该属性以将其包含在初始化器中。
import SwiftUI
@MemberwiseInit(.internal)
struct MyView: View {
@Init @State var isOn: Bool // 👈 `@Init`
var body: some View { … }
}
这产生
internal init(
isOn: Bool
) {
self.isOn = isOn
}
将 @InitWrapper
应用于由属性包装器包装并需要使用属性包装器的类型直接初始化的属性。 例如,这是一个与 SwiftUI 的 @Binding
简单的用法
import SwiftUI
@MemberwiseInit
struct CounterView: View {
@InitWrapper(type: Binding<Int>)
@Binding var count: Int
var body: some View { … }
}
这产生
internal init(
count: Binding<Int>
) {
self._count = count
}
MemberwiseInit 自动将初始化器参数中的闭包标记为 @escaping
。 如果对闭包使用类型别名,请使用 @Init(escaping: true)
显式注释该属性。
Swift 宏在语法级别运行,并且本身不了解类型信息。 MemberwiseInit 将为闭包类型添加 @escaping
,前提是闭包类型直接声明为属性的一部分。 幸运的是,这是典型的场景。
相比之下,Swift 的成员初始化器具有使用类型信息的优势。 这使得它即使在类型别名中“隐藏”了闭包类型时也能识别并添加 @escaping
。
考虑以下结构体
public struct TaskRunner {
public let onCompletion: () -> Void
}
通过观察(或深入研究编译器的源代码),我们可以看到 Swift 自动提供了以下内部 init
internal init(
onCompletion: @escaping () -> Void // 🎉 `@escaping` automatically
) {
self.onCompletion = onCompletion
}
现在,使用 MemberwiseInit
@MemberwiseInit // 👈
public struct TaskRunner {
public let onCompletion: () -> Void
}
我们得到相同的 init
,我们可以使用 Xcode 的“展开宏”命令来检查
internal init(
onCompletion: @escaping () -> Void // 🎉 `@escaping` automatically
) {
self.onCompletion = onCompletion
}
我们可以让 MemberwiseInit 提供一个公共的 init
@MemberwiseInit(.public) // 👈 `.public`
public struct TaskRunner {
public let onCompletion: () -> Void
}
这产生
public init( // 🎉 `public`
onCompletion: @escaping () -> Void
) {
self.onCompletion = onCompletion
}
现在,假设 onCompletion
的类型变得更加复杂,我们决定提取类型别名
public typealias CompletionHandler = @Sendable () -> Void
@MemberwiseInit(.public)
public struct TaskRunner: Sendable {
public let onCompletion: CompletionHandler
}
由于 Swift 宏本身不了解类型信息,因此 MemberwiseInit 无法“看到”CompletionHandler
代表需要标记为 @escaping
的闭包类型。 这会导致编译器错误
public init(
onCompletion: CompletionHandler // 👈 Missing `@escaping`!
) {
self.onCompletion = onCompletion // 🛑 Compiler error:↵
// Assigning non-escaping parameter 'onCompletion' to an @escaping closure
}
为了解决这个问题,当对闭包使用类型别名时,您必须使用 @Init(escaping: true)
显式标记该属性
public typealias CompletionHandler = @Sendable () -> Void
@MemberwiseInit(.public)
public struct TaskRunner: Sendable {
@Init(escaping: true) public let onCompletion: CompletionHandler // 👈
}
这会产生以下有效且可检查的公共 init
public init(
onCompletion: @escaping CompletionHandler // 🎉 Correctly `@escaping`
) {
self.onCompletion = onCompletion
}
@_UncheckedMemberwiseInit
是一个实验性宏,它绕过编译时安全检查和严格的访问控制强制。 它为类型的所有属性生成一个初始化器,无论其声明的访问级别如何。 请谨慎使用它。
主要特点
@MemberwiseInit
不同)@MemberwiseInit
相同的使用模式示例
@_UncheckedMemberwiseInit(.public)
public struct APIResponse: Codable {
public let id: String
@Monitored internal var statusCode: Int
private var rawResponse: Data
// Computed properties and methods...
}
这将产生一个包含所有属性的公共初始化器,无论其访问级别或属性如何。 与 @MemberwiseInit
不同,此宏不需要 @Init
注释或任何其他显式选择加入。 生成的初始化器是
public init(
id: String,
statusCode: Int,
rawResponse: Data
) {
self.id = id
self.statusCode = statusCode
self.rawResponse = rawResponse
}
注意 倾向于在属性级别使用
@Init(label:)
显式指定非下划线名称 —@MemberwiseInit(_deunderscoreParmeters:)
可能很快会被弃用。
设置 @MemberwiseInit(_deunderscoreParmeters: true)
可以在生成初始化器参数名称时,移除属性名称中的下划线前缀。 如果希望保留下划线或为特定属性提供自定义标签,请使用 @Init(label: String)
。
如果移除下划线会导致初始化器中包含的属性名称冲突,MemberwiseInit 将不会移除下划线。(忽略的属性不会导致冲突。)
在 Swift 中,以下划线为前缀的属性通常用作内部存储或支持属性。 设置 _deunderscoreParameters: true
会遵守此约定,生成省略下划线的初始化器参数名称。
@MemberwiseInit(.public, _deunderscoreParmeters: true)
public struct Review {
@Init(.public) private let _rating: Int
public var rating: String {
String(repeating: "⭐️", count: self._rating)
}
}
这产生
public init(
rating: Int // 👈 Non-underscored parameter
) {
self._rating = rating
}
要在属性级别覆盖移除下划线的行为,请使用 @Init(label: String)
。
@MemberwiseInit(.public, _deunderscoreParameters: true)
public struct Review {
@Init(.public, label: "_rating") private let _rating: Int
}
这产生
public init(
_rating: Int // 👈 Underscored parameter
) {
self._rating = _rating
}
使用 @MemberwiseInit(_optionalsDefaultNil: Bool)
显式控制提供的初始化器中可选属性是否默认为 nil
。
_optionalsDefaultNil: true
会将所有可选属性默认为 nil
,但会牺牲编译时指导。_optionalsDefaultNil: false
可确保 MemberwiseInit 永远不会将可选属性默认为 nil
。MemberwiseInit 关于可选属性的默认行为与 Swift 的成员逐一初始化器保持一致。
var
可选属性会自动默认为 nil
。_optionalsDefaultNil
设置为 true
。let
可选属性永远不会自动默认为 nil
。 将 _optionalsDefaultNil
设置为 true
是导致它们默认为 nil
的唯一方法。注意 使用
@Init(default:)
通常指定默认值,这是一种更安全、更明确的替代_optionalsDefaultNil
的方法。
使用 _optionalsDefaultNil
,你可以控制 Swift 成员逐一初始化的默认行为。 并且,它允许你显式选择让你的公共初始化器将可选属性默认为 nil
。
简化实例化是 _optionalsDefaultNil
的主要目的,当你的类型镜像松散结构的外部依赖项(例如,镜像 HTTP API 的 Codable
结构体)时,此功能尤其有用。 但是,_optionalsDefaultNil
存在一个缺点:当属性更改时,编译器不会标记过时的实例化,从而可能导致意外的 nil
赋值和潜在的运行时错误。
在 Swift 中
var
属性声明自然会导致 Swift 和 MemberwiseInit 的初始化器中的默认成员逐一 init
参数值。let
属性变为不可变,因此不能用于指定默认的 init
参数值。例如,可以将 var
属性声明初始化为 nil
@MemberwiseInit(.public)
public struct User {
public var name: String? = nil // 👈
}
_ = User() // 'name' defaults to 'nil'
产生
public init(
name: String? = nil // 👈
) {
self.name = name
}
这对于 let
属性是不可行的
@MemberwiseInit(.public)
public struct User {
public let name: String? = nil // ✋ 'name' is 'nil' forever
}
在适当的情况下,_optionalsDefaultNil
可以是在生成的初始化器中将可选属性默认为 nil
的一种便捷方式。
@MemberwiseInit(.public, _optionalsDefaultNil: true)
public struct User: Codable {
public let id: Int
public let name: String?
public let email: String?
public let address: String?
}
产生
public init(
id: Int,
name: String? = nil,
email: String? = nil,
address: String? = nil
) {
self.id = id
self.name = name
self.email = email
self.address = address
}
不支持在属性声明中使用元组语法。
@MemberwiseInit
struct Point2D {
let (x, y): (Int, Int)
//┬─────────────────────
//╰─ 🛑 @MemberwiseInit does not support tuple destructuring for
// property declarations. Use multiple declartions instead.
}
Swift 自动提供的成员逐一初始化器巧妙地减少了结构体的样板代码。 但是,它们必须始终谨慎,以确保不会对开发人员的意图做出任何假设。 虽然这种保守的方法对于避免意外行为至关重要,但它也常常导致返回使用样板初始化器。
Swift 的成员逐一初始化器无法假定应该从外部模块构造公共类型,因此它永远不会提供访问级别高于“internal”的初始化器。 安全地向类型添加公共初始化器需要开发人员的明确意图。 传统上,这意味着手动声明初始化器,或使用 Xcode 生成样板初始化器。 看一下这个简单的例子
public struct Person {
public let name: String
}
Swift 透明地添加以下熟悉的 init
internal init(
name: String
) {
self.name = name
}
MemberwiseInit 可以提供完全相同的 init
@MemberwiseInit // 👈
public struct Person {
public let name: String
}
与 Swift 的成员逐一初始化器不同,你可以通过右键单击 @MemberwiseInit
并选择“Expand Macro”,使用 Xcode 检查 MemberwiseInit 的初始化器。
注意 引入显式的
init
会阻止添加 Swift 的成员逐一初始化器。 MemberwiseInit 的初始化器始终会添加,并且可以与你的其他初始化器共存,即使对于直接遵循指定init
的协议(如Decodable
和RawRepresentable
)的类型也是如此。1
与 Swift 的成员逐一初始化器相比,MemberwiseInit 可以提供任何访问级别的初始化器,包括 public。 你可以通过使用 @MemberwiseInit(.public)
标记 Person
来显式指示 MemberwiseInit 提供 public 的 init
。
@MemberwiseInit(.public) // 👈 `.public`
public struct Person {
public let name: String
}
通过此调整,展开宏会产生
public init( // 🎉 `public`
name: String
) {
self.name = name
}
假设你随后向 Person
添加了一个私有成员
@MemberwiseInit(.public)
public struct Person {
public let name: String
private var age: Int? // 👈 `private`
}
现在,与 Swift 的成员逐一初始化器必须降级为提供私有 init
不同,MemberwiseInit 会失败并显示诊断信息
@MemberwiseInit(.public)
public struct Person {
public let name: String
private var age: Int?
//┬──────
//╰─ 🛑 @MemberwiseInit(.public) would leak access to 'private' property
// ✏️ Add '@Init(.public)'
// ✏️ Replace 'private' access with 'public'
// ✏️ Add '@Init(.ignore)' and an initializer
}
注意 默认情况下,Swift 和 MemberwiseInit 的成员逐一初始化器都是安全的。 两者都不会提供无意中泄漏对更多受限属性的访问权限的初始化器。
要通过 MemberwiseInit 的初始化器公开暴露 age
,请使用 @Init(.public)
标记它
@MemberwiseInit(.public)
public struct Person {
public let name: String
@Init(.public) private var age: Int? // 👈 `@Init(.public)`
}
现在 MemberwiseInit 提供了一个公开 private age
属性的 public init
public init( // 👈 `public`
name: String,
age: Int? // 👈 Exposed deliberately
) {
self.name = name
self.age = age
}
与 Swift 的成员逐一初始化器相比,MemberwiseInit 的方法有几个优点
@MemberwiseInit(.public)
是开发人员明确意图的声明,从而避免了关于初始化器所需访问级别的任何歧义。@MemberwiseInit
,并且可以通过诊断消息对 MemberwiseInit 的即时反馈做出响应来纠正大多数使用问题2。让我们给 age
一个默认值
@MemberwiseInit(.public)
public struct Person {
public let name: String
@Init(.public) private var age: Int? = nil // 👈 Default value
}
现在 MemberwiseInit 的 init
参数包括默认的 age
值
public init(
name: String,
age: Int? = nil // 👈 Default value
) {
self.name = name
self.age = age
}
假设我们不想通过 init
公开暴露 age
。 只要以其他方式初始化 age
(例如,使用默认值声明),我们就可以使用 @Init(.ignore)
明确告诉 MemberwiseInit 忽略它
@MemberwiseInit(.public)
public struct Person {
public let name: String
@Init(.ignore) private var age: Int? = nil // 👈 `.ignore`
}
现在 MemberwiseInit 忽略了私有的 age
属性并提供了 public 的 init
public init( // 👈 `public`, ignoring `age` property
name: String
) {
self.name = name
}
MemberwiseInit 在 MIT 许可下可用。 有关更多信息,请参见 LICENSE 文件。
当存在任何显式的 init
时,Swift 会省略其成员逐一初始化器。 你可以执行一个 “扩展舞蹈” 来保留 Swift 的成员逐一 init
,但会带来强制的权衡。 ↩
MemberwiseInit 目前有一些诊断信息,并附带 fix-its。 但是,它正在积极努力提供更广泛和更全面的 fix-its 集。 目前还存在一些使用错误留给编译器检查提供的 init
,这些错误将来可能会直接解决,例如,与其隐式忽略使用诸如 @State
之类的属性标记的属性,不如 MemberwiseInit 可能会引发诊断错误和 fix-its,以添加 @Init
,@Init(.ignore)
或为变量声明分配默认值。 ↩