美文网首页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