浏览 Swift API设计规范, 我们可以知道:
注重使用时的清晰度 是定义接口时最重要的目标
清晰度远比简洁更重要
另外,纯 Swift 编写的框架是没有前缀的
- C 和 Objective-C 符号是全局可用的
- Swift 模块系统可以消除歧义
谨记,每个源文件都将导入到相同的命名空间中。
内容概览
- 值与引用
- 协议与泛型
- 通过 KeyPath 查找属性
- 属性包装器
值与引用
引用类型、值类型,该如何选择?
优先使用 struct,只在确实需要使用 引用语义 时才使用 class
-
在这些情况下, class 将会是一个好选择
- 你需要引用计数和析构(deinit)
- 值被集中持有和分享
- 验证同一性
如果在值类型内使用了引用类型呢?
举个例子:
在复制 Material 时,你将面对一个问题:
Material 在进行复制的时候,只复制了对 Texture 的引用。
不同的 Material 实例就会操作同一个 Texture。
你如何解决这个问题?
如果 Texture 是不需要修改的,我们可以将 Texture 定义为不可变的引用类型。
如果你确实需要为 Material 修改 Texture,那么你需要对 Texture 进行复制。
如果 Material 只需要使用 Texture 的某些属性,你可以只暴露这些属性:
isKnownUniquelyReferenced
是 Swift 标准库中的方法,用于检测对象是否只有一个强引用。
利用这个方法,你可以很容易地实现自己的写时复制需求。
协议与泛型
使用协议时,不建议 立刻从定义协议开始
- 从实际的使用场景开始
- 找到需要使用泛型的代码
- 先尝试从现有的协议中组合解决方案
- 优先考虑使用泛型而不是协议
接下来,请看一个反例:
定义 GeometricVector 协议,用于扩展 SIMD 协议,从而扩展 SIMD2, SIMD3, SIMD4 ... 等类型。
然后,在 GeometricVector 的 extension 中提供默认的实现:
然后,为 SIMD2, SIMD3, SIMD4, SIMD64 提供扩展:
如果 SIMD 有很多种具体的类型,你需要为很多类型(SIMD2, SIMD3, SIMD4, SIMD8, SIMD16, SIMD32, SIMD64)进行扩展。由于协议是动态的多态,所以这也会导致性能的消耗。
关于静态/动态的多态,请参考 理解 Swift 性能 中讲解的协议类型和泛型代码。
如果使用泛型,就可以将动态的多态变为静态的多态,性能会得到大幅度地提升!
通过 KeyPath 查找属性
如果现在要添加 X 运算符,然后扩展 SIMD3,使用 X 运算符实现叉积运算:
如果要为 GeometricVector 添加运算符以支持 SIMD,我们可能会这样实现:
请注意观察,这个方法中充斥着密集的 value 属性。
有什么办法可以优化这个问题吗?
Key Path Member Lookup 源于 Swift Evolution: SE-0252。
现在,SIMD 中的属性在 GeometricVector 中都是可用的:
所以,可以在叉积运算方法中直接访问这些属性:
我们还可以用 @dynamicMemberLookup 来优化前面定义的 Material:
现在,我们可以像访问 Material 的属性一样,直接访问 Texture 的属性!
属性包装器
属性包装器 源于 Swift Evolution: SE-0258。
下面这段代码是常见的计算属性封装存储属性的使用场景,它遵循着固定的样板。
你可能觉得可以使用 lazy
来简化上述代码:
如果封装的逻辑是下面这样的呢?
每一次你需要使用计算属性来包装存储属性时,你都需要完成类似结构的样板代码。
Really boring, right?
如果是这样的语法结构,而且你还可以根据自己的需要来自定义这种属性,会不会让你觉得眼前一亮呢?
属性包装器:捕获后备存储属性和访问策略以便重复使用
属性包装器提供了类似于内建懒加载机制的好处
- 消除了样板代码
- 在定义时说明语义
你是不是已经迫不及待地想知道如何实现属性包装器?
@propertyWrapper
public struct LateInitialized {
private var storage: Value?
public init() {
storage = nil
}
public var value: Value {
get {
guard let value = storage else {
fatalError("value has not yet been set!")
}
return value
}
set {
storage = newValue
}
}
}
简要的分析:
@propertyWrapper
表明这个类是一个属性包装类;
泛型
的使用,可以让这个属性包装类支持任何类型;
可以在 init
时传入某些所需的参数,对属性包装类进行自定义;
有 Python 使用经验的朋友也许会想到 Python 的属性包装器。
接下来,请观察使用属性包装类时编译器生成的相关代码:
$text
的类型为 LateInitialized
,而 text
的类型为 String
。
再来定义一个属性包装器:
初始化和每一次设置新的值时,都会拷贝一份新的版本,然后存储到 storage 属性中。
让我们来观察使用上述属性包装器时编译器生成的代码:
DefensiveCopying
获得了 UIBezierPath()
作为初始化的值。
也可以在初始化时不进行拷贝:
现在,使用新方法的方式有两种:
设计API时,使用属性包装器
- 属性包装器描述了数据访问背后的策略
- 许多API与数据访问相关
SwiftUI 中的属性包装器
- 视图数据的依赖使用属性包装器来表示
请注意 slide.number
和 $slide.title
的区别!
前者是 Slide 中的属性,而后者是 @Binding 中的属性。
请观察 Binding 的定义,同时使用了 属性包装器 和 动态成员查找:
所以,$slide.title
是什么类型呢?
总结
- 注意 值语义 和 引用语义 的使用
- 使用协议和泛型进行代码重用,而不是协议与扩展
- 使用属性包装器重用计算属性的定义
参考内容:
Modern Swift API Design
转载请注明出处,谢谢~