我们经常遇到这样的情况:我们需要找到一种基于某种身份概念存储对象的方法。无论是在缓存中、存储在磁盘上的对象表示,还是仅仅使用字典——我们经常需要找到唯一标识我们处理的对象的方法。

本周,让我们来看看Swift中使用的一些常见的身份概念,以及我们如何以不同的方式将它们用于值和对象。

Equatable

一个经常被用来比较对象和值的核心协议是Equatable。这是一个您可能已经熟悉的协议,因为任何时候您想要使==操作符与类型一起使用,您需要遵循它。这里有一个例子:

struct Book {
    let title: String
    let author: String
}
extension Book: Equatable {
    static func ==(lhs: Book, rhs: Book) -> Bool {
        guard lhs.title == rhs.title else {
            return false
        }
        guard lhs.author == rhs.author else {
            return false
        }
        return true
    }
}

注意,上面的==的手动实现实际上不再需要了,因为编译器会自动将一致性合成为Equatable,对于任何只有Equatable属性的类型(就像我们上面的Book类型)。

Instance equatable

虽然Equatable非常适合处理值(如结构或枚举),但对于对象/类,它可能不是您要寻找的。有时你想检查两个对象是否是同一个实例。要做到这一点,我们使用的兄弟姐妹;=,它允许你基于实例来比较两个对象,而不是它们的值。

让我们看一个例子,我们想重新加载一个InventoryManager,每次它被分配了一个新的数据源:

// Protocols with the 'AnyObject' constraint can only be conformed to
// by classes, enabling us to assume that an object will be used.
protocol InventoryDataSource: AnyObject {
    var numberOfItems: Int { get }
    func item(at index: Int) -> Item
}
class InventoryManager {
    var dataSource: InventoryDataSource? {
        // 'oldValue' is a magic variable that always contains the
        // previous value of the property when a new one is set
        didSet { dataSourceDidChange(from: oldValue) }
    }
    private func dataSourceDidChange(from previousDataSource: InventoryDataSource?) {
        // We don't want to reload if the same data source is re-assigned
        guard previousDataSource !== dataSource else {
            return
        }
        reload()
    }
}

正如你在上面看到的,使用===可以非常整洁,因为它使你可以执行检查,而不需要符合某个协议的类型也实现Equatable(这增加了样板文件,也对协议进行了自我约束——更多关于后者的内容将在以后的文章中介绍)。

Hashable

与Equatable一样,另一个在处理值类型时非常常见的协议是可哈希的。当您在某种基于散列的集合(如集合)中使用类型或在字典中作为键时,这是一个需求。

让我们将之前的Book类型扩展到也符合Hashable:

extension Book: Hashable {
    func hash(into hasher: inout Hasher) {
        hasher.combine(title)
        hasher.combine(author)
    }
}

同样,如果类型的所有属性也符合Hashable,编译器也能够自动合成上述一致性。

例如,我们现在可以建立一个独特的书籍收藏:

class BookStore {
    var inventory = [Book]()
    var uniqueBooks = Set<Book>()

    func add(_ book: Book) {
        // Inventory will contain all books, including duplicates
        inventory.append(book)

        // By using a set (which we can, now that 'Book' conforms to 'Hashable')
        // we can guarantee that 'uniqueBooks' only contains unique values
        uniqueBooks.insert(book)
    }
}

Protocols and Hashable

虽然Hashable在处理具体类型时很容易使用(尽管实现起来有点样板式)(比如上面书店的例子),但在处理协议时可能会有点棘手。

在使用散列值时需要记住的一件事是,只有当您知道所有对象或值都是完全相同的类型时,您才能依赖它们。由于这不是协议的情况,我们不得不依赖其他一些方法。

假设我们正在构建一个渲染API,在这个API中,不同的对象可以请求在屏幕下次绘制帧时进行渲染。要使用这个API,对象遵循一个可渲染的协议,并且在需要的时候使用一个渲染器来进入自己的渲染队列(类似于UIView的setNeedsLayout方法),像这样:

class Circle {
    var radius: CGFloat {
        didSet { renderer?.enqueue(self) }
    }
    var strokeColor: UIColor {
        didSet { renderer?.enqueue(self) }
    }
    weak var renderer: Renderer?
}
extension Circle: Renderable {
    func render(in context: CGContext) {
       context.drawCircle(withRadius: radius, strokeColor: strokeColor)
    }
}

作为一个优化,我们想要确保我们在每一帧中只渲染一个对象一次,即使它可能会在一帧绘制之前要求多次渲染。为了实现这一点,我们需要跟踪已经进入队列的符合Renderable的唯一实例,但由于这些实例可能是完全不同的类型,我们不能只添加Equatable或Hashable作为要求。

ObjectIdentifier

解决上述问题的一个方法是使用Swift的ObjectIdentifier类型来识别实例,并确保我们的渲染队列不会包含重复的对象。在Swift中,每个类的实例都有一个唯一的标识符,你可以通过为它创建一个ObjectIdentifier来获得它,像这样:

func enqueue(_ renderable: Renderable) {
    let identifier = ObjectIdentifier(renderable)
}

ObjectIdentifier已经符合Equatable和Hashable,这使我们能够围绕Renderable编写一个精简的包装类型,使用每个实例的标识符为其提供身份:

class RenderableWrapper {
    fileprivate let renderable: Renderable
    init(renderable: Renderable) {
        self.renderable = renderable
    }
}
extension RenderableWrapper: Renderable {
    func render(in context: CGContext) {
        // Forward the call to the underlying instance
        renderable.render(in: context)
    }
}
extension RenderableWrapper: Equatable {
    static func ==(lhs: RenderableWrapper, rhs: RenderableWrapper) -> Bool {
        // Compare the underlying instances
        return lhs.renderable === rhs.renderable
    }
}
extension RenderableWrapper: Hashable {
    func hash(into hasher: inout Hasher) {
        // Use the instance's unique identifier for hashing
        hasher.combine(ObjectIdentifier(renderable))
    }
}

我们现在可以简单地使用Set来跟踪需要在渲染器中渲染的唯一实例:

class Renderer {
    private var objectsNeedingRendering = Set<RenderableWrapper>()

    func enqueue(_ renderable: Renderable) {
        // Since we use a 'Set', duplicates will automatically be
        // discarded, we don't have to write any code for it
        let wrapper = RenderableWrapper(renderable: renderable)
        objectsNeedingRendering.insert(wrapper)
    }

    func screenDidDraw() {
        // Start by emptying the queue
        let objects = objectsNeedingRendering
        objectsNeedingRendering = []

        // Render each object
        for object in objects {
            let context = makeContext()
            object.render(in: context)
        }
    }
}

Conclusion

当使用值时,可平等性和可哈希性可能是在身份方面的做法——因为你更多的是比较一个值的规范化表示,而不是唯一的实例。然而,当处理对象时,使用这篇文章中的一些技术可以使你的api更容易使用,从而降低复杂性,增加稳定性。

没有要求实现者遵守Equatable,或公开某种形式的唯一标识符(如UUID),您可以使用===操作符和ObjectIdentifier类型这样的技术来快速且唯一地标识对象,而不需要太多额外的代码。

原文链接