要合并两个 Swift 结构体值,请编写一个函数,为每个字段选择来源并返回新值。这个 macOS 示例组合两种合成的绘图面板样式,并明确规定替换、保留、可选值清除和无效输入的处理规则。
编写合并代码前先明确优先规则
读者要做的是在 Mac 上根据两个现有值计算出一种绘图面板样式。将输入称为 base 和 incoming:这些名称说明了它们各自的角色,而字段级约定决定哪个值优先。计算只使用内存中的合成值。
Swift 将结构体描述为值类型,并支持通过初始化器创建实例。Apple 文档中的 merge(_:uniquingKeysWith:) API 属于 Dictionary,它通过闭包处理重复键。对于这个自定义结构体,应定义一个合并函数,在实现中表达预期的字段规则。结构体和类、Dictionary merge(_:uniquingKeysWith:)。
以下是一个示例约定。这些是本示例的设计选择,并非 Swift 自动提供的行为。
| 字段 | 规则 | 示例结果 |
|---|---|---|
title |
使用传入文本,去除首尾空白和换行符,然后拒绝空结果 | " Tile Study \n" 变为 \"Tile Study\" |
spacing |
保留基值;要求其为非负数 | 基值 12 优先于传入值 30 |
showsGrid |
原样使用传入的布尔值 | 传入值 false 替换基值 true |
caption |
原样使用传入的可选值 | 传入值 nil 清除基值标题 |
此表区分了三个容易混淆的操作:选择来源、规范化所选值,以及验证结果。传入的间距会被刻意忽略。无论它与基值间距是否不同,都不会改变输出。
Foundation 的 trimmingCharacters(in:) 会返回一个新字符串,其中首尾匹配的字符已被移除。Apple 文档说明可使用 .whitespacesAndNewlines 去除首尾空白;如果字符串完全由这些字符构成,结果就会变为空字符串。拒绝这个空结果是我们额外添加的规则。Foundation trimmingCharacters(in:)。
在 Mac 上运行完整的本地示例
此示例使用 Swift 6 工具链。Swift.org 提供通过 Swiftly 在 macOS 上安装的说明,并将 Xcode 列为 Apple 平台开发的选项。打开 Terminal 并运行 swift --version,确认命令可用。如果找不到该命令,请先按照安装说明操作。在 macOS 上安装 Swift、入门指南。
将以下完整程序保存到工作文件夹中的 MergeStyles.swift。程序导入 Foundation,定义两个输入值,执行合并,检查预期结果,并测试两种拒绝情况。此计算不会发送网络请求,也不会存储应用程序数据。
import Foundation
struct PanelStyle: Equatable, Sendable {
let title: String
let spacing: Int
let showsGrid: Bool
let caption: String?
}
enum StyleMergeError: Error {
case emptyTitle
case negativeSpacing
}
func mergeStyles(
base: PanelStyle,
incoming: PanelStyle
) throws -> PanelStyle {
let title = incoming.title.trimmingCharacters(
in: .whitespacesAndNewlines
)
guard !title.isEmpty else {
throw StyleMergeError.emptyTitle
}
guard base.spacing >= 0 else {
throw StyleMergeError.negativeSpacing
}
return PanelStyle(
title: title,
spacing: base.spacing,
showsGrid: incoming.showsGrid,
caption: incoming.caption
)
}
let base = PanelStyle(
title: "Shape Study",
spacing: 12,
showsGrid: true,
caption: "Blue squares"
)
let incoming = PanelStyle(
title: " Tile Study \n",
spacing: 30,
showsGrid: false,
caption: nil
)
let expected = PanelStyle(
title: "Tile Study",
spacing: 12,
showsGrid: false,
caption: nil
)
let result = try mergeStyles(base: base, incoming: incoming)
precondition(result == expected)
precondition(base.title == "Shape Study")
precondition(base.caption == "Blue squares")
precondition(incoming.title == " Tile Study \n")
print(result.title)
print("spacing: \(result.spacing)")
print("grid: \(result.showsGrid)")
print("caption cleared: \(result.caption == nil)")
let blankTitle = PanelStyle(
title: " \n\t",
spacing: 30,
showsGrid: false,
caption: nil
)
do {
_ = try mergeStyles(base: base, incoming: blankTitle)
preconditionFailure("Expected emptyTitle")
} catch StyleMergeError.emptyTitle {
print("Rejected blank title")
}
let negativeBase = PanelStyle(
title: "Shape Study",
spacing: -1,
showsGrid: true,
caption: "Blue squares"
)
do {
_ = try mergeStyles(base: negativeBase, incoming: incoming)
preconditionFailure("Expected negativeSpacing")
} catch StyleMergeError.negativeSpacing {
print("Rejected negative spacing")
}
在包含该文件的文件夹中运行:
swift MergeStyles.swift
预期输出如下。研究环境中没有 Swift,因此程序经过了检查,但未在该环境中运行。
Tile Study
spacing: 12
grid: false
caption cleared: true
Rejected blank title
Rejected negative spacing
初始化器是核心的决策辅助工具:每个参数都对应所选输入字段或一个有名称的中间值。两个输入都不会被修改。Swift 的值语义支持这种做法;这里的字段本身也是值类型。如果以后引入类类型的字段,复制外层结构体并不会独立复制它所引用的类实例。结构体和类。
验证会在创建实例前进行。传入标题为空时会抛出错误,而不是悄悄恢复基标题;基间距为负数时也会抛出错误,而不是悄悄将其截为零。如果两个条件都不满足,首先会报告 emptyTitle,因为对应的 guard 语句先执行。调用方应先处理错误,再使用结果。
决定可选值表示清除还是保留
主示例将 incoming.caption 视为期望的完整标题。因此,它的 nil 表示“结果没有标题”。虽然其表示形式只是一个可选值,但这样就能在约定中明确表达清除操作。
另一种需求可能规定,传入的 nil 表示“保留基标题”。对于这种策略,替换后的初始化器参数应为 caption: incoming.caption ?? base.caption。如果左操作数包含值,Swift 的空合并运算符会选择该值;否则使用右操作数。这是带标签的替换片段,不是第二个完整程序。空合并运算符。
这种回退策略无法同时使用同一个传入的 nil 来请求清除。在采用这个表达式前,先确定其含义。空字符串是另一个不同的值:无论使用哪种策略,传入标题为 \"\" 时都会保留为空标题,除非你明确添加了规范化处理。
如果未来的编辑操作需要支持全部三种动作——保留、替换和清除,就应单独表示操作。下面这个独立完整程序使用自定义枚举,演示了仅针对标题的操作:
enum CaptionChange {
case keep
case set(String?)
}
func applying(
_ change: CaptionChange,
to caption: String?
) -> String? {
switch change {
case .keep:
return caption
case .set(let replacement):
return replacement
}
}
let original: String? = "Blue squares"
precondition(applying(.keep, to: original) == original)
precondition(applying(.set("Green circles"), to: original)
== "Green circles")
precondition(applying(.set(nil), to: original) == nil)
print("Caption actions checked")
这个枚举是对约定的示例性扩展。如果两个输入都是完整样式,请保留原来的双值函数;当输入实际描述编辑操作时,再引入操作类型。
用刻意不同的值检查优先规则
有效的合并检查应让相互竞争的字段取不同值。如果两个输入的间距都是 12,并且都启用网格,那么错误的来源选择可能不会被发现。示例使用 12 和 30 作为间距,并使用 true 和 false 作为对应值,因此预期结果能够体现这些选择。
修改函数前,请根据预期约定检查以下边界情况:
- 传入的
false必须保留为false;不要用布尔值 OR 表达替换操作。 - 按照主策略,传入标题
nil必须清除标题。 - 应去除标题首尾的空白,同时保留内部空格。
- 仅含空白字符的传入标题必须抛出错误,不能回退到基标题。
- 基间距为
0时必须成功;负数基间距必须抛出错误。 - 交换参数后,规则必须应用于重新指定的角色。
最后一点很重要,因为这种合并依赖参数顺序。反转示例中的输入后,会选出 \"Shape Study\"、间距 30、网格 true 和标题 \"Blue squares\"。这是依照既定规则得到的结果。在调用处为两个参数命名,让它们的角色一目了然。
添加字段时,请扩展表格、初始化器,并添加一项使用不同竞争值的检查。如果未来的嵌套样式包含多个独立设置,请决定是替换整个嵌套值,还是为它的每个字段分别规定优先级。当前实现只定义了所示的四个字段。
将并发与合并策略分开处理
此计算是同步的:它读取参数,验证所选值,然后返回结果或抛出错误。确定优先级不需要任务或 actor。Swift 将异步函数描述为可能暂停的函数;添加异步执行并不能决定哪个字段优先。并发。
示例将 PanelStyle 声明为 Sendable,表示其值可以跨并发边界传递。Apple 要求可发送结构体包含的成员也都是可发送的。该一致性声明涉及值的传递;字段选择规则仍完全由 mergeStyles 决定。Sendable。
对于这个本地绘图面板任务,请将约定放在函数旁边,并使用明显不同的示例输入。最终结果应能逐字段解释:规范化后的传入标题、保留的基间距、传入的网格设置,以及能够明确清除先前值的传入标题。
来源
- 结构体和类 — Swift.org。
- merge(_:uniquingKeysWith:) — Apple 开发者文档。
- trimmingCharacters(in:) — Apple 开发者文档。
- 安装 Swift — macOS — Swift.org。
- 入门指南 — Swift.org。
- ??(::) — Apple 开发者文档。
- 并发 — Swift.org。
- Sendable — Apple 开发者文档。