(WWDC) 新式的 Swift API 设计



浏览 Swift API设计规范, 我们可以知道:

注重使用时的清晰度 是定义接口时最重要的目标
清晰度远比简洁更重要



另外,纯 Swift 编写的框架是没有前缀的

  • C 和 Objective-C 符号是全局可用的
  • Swift 模块系统可以消除歧义

谨记,每个源文件都将导入到相同的命名空间中。

 


 

内容概览

  • 值与引用
  • 协议与泛型
  • 通过 KeyPath 查找属性
  • 属性包装器


 


 

值与引用

 

1306450-d45bac52cd7b13ed.png

类 —— 引用类型

1306450-fbca830b608e1afc.png

结构体、枚举 —— 值类型



引用类型、值类型,该如何选择?

  • 优先使用 struct,只在确实需要使用 引用语义 时才使用 class

  • 在这些情况下, class 将会是一个好选择

    • 你需要引用计数和析构(deinit)
    • 值被集中持有和分享
    • 验证同一性





 

如果在值类型内使用了引用类型呢?





 

举个例子:

1306450-214834e664c38765.png


 

在复制 Material 时,你将面对一个问题:

1306450-b79c44ee9b6f273c.png

Material 在进行复制的时候,只复制了对 Texture 的引用。
不同的 Material 实例就会操作同一个 Texture。



你如何解决这个问题?


 

如果 Texture 是不需要修改的,我们可以将 Texture 定义为不可变的引用类型。

如果你确实需要为 Material 修改 Texture,那么你需要对 Texture 进行复制。

1306450-b7552b2e36ce4141.png

如果 Material 只需要使用 Texture 的某些属性,你可以只暴露这些属性:

1306450-1d4e455ff4f3b211.png

isKnownUniquelyReferenced 是 Swift 标准库中的方法,用于检测对象是否只有一个强引用。
利用这个方法,你可以很容易地实现自己的写时复制需求。


 


 

协议与泛型

 

1306450-67ed6c4a92bbd0d7.png

协议可以应用于值类型



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

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


 

接下来,请看一个反例:

1306450-e3a745f71ae053ad.png

SIMD 协议

1306450-76b53026240d092e.png

实现 SIMD 协议的类型

1306450-af0b4a66e77f6b03.png

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

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

1306450-af7fe8c93defc207.png

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

1306450-1776f1208fcb12f5.png

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

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


 

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

1306450-2d6b42b7a42e3fe9.png

使用泛型定义 GeometricVector

1306450-ac872104ada3edb0.png

为 GeometricVector 重载运算符

1306450-f4d32fa5909d9f11.png

定义其他方法


 


 

通过 KeyPath 查找属性

 

如果现在要添加 X 运算符,然后扩展 SIMD3,使用 X 运算符实现叉积运算:

1306450-b88e704fdc904f1e.png

如果要为 GeometricVector 添加运算符以支持 SIMD,我们可能会这样实现:

1306450-8529fd084126c9b3.png

请注意观察,这个方法中充斥着密集的 value 属性。

 

有什么办法可以优化这个问题吗?

1306450-12642e9f62975e8f.png

Key Path Member Lookup 源于 Swift Evolution: SE-0252。

现在,SIMD 中的属性在 GeometricVector 中都是可用的:

1306450-ce60f33686ad5256.png

所以,可以在叉积运算方法中直接访问这些属性:

1306450-3c1e4b2a86c6ff24.png

我们还可以用 @dynamicMemberLookup 来优化前面定义的 Material:

1306450-764a46c127bb88e6.png

现在,我们可以像访问 Material 的属性一样,直接访问 Texture 的属性!


 


 

属性包装器

 

属性包装器 源于 Swift Evolution: SE-0258。

 

下面这段代码是常见的计算属性封装存储属性的使用场景,它遵循着固定的样板。

1306450-73c66696022fda2a.png

你可能觉得可以使用 lazy 来简化上述代码:

1306450-03bd14d5c9a47bf5.png

如果封装的逻辑是下面这样的呢?

1306450-976042d7da907b4d.png

每一次你需要使用计算属性来包装存储属性时,你都需要完成类似结构的样板代码。
Really boring, right??

 

如果是这样的语法结构,而且你还可以根据自己的需要来自定义这种属性,会不会让你觉得眼前一亮呢?

1306450-f26255fb1cc1e608.png




 

属性包装器:捕获后备存储属性和访问策略以便重复使用

 

属性包装器提供了类似于内建懒加载机制的好处

  • 消除了样板代码
  • 在定义时说明语义

 

你是不是已经迫不及待地想知道如何实现属性包装器?

@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 的属性包装器。?


 

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

1306450-1f57a2c18a8006cd.png

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


 

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

1306450-540abe5375109458.png

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

 

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

1306450-e9e71507de7c175b.png

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

 

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

1306450-65d1fc3767353f5a.png

 

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

1306450-0c570c9df69e113d.png

手动初始化

1306450-faade12710f23464.png

自动初始化



 

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

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

1306450-30470adebcb78767.png



 

SwiftUI 中的属性包装器

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

1306450-d852ceee22180359.png

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

 

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

1306450-c8e7d53396df6661.png

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





 

1306450-f91f1f44d4c7008d.png

1306450-8af836c99b87d481.png

1306450-5e7392418e26e9c0.png






 

总结

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



 



参考内容:
Modern Swift API Design

 




转载请注明出处,谢谢~

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值