美文网首页SwiftUI
(WWDC) 新式的 Swift API 设计

(WWDC) 新式的 Swift API 设计

作者: FicowShen | 来源:发表于2019-07-02 12:58 被阅读13次



浏览 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<Value> {

    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 表明这个类是一个属性包装类;
泛型 <Value> 的使用,可以让这个属性包装类支持任何类型;
可以在 init 时传入某些所需的参数,对属性包装类进行自定义;

Python 使用经验的朋友也许会想到 Python 的属性包装器。😄


接下来,请观察使用属性包装类时编译器生成的相关代码:

$text 的类型为 LateInitialized<String>,而 text 的类型为 String


再来定义一个属性包装器:

初始化和每一次设置新的值时,都会拷贝一份新的版本,然后存储到 storage 属性中。

让我们来观察使用上述属性包装器时编译器生成的代码:

DefensiveCopying 获得了 UIBezierPath() 作为初始化的值。

也可以在初始化时不进行拷贝:

现在,使用新方法的方式有两种:

手动初始化 自动初始化



设计API时,使用属性包装器

  • 属性包装器描述了数据访问背后的策略
  • 许多API与数据访问相关



SwiftUI 中的属性包装器

  • 视图数据的依赖使用属性包装器来表示

请注意 slide.number$slide.title 的区别!
前者是 Slide 中的属性,而后者是 @Binding 中的属性。

请观察 Binding 的定义,同时使用了 属性包装器 和 动态成员查找:

所以,$slide.title 是什么类型呢?










总结

  • 注意 值语义 和 引用语义 的使用
  • 使用协议和泛型进行代码重用,而不是协议与扩展
  • 使用属性包装器重用计算属性的定义





参考内容:
Modern Swift API Design




转载请注明出处,谢谢~

相关文章

网友评论

    本文标题:(WWDC) 新式的 Swift API 设计

    本文链接:https://www.haomeiwen.com/subject/zhnbcctx.html