@MemberwiseInit

GitHub Workflow Status (with event)

一个 Swift 宏,用于增强自动成员初始化器,大大减少手动样板代码

swift-memberwise-init-hero04

在显式开发者提示的指导下,MemberwiseInit 可以更频繁地自动提供您期望的成员初始化器 init,同时保持与 Swift 成员初始化器一致的安全默认标准。

重要提示

@MemberwiseInit 是一个 Swift 宏,需要 swift-tools-version: 5.9 或更高版本(Xcode 15 或更高版本)。

快速开始

使用 MemberwiseInit

  1. 安装
    在 Xcode 中,使用以下方式添加 MemberwiseInit:FileAdd 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"),
  2. 导入 & 基本用法
    导入 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 正在为其提供 init 的结构体的属性声明。

@InitWrapper(type:)

等等

特性和限制

自定义 init 参数标签

要控制所提供初始化器中参数的命名,请使用 @Init(label: String)。 提示:对于无标签/通配符参数,请使用 @Init(label: "_")

说明

使用 @Init(label: String) 自定义您的初始化器参数标签

  1. 无标签/通配符参数

    @MemberwiseInit
    struct Point2D {
      @Init(label: "_") let x: Int
      @Init(label: "_") let y: Int
    }

    产生

    init(
      _ x: Int,
      _ y: Int
    ) {
      self.x = x
      self.y = y
    }
  2. 自定义参数标签

    @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]
}

默认值,即使是 let 属性

使用 @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 忽略了一个带属性的属性并导致编译器错误,您有两种直接的补救方法

  1. 为该属性分配一个默认值。
  2. 使用 @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

从这里,您有两种选择

  1. 分配一个默认值
    将属性默认为一个值使提供的 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
  2. 使用 @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
}

自动 @escaping 用于闭包类型(通常)

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 是一个实验性宏,它绕过编译时安全检查和严格的访问控制强制。 它为类型的所有属性生成一个初始化器,无论其声明的访问级别如何。 请谨慎使用它。

主要特点

示例

@_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
}

实验性:将可选类型默认为 nil

使用 @MemberwiseInit(_optionalsDefaultNil: Bool) 显式控制提供的初始化器中可选属性是否默认为 nil

MemberwiseInit 关于可选属性的默认行为与 Swift 的成员逐一初始化器保持一致。

注意 使用 @Init(default:) 通常指定默认值,这是一种更安全、更明确的替代 _optionalsDefaultNil 的方法。

说明

使用 _optionalsDefaultNil,你可以控制 Swift 成员逐一初始化的默认行为。 并且,它允许你显式选择让你的公共初始化器将可选属性默认为 nil

简化实例化是 _optionalsDefaultNil 的主要目的,当你的类型镜像松散结构的外部依赖项(例如,镜像 HTTP API 的 Codable 结构体)时,此功能尤其有用。 但是,_optionalsDefaultNil 存在一个缺点:当属性更改时,编译器不会标记过时的实例化,从而可能导致意外的 nil 赋值和潜在的运行时错误。

在 Swift 中

例如,可以将 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 的协议(如 DecodableRawRepresentable)的类型也是如此。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 的方法有几个优点

  1. 明确的意图@MemberwiseInit(.public) 是开发人员明确意图的声明,从而避免了关于初始化器所需访问级别的任何歧义。
  2. 安全:通过在未满足预期时快速失败,MemberwiseInit 可防止可能损害代码的封装和安全性的意外访问级别泄漏。 也就是说,它在默认情况下仍然是安全的。
  3. 更简单:MemberwiseInit 的复杂性降低使其更易于使用,因为其行为更直接和可预测。
  4. 可学习:可以简单地应用 @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 文件。

脚注

  1. 当存在任何显式的 init 时,Swift 会省略其成员逐一初始化器。 你可以执行一个 “扩展舞蹈” 来保留 Swift 的成员逐一 init,但会带来强制的权衡。

  2. MemberwiseInit 目前有一些诊断信息,并附带 fix-its。 但是,它正在积极努力提供更广泛和更全面的 fix-its 集。 目前还存在一些使用错误留给编译器检查提供的 init,这些错误将来可能会直接解决,例如,与其隐式忽略使用诸如 @State 之类的属性标记的属性,不如 MemberwiseInit 可能会引发诊断错误和 fix-its,以添加 @Init@Init(.ignore) 或为变量声明分配默认值。