利用Swift中的值语义

作者: iOS_小久 | 来源:发表于2019-07-24 14:00 被阅读27次

    Swift整体设计的一个非常有趣的方面是它围绕价值类型概念的集中性。大多数标准库的核心类型(例如StringArrayDictionary)都被建模为值,甚至基本语言概念(例如可选项)也被表示为引擎盖下的值。

    值类型的独特之处在于它们如何在程序的各个部分之间传递,以及如何将突变应用于给定实例的语义。本周,我们来看看我们可以使用这些语义的几种不同方式 - 以及如何这样做可以显着提高基于价值的代码的灵活性。

    小编这里有大量的书籍和面试资料哦(点击下载

    提供基础: 值类型和引用类型的基础知识,以及它们之间的区别,在解释这篇文章

    解锁局部突变

    通常,程序包含的可变状态越少,发生错误的可能性就越小。当事情保持不变时,它们本身就更具可预测性,因为没有发生意外变化的可能性。然而,制造不可变的东西通常也意味着牺牲灵活性,这有时会成为问题。

    假设我们正在处理一个处理视频的应用程序,为了使我们的核心Video模型尽可能可预测,我们选择通过使用let以下方法定义它们使其所有属性不可变:

    struct Video {
        let id: UUID
        let url: URL
        let title: String
        let description: String
        let tags: Set<Tag>
    }
    

    以上看起来似乎是一个好主意,特别是如果获取Video实例的唯一当前方法是从通过网络下载的数据解码它们。然而,由于价值语义的力量,做上述事情通常是没有必要的。

    默认情况下,结构不仅是不可变的,除非它们存储在本身可变的变量中,对结构实例的任何突变也将始终只应用于该值的本地副本。这意味着即使我们打开我们Video的突变类型,我们也不会冒险引入由未处理的状态变化引起的错误。

    因此,让我们通过使用var来定义我们模型的大部分属性,而不是let。但是,我们不会对所有属性进行更改- 因为我们希望它们中的一些始终保持不变 - 例如idurl在这种情况下:

    struct Video {
        let id: UUID
        let url: URL
        var title: String
        var description: String
        var tags: Set<Tag>
    }
    

    通过执行上述更改,我们可以清楚地了解Video未来可能会发生变化的模型的哪些部分(即使这些突变发生在其他地方,例如在我们的服务器上),但我们也为该模型解锁了新的用例 - 例如用它来跟踪当地的状态。

    假设我们正在为我们的视频应用添加新功能,该功能可让我们的用户对之前下载的视频执行本地修改。既然我们现在已经打开了我们Video的本地突变模型,我们可以简单地让这样的视频编辑器直接在我们的模型实例上运行 - 像这样:

    class VideoEditingViewController: UIViewController {
        private var video: Video
    
        ...
    
        func titleTextFieldDidChange(_ textField: UITextField) {
            textField.text.map { video.title = $0 }
        }
    
        func tagSelectionView(_ view: TagSelectionView,
                              didAddTagNamed tagName: String) {
            video.tags.insert(Tag(name: tagName))
        }
    }
    

    上述场景中价值语义的美妙之处在于,任何VideoEditingViewController导致其私有Video价值的局部突变都不会传播到其他地方。这意味着我们可以完全隔离地处理该值,并且可以将所有用户的编辑与原始数据源明确分开。

    另一方面,每当我们想要保持任何Video实例完全不可变时,我们所要做的就是使用let- 来引用它- 并且不允许任何突变:

    struct SearchResult {
        let video: Video
        let matchedQuery: Query
    }
    

    对于我们的核心数据模型,如上述Video类型,让封闭的上下文决定是否允许突变 - 而不是将这些决策烘焙到每个模型中 - 通常会使我们的模型代码更加灵活,而不会带来任何实质性风险,一切都归功于价值语义。

    确保数据一致性

    但是,有时我们确实需要对模型如何允许变异进行更多控制,特别是如果该模型的不同部分以某种方式相互连接或相互依赖。

    例如,我们现在说我们正在开发一个包含ShoppingCart模型的购物应用程序。除了存储Product用户已添加到购物车中的一系列值之外,我们还存储所有产品的总价格及其ID - 以避免每次访问时重新计算总价格,并启用常量 -时间查询是否已添加给定产品:

    struct ShoppingCart {
        var totalPrice: Int
        var productIDs: Set<UUID>
        var products: [Product]
    }
    

    上面的设置为我们提供了极大的灵活性和出色的性能,因为我们将在一个ShoppingCart实例上执行的许多常见操作可以在固定的时间内执行 - 但是,在这种情况下,使一切变得可变也为我们增加了重大风险数据变得不一致。

    我们不仅要始终记得在添加或删除产品时更新totalPriceproductIDs属性,每个属性也可以随时变更 - 产品没有任何变化。这不是很好,但幸运的是,有一个解决方案可以让我们继续使用价值语义,但是以一种稍微受控制的方式。

    不要让每个属性完全可变,让我们ShoppingCart通过使用private(set)访问修饰符限制大多数突变只允许在类型本身内。然后我们将products使用属性观察器直接响应数组中的更改来执行这些突变- 如下所示:

    struct ShoppingCart {
        private(set) var totalPrice = 0
        private(set) var productIDs: Set<UUID> = []
        var products: [Product] {
            didSet { productsDidChange() }
        }
    
        init(products: [Product]) {
            self.products = products
            // Note how we need to manually call our handling
            // method within our initializer, since property
            // observers aren't triggered until after a value
            // has been fully initialized.
            productsDidChange()
        }
    
        private mutating func productsDidChange() {
            totalPrice = products.reduce(0) { price, product in
                price + product.price
            }
    
            productIDs = []
            products.forEach { productIDs.insert($0.id) }
        }
    }
    

    通过上述更改,我们仍然在充分利用我们的products属性的值语义,同时现在也能够保证整个模型的完整数据一致性。

    简化重复突变

    虽然价值语义在限制突变的发生方式和位置方面给我们带来了很多好处,但有时这些限制会使某些代码片段比它们需要的更复杂一些。

    在这里,我们正在研究一种类型,它允许我们将图像渲染管道建模为一系列基于闭包的操作,这些操作将应用于RenderingContext结构。每个操作都作为输入传递给上一个上下文,使其变异,然后将更新的值作为输出返回。最后,一旦执行了所有操作,我们将获取最终上下文值并使用它来生成图像:

    struct RenderingPipeline {
        var operations: [(RenderingContext) -> RenderingContext]
    
        func render() -> Image {
            var context = RenderingContext()
    
            context = operations.reduce(context) { context, operation in
                operation(context)
            }
    
            return context.makeImage()
        }
    }
    

    有关更多信息reduce,请查看“在Swift中转换集合”

    上面的工作,但有一个问题。在每个闭包操作中,我们都需要手动将当前上下文复制到一个可变变量中,一旦我们执行了我们的突变,我们还需要显式返回更新后的值 - 如下所示:

    extension RenderingPipeline {
        mutating func fill(with color: Color) {
            operations.append { context in
                var context = context
                context.changeFillColor(to: color)
                context.fill(rect: Rect(origin: .zero, size: context.size))
                return context
            }
        }
    }
    

    上面的内容似乎不是什么大问题,如果我们可以执行的操作数量保持在最低限度,那么可能不会。但是,总是必须复制并返回当前的渲染上下文确实需要我们编写相当数量的样板,所以让我们看看我们是否可以做些什么。

    虽然我们想让它们RenderingContext保持结构,但实际上在调用每个操作时,通过引用而不是传递它- 使用inout关键字。这样我们可以简单地在整个管​​道中改变相同的上下文值:

    struct RenderingPipeline {
        var operations: [(inout RenderingContext) -> Void]
    
        func render() -> Image {
            var context = RenderingContext()
            operations.forEach { $0(&context) }
            return context.makeImage()
        }
    }
    

    使用inout关键字实际上并不传递指向我们的值的指针,而是通过自动创建可变副本并将结果值分配回传入的变量,从而为我们提供与使用引用类型时相同的顶级行为。 。

    上述更改不仅使我们的RenderingPipeline类型更简单,现在它还允许我们在操作中直接改变每个上下文 - 不需要复制或返回值:

    extension RenderingPipeline {
        mutating func fill(with color: Color) {
            operations.append { context in
                context.changeFillColor(to: color)
                context.fill(rect: Rect(origin: .zero, size: context.size))
            }
        }
    }
    

    尽管inout关键字绝对应该谨慎使用,因为它确实规避了值类型在避免共享状态方面给出的一些保护措施 - 当在内部使用类型时,就像我们上面所做的那样,它可以再次给我们一些非常实际的好处,没有太大的风险。

    结论

    在处理突变的方式上充分利用值类型,以及它们如何确保状态默认保持在本地,这可以是提高模型代码稳定性和灵活性的好方法。

    但是,默认情况下使一切变得可变并不一定是最好的方法 - 有时我们确实需要锁定一些以确保数据一致性,并发送一个明确的信号来确定哪些数据永远不应该被修改。

    最后,使用inout关键字可以让我们继续利用值类型的强大功能 - 同时在适当的情况下使用时也会引入一些参考类型的便利性。


    扫码进交流群 有技术的来闲聊 没技术的来学习

    691040931

    原文转载地址 https://www.swiftbysundell.com/posts/utilizing-value-semantics-in-swift

    相关文章

      网友评论

        本文标题:利用Swift中的值语义

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