(WWDC) 新式的 Swift API 设计



浏览 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 标准库中的方法,用于检测对象是否只有一个强引用。
利用这个方法,你可以很容易地实现自己的写时复制需求。




协议与泛型


协议可以应用于值类型



使用协议时,不建议 立刻从定义协议开始

  • 从实际的使用场景开始
  • 找到需要使用泛型的代码
  • 先尝试从现有的协议中组合解决方案
  • 优先考虑使用泛型而不是协议



接下来,请看一个反例:

SIMD 协议
实现 SIMD 协议的类型

定义 GeometricVector 协议,用于扩展 SIMD 协议,从而扩展 SIMD2, SIMD3, SIMD4 ... 等类型。

然后,在 GeometricVector 的 extension 中提供默认的实现:

然后,为 SIMD2, SIMD3, SIMD4, SIMD64 提供扩展:

如果 SIMD 有很多种具体的类型,你需要为很多类型(SIMD2, SIMD3, SIMD4, SIMD8, SIMD16, SIMD32, SIMD64)进行扩展。由于协议是动态的多态,所以这也会导致性能的消耗。

关于静态/动态的多态,请参考 理解 Swift 性能 中讲解的协议类型和泛型代码。



如果使用泛型,就可以将动态的多态变为静态的多态,性能会得到大幅度地提升!

使用泛型定义 GeometricVector
为 GeometricVector 重载运算符
定义其他方法




通过 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




转载请注明出处,谢谢~

你可能感兴趣的:((WWDC) 新式的 Swift API 设计)