前言
本文内容基于 WWDC19 - 415 Modern Swift API Design 整理。
Swift 是一门简洁同时富有表现力的语言,这其中隐藏着众多的设计细节。
本文通过提出一个 Struct 的语义问题,在寻找答案的过程中逐步介绍这些概念:
- DynamicMemberLookup 应用
- PropertyWrapper 的实现原理
- SwiftUI DSL 中 PropertyWrapper 的应用
来一起看看更现代的 API 背后的设计过程。
WWDC19 部分 sessions 和示例代码中的 PropertyDelegate 即是 PropertyWrapper,后续会统一命名为 PropertyWrapper
Clarity at the point of use
最大化使用者的清晰度是 API 设计的第一要义
- Swift 的模块系统可以消歧义
- Swift-only Framework 命名不再有前缀
- C & Objective-C 符号都是全局的
提醒:
每一个源文件都会将所有 import 模块汇总到同一个命名空间下。你依旧应该谨慎的对待命名,以确保同一命名在复杂上下文依旧有清晰的语义。
选择 Struct 还是 Class?
相很多类似问题一样,你需要重新思考二者的语义。
默认情况下,你应该优先选择 Struct。除非你必须要用到 Class 的特性。
比如这些需要使用 Class 的场景:
- 需要引用计数或者关心析构过程
- 数据需要集中管理或共享
- 比较操作很重要,有类似 ID 的独立概念
很多文章有过讨论,这里不作过多介绍,下面我们看看实际问题。
Struct 中嵌套 Class 的拷贝问题
无论是基于历史问题还是要对不同类型数据组合使用,常常碰到 Struct 和 Class 组合嵌套的情况。
- Class 中存在 Struct,这种情况再正常不过,使用时也不会带来什么问题,不必讨论
- Struct 中存在 Class,这种情况破坏了 Struct 的语义,运行时拷贝也可能带来不符合预期的情况,下面重点讨论这个问题。
定义如代码所示,Struct Material 有一个成员属性 texture 是 Class 类型:
struct Material {
public var roughness: Float
public var color: Color
public var texture: Texture
}
class Texture {
var isSparkly: Bool
}
当 Material 实例发生拷贝时,会发生什么?

很显然,两个 Material 实例持有同一个的 texture,所有 texture 引用所做的任何修改都会对两个 Struct 产生影响,这破坏了 Struct 本身的语义。
今天我们重点看看如何解决这个问题。
一个思路:把 texture 设为不可变类型?

如图所示,并没有什么作用。
texture 对象的属性依旧可以被修改,一个标记 immutable 的实例属性还能被修改,这会带来更多困扰。
另一个思路:修改时拷贝
struct Material {
private var _texture: Texture
public var texture {
get { _texture }
set { _texture = Texture(copying: newValue) }
}
}
隐藏存储属性,开放计算属性。在计算属性被赋值时进行拷贝。
针对修改 Material 实例的 texture 属性这一场景,的确会生成单独的拷贝。然而除此之外,有太多的问题。
- texture 实例的内部属性,依旧可能被意外修改
- Material 发生写时拷贝时,被拷贝的存储属性 _texture 依旧是同一个
再一个思路:模仿 Copy On Write
既然我们连 Texture 的内部属性都要控制,开放 texture 访问带来太多问题,索性完全禁用 texture 的外部访问,把 texture 的属性(如 isSparkly)提升到 Material 属性层级,在访问 isSparkly 时,确保 _texture 引用唯一。
struct Material {
private var _texture: Texture
public var isSparkly: Bool {
get {
if !isKnownUniquelyReferenced(&_texture) { // 确保 _texture 引用计数为 1
_texture = Texture(copying: _texture)
}
return _texture.isSparkly
}
set {
_texture.isSparkly = newValue
}
}
}
这样的确完整实现了 Struct Material 语义。哪怕 Material 写时拷贝有多个 _texture 引用,在访问 isSparkly 属性时也会发生拷贝,确保每个 Material 实例的 _texture 属性唯一。
唯一(而且是很重要)的问题是如果 Class Texture 属性很多,会引入大量相似代码。『可行』不代表『可用』。
没关系,我们再试试引入 DynamicMemberLookup。
初试 DynamicMemberLookup
DynamicMemberLookup 具体概念可以参考卓同学的这篇文章:细说 Swift 4.2 新特性:Dynamic Member Lookup
DynamicMemberLookup 是 Swift4.2 引入的新特性,使用在什么场景一度让人困惑。这里恰好能解决我们的问题。先上代码:
@dynamicMemberLookup
struct Material {
public var roughness: Float
public var color: Color
private var _texture: Texture
public subscript<T>(dynamicMember keyPath: ReferenceWritableKeyPath<Texture, T>) -> T {
get { _texture[keyPath: keyPath] }
set {
if !isKnownUniquelyReferenced(&_texture) { _texture = Texture(copying: _texture) }
_texture[keyPath: keyPath] = newValue
}
}
}
实现思路与之前的代码完全一致,只是引入 dynamicMemberLookup 动态提供对 Texture 的属性访问,这样无论 Class Texture 有多少属性,几行代码轻松支持。
需要留意的是 Xcode 11 完全支持 dynamicMemberLookup,代码提示也毫无压力

至此,似乎『完美解决』了Struct 中嵌套 Class 的拷贝问题。
此处卖个关子,后面还有更简洁的实现。先来看看 PropertyWrapper。
PropertyWrapper
Swift Evolution: SE-0258
实际项目中有些属性的初始化性能开销较大,我们常常会用到懒加载:
public lazy var image: UIImage = loadDefaultImage()
如果不用 lazy
关键字,我们也可以这样实现:
public struct MyType {
private var imageStorage: UIImage? = nil
public var image: UIImage {
mutating get {
if imageStorage == nil {
imageStorage = loadDefaultImage()
}
return imageStorage!
}
set { imageStorage = newValue }
}
}
基于同样的思路,也会有另一些场景,比如需要的是延迟外部赋值,期望未赋值调用时抛出错误:
public struct MyType {
var textStorage: String? = nil
public var text: String {
get {
guard let value = textStorage else {
fatalError("text has not yet been set!")
}
return value
}
set { textStorage = newValue }
}
}
看起来不错。支持延迟外部赋值又有检查机制。唯一(而且是很重要)的问题是实现太臃肿。每个有同样逻辑的属性都需要大段重复代码。
还记得我们说过的:保持使用者的清晰度是 API 设计的第一要义
我们更倾向于使用者看到这样的代码:
```swift
前言
本文内容基于 WWDC19 - 415 Modern Swift API Design 整理。
Swift 是一门简洁同时富有表现力的语言,这其中隐藏着众多的设计细节。
本文通过提出一个 Struct 的语义问题,在寻找答案的过程中逐步介绍这些概念:
- DynamicMemberLookup 应用
- PropertyWrapper 的实现原理
- SwiftUI DSL 中 PropertyWrapper 的应用
来一起看看更现代的 API 背后的设计过程。
WWDC19 部分 sessions 和示例代码中的 PropertyDelegate 即是 PropertyWrapper,后续会统一命名为 PropertyWrapper
Clarity at the point of use
最大化使用者的清晰度是 API 设计的第一要义
- Swift 的模块系统可以消歧义
- Swift-only Framework 命名不再有前缀
- C & Objective-C 符号都是全局的
提醒:
每一个源文件都会将所有 import 模块汇总到同一个命名空间下。你依旧应该谨慎的对待命名,以确保同一命名在复杂上下文依旧有清晰的语义。
选择 Struct 还是 Class?
相很多类似问题一样,你需要重新思考二者的语义。
默认情况下,你应该优先选择 Struct。除非你必须要用到 Class 的特性。
比如这些需要使用 Class 的场景:
- 需要引用计数或者关心析构过程
- 数据需要集中管理或共享
- 比较操作很重要,有类似 ID 的独立概念
很多文章有过讨论,这里不作过多介绍,下面我们看看实际问题。
Struct 中嵌套 Class 的拷贝问题
无论是基于历史问题还是要对不同类型数据组合使用,常常碰到 Struct 和 Class 组合嵌套的情况。
- Class 中存在 Struct,这种情况再正常不过,使用时也不会带来什么问题,不必讨论
- Struct 中存在 Class,这种情况破坏了 Struct 的语义,运行时拷贝也可能带来不符合预期的情况,下面重点讨论这个问题。
定义如代码所示,Struct Material 有一个成员属性 texture 是 Class 类型:
struct Material {
public var roughness: Float
public var color: Color
public var texture: Texture
}
class Texture {
var isSparkly: Bool
}
当 Material 实例发生拷贝时,会发生什么?

很显然,两个 Material 实例持有同一个的 texture,所有 texture 引用所做的任何修改都会对两个 Struct 产生影响,这破坏了 Struct 本身的语义。
今天我们重点看看如何解决这个问题。
一个思路:把 texture 设为不可变类型?

如图所示,并没有什么作用。
texture 对象的属性依旧可以被修改,一个标记 immutable 的实例属性还能被修改,这会带来更多困扰。
另一个思路:修改时拷贝
struct Material {
private var _texture: Texture
public var texture {
get { _texture }
set { _texture = Texture(copying: newValue) }
}
}
隐藏存储属性,开放计算属性。在计算属性被赋值时进行拷贝。
针对修改 Material 实例的 texture 属性这一场景,的确会生成单独的拷贝。然而除此之外,有太多的问题。
- texture 实例的内部属性,依旧可能被意外修改
- Material 发生写时拷贝时,被拷贝的存储属性 _texture 依旧是同一个
再一个思路:模仿 Copy On Write
既然我们连 Texture 的内部属性都要控制,开放 texture 访问带来太多问题,索性完全禁用 texture 的外部访问,把 texture 的属性(如 isSparkly)提升到 Material 属性层级,在访问 isSparkly 时,确保 _texture 引用唯一。
struct Material {
private var _texture: Texture
public var isSparkly: Bool {
get {
if !isKnownUniquelyReferenced(&_texture) { // 确保 _texture 引用计数为 1
_texture = Texture(copying: _texture)
}
return _texture.isSparkly
}
set {
_texture.isSparkly = newValue
}
}
}
这样的确完整实现了 Struct Material 语义。哪怕 Material 写时拷贝有多个 _texture 引用,在访问 isSparkly 属性时也会发生拷贝,确保每个 Material 实例的 _texture 属性唯一。
唯一(而且是很重要)的问题是如果 Class Texture 属性很多,会引入大量相似代码。『可行』不代表『可用』。
没关系,我们再试试引入 DynamicMemberLookup。
初试 DynamicMemberLookup
DynamicMemberLookup 具体概念可以参考卓同学的这篇文章:细说 Swift 4.2 新特性:Dynamic Member Lookup
DynamicMemberLookup 是 Swift4.2 引入的新特性,使用在什么场景一度让人困惑。这里恰好能解决我们的问题。先上代码:
@dynamicMemberLookup
struct Material {
public var roughness: Float
public var color: Color
private var _texture: Texture
public subscript<T>(dynamicMember keyPath: ReferenceWritableKeyPath<Texture, T>) -> T {
get { _texture[keyPath: keyPath] }
set {
if !isKnownUniquelyReferenced(&_texture) { _texture = Texture(copying: _texture) }
_texture[keyPath: keyPath] = newValue
}
}
}
实现思路与之前的代码完全一致,只是引入 dynamicMemberLookup 动态提供对 Texture 的属性访问,这样无论 Class Texture 有多少属性,几行代码轻松支持。
需要留意的是 Xcode 11 完全支持 dynamicMemberLookup,代码提示也毫无压力

至此,似乎『完美解决』了Struct 中嵌套 Class 的拷贝问题。
此处卖个关子,后面还有更简洁的实现。先来看看 PropertyWrapper。
PropertyWrapper
Swift Evolution: SE-0258
实际项目中有些属性的初始化性能开销较大,我们常常会用到懒加载:
public lazy var image: UIImage = loadDefaultImage()
如果不用 lazy
关键字,我们也可以这样实现:
public struct MyType {
private var imageStorage: UIImage? = nil
public var image: UIImage {
mutating get {
if imageStorage == nil {
imageStorage = loadDefaultImage()
}
return imageStorage!
}
set { imageStorage = newValue }
}
}
基于同样的思路,也会有另一些场景,比如需要的是延迟外部赋值,期望未赋值调用时抛出错误:
public struct MyType {
var textStorage: String? = nil
public var text: String {
get {
guard let value = textStorage else {
fatalError("text has not yet been set!")
}
return value
}
set { textStorage = newValue }
}
}
看起来不错。支持延迟外部赋值又有检查机制。唯一(而且是很重要)的问题是实现太臃肿。每个有同样逻辑的属性都需要大段重复代码。
还记得我们说过的:保持使用者的清晰度是 API 设计的第一要义
我们更倾向于使用者看到这样的代码:
```swift
前言
本文内容基于 WWDC19 - 415 Modern Swift API Design 整理。
Swift 是一门简洁同时富有表现力的语言,这其中隐藏着众多的设计细节。
本文通过提出一个 Struct 的语义问题,在寻找答案的过程中逐步介绍这些概念:
- DynamicMemberLookup 应用
- PropertyWrapper 的实现原理
- SwiftUI DSL 中 PropertyWrapper 的应用
来一起看看更现代的 API 背后的设计过程。
WWDC19 部分 sessions 和示例代码中的 PropertyDelegate 即是 PropertyWrapper,后续会统一命名为 PropertyWrapper
Clarity at the point of use
最大化使用者的清晰度是 API 设计的第一要义
- Swift 的模块系统可以消歧义
- Swift-only Framework 命名不再有前缀
- C & Objective-C 符号都是全局的
提醒:
每一个源文件都会将所有 import 模块汇总到同一个命名空间下。你依旧应该谨慎的对待命名,以确保同一命名在复杂上下文依旧有清晰的语义。
选择 Struct 还是 Class?
相很多类似问题一样,你需要重新思考二者的语义。
默认情况下,你应该优先选择 Struct。除非你必须要用到 Class 的特性。
比如这些需要使用 Class 的场景:
- 需要引用计数或者关心析构过程
- 数据需要集中管理或共享
- 比较操作很重要,有类似 ID 的独立概念
很多文章有过讨论,这里不作过多介绍,下面我们看看实际问题。
Struct 中嵌套 Class 的拷贝问题
无论是基于历史问题还是要对不同类型数据组合使用,常常碰到 Struct 和 Class 组合嵌套的情况。
- Class 中存在 Struct,这种情况再正常不过,使用时也不会带来什么问题,不必讨论
- Struct 中存在 Class,这种情况破坏了 Struct 的语义,运行时拷贝也可能带来不符合预期的情况,下面重点讨论这个问题。
定义如代码所示,Struct Material 有一个成员属性 texture 是 Class 类型:
struct Material {
public var roughness: Float
public var color: Color
public var texture: Texture
}
class Texture {
var isSparkly: Bool
}
当 Material 实例发生拷贝时,会发生什么?

很显然,两个 Material 实例持有同一个的 texture,所有 texture 引用所做的任何修改都会对两个 Struct 产生影响,这破坏了 Struct 本身的语义。
今天我们重点看看如何解决这个问题。
一个思路:把 texture 设为不可变类型?

如图所示,并没有什么作用。
texture 对象的属性依旧可以被修改,一个标记 immutable 的实例属性还能被修改,这会带来更多困扰。
另一个思路:修改时拷贝
struct Material {
private var _texture: Texture
public var texture {
get { _texture }
set { _texture = Texture(copying: newValue) }
}
}
隐藏存储属性,开放计算属性。在计算属性被赋值时进行拷贝。
针对修改 Material 实例的 texture 属性这一场景,的确会生成单独的拷贝。然而除此之外,有太多的问题。
- texture 实例的内部属性,依旧可能被意外修改
- Material 发生写时拷贝时,被拷贝的存储属性 _texture 依旧是同一个
再一个思路:模仿 Copy On Write
既然我们连 Texture 的内部属性都要控制,开放 texture 访问带来太多问题,索性完全禁用 texture 的外部访问,把 texture 的属性(如 isSparkly)提升到 Material 属性层级,在访问 isSparkly 时,确保 _texture 引用唯一。
struct Material {
private var _texture: Texture
public var isSparkly: Bool {
get {
if !isKnownUniquelyReferenced(&_texture) { // 确保 _texture 引用计数为 1
_texture = Texture(copying: _texture)
}
return _texture.isSparkly
}
set {
_texture.isSparkly = newValue
}
}
}
这样的确完整实现了 Struct Material 语义。哪怕 Material 写时拷贝有多个 _texture 引用,在访问 isSparkly 属性时也会发生拷贝,确保每个 Material 实例的 _texture 属性唯一。
唯一(而且是很重要)的问题是如果 Class Texture 属性很多,会引入大量相似代码。『可行』不代表『可用』。
没关系,我们再试试引入 DynamicMemberLookup。
初试 DynamicMemberLookup
DynamicMemberLookup 具体概念可以参考卓同学的这篇文章:细说 Swift 4.2 新特性:Dynamic Member Lookup
DynamicMemberLookup 是 Swift4.2 引入的新特性,使用在什么场景一度让人困惑。这里恰好能解决我们的问题。先上代码:
@dynamicMemberLookup
struct Material {
public var roughness: Float
public var color: Color
private var _texture: Texture
public subscript<T>(dynamicMember keyPath: ReferenceWritableKeyPath<Texture, T>) -> T {
get { _texture[keyPath: keyPath] }
set {
if !isKnownUniquelyReferenced(&_texture) { _texture = Texture(copying: _texture) }
_texture[keyPath: keyPath] = newValue
}
}
}
实现思路与之前的代码完全一致,只是引入 dynamicMemberLookup 动态提供对 Texture 的属性访问,这样无论 Class Texture 有多少属性,几行代码轻松支持。
需要留意的是 Xcode 11 完全支持 dynamicMemberLookup,代码提示也毫无压力

至此,似乎『完美解决』了Struct 中嵌套 Class 的拷贝问题。
此处卖个关子,后面还有更简洁的实现。先来看看 PropertyWrapper。
PropertyWrapper
Swift Evolution: SE-0258
实际项目中有些属性的初始化性能开销较大,我们常常会用到懒加载:
public lazy var image: UIImage = loadDefaultImage()
如果不用 lazy
关键字,我们也可以这样实现:
public struct MyType {
private var imageStorage: UIImage? = nil
public var image: UIImage {
mutating get {
if imageStorage == nil {
imageStorage = loadDefaultImage()
}
return imageStorage!
}
set { imageStorage = newValue }
}
}
基于同样的思路,也会有另一些场景,比如需要的是延迟外部赋值,期望未赋值调用时抛出错误:
public struct MyType {
var textStorage: String? = nil
public var text: String {
get {
guard let value = textStorage else {
fatalError("text has not yet been set!")
}
return value
}
set { textStorage = newValue }
}
}
看起来不错。支持延迟外部赋值又有检查机制。唯一(而且是很重要)的问题是实现太臃肿。每个有同样逻辑的属性都需要大段重复代码。
还记得我们说过的:保持使用者的清晰度是 API 设计的第一要义
我们更倾向于使用者看到这样的代码:
```swift