在Swift中创建自定义集合

118 阅读5分钟

在Swift中创建自定义集合

hudson 译 原文

在Swift中创建对象或值集合时,我们通常使用标准库提供的数据结构,如ArrayDictionarySet。虽然这三个涵盖了大多数用例,但有时创建自定义包装器集合可以使您使代码更可预测,更不容易出错。

本周,让我们看看作为应用程序开发人员如何在Swift中定义此类自定义集合,以及结合强大的enums枚举,为我们自己创建一些相当不错的API。

删除可选项

就像我们在“在Swift中处理非可选选项”中查看的那样,当您正在寻找的值实际也需要时,减少使用可选项的需求,可以真正帮助我们避免错误,并使我们的代码更容易使用。

一般来说,集合的问题在于,您通常无法保证它们是否包含一定的值,因此您最终往往会有很多选项和逻辑,需要以这样或那样的方式打开它们。

假设我们正在为杂货店构建一个应用程序,我们希望有一个用户界面,让用户按类别显示所有产品。要为这样的UI创建模型,我们可能会用一个Dictionary,该词典使用Category作为其键类型 Category,[Product]作为其值类型,如下:

let products: [Category : [Product]] = [
    .dairy: [
        Product(name: ”Milk“, category: .dairy),
        Product(name: ”Butter“, category: .dairy)
    ],
    .vegetables: [
        Product(name: ”Cucumber“, category: .vegetables),
        Product(name: ”Lettuce“, category: .vegetables)
    ]
]

虽然上述代码可以工作,但需要我们编写这样的代码,以便-例如-只显示所有乳制品:

if let dairyProducts = products[.dairy] {
    guard !dairyProducts.isEmpty else {
        renderEmptyView()
        return
    }

    render(dairyProducts)
} else {
    renderEmptyView()
}

上面的代码没问题,但可能会更好一些。然而,插入新产品变得更加麻烦:

class ShoppingCart {
    private(set) var products = [Category : [Product]]()

    func add(_ product: Product) {
        if var productsInCategory = products[product.category] {
            productsInCategory.append(product)
            products[product.category] = productsInCategory
        } else {
            products[product.category] = [product]
        }
    }
}

好消息是,通过创建自定义集合,我们可以使上述两个示例都更好、更干净。 更好的消息是,由于Swift以协议为导向的设计,创建这样的集合实际上很容易!

成为一个集合

Swift标准库中的所有集合都符合Collection协议,而Collection协议又继承自Sequence协议。 通过使自定义集合符合这两个协议,就可以完全利用所有标准集合操作,例如遍历和过滤。

让我们从定义我们自定义ProductCollection的基础开始,这将使我们能够以更好的方式处理产品和类别。

struct ProductCollection {
    typealias DictionaryType = [Category : [Product]]

    // Underlying, private storage, that is the same type of dictionary
    // that we previously was using at the call site
    private var products = DictionaryType()

    // Enable our collection to be initialized with a dictionary
    init(products: DictionaryType) {
        self.products = products
    }
}

接下来,我们将通过实现协议要求使其符合Collection。我们将做的大多数事情只是简单地将调用转发到底层 products 字典,并让它做“沉重的提升”:

extension ProductCollection: Collection {
    // Required nested types, that tell Swift what our collection contains
    typealias Index = DictionaryType.Index
    typealias Element = DictionaryType.Element

    // The upper and lower bounds of the collection, used in iterations
    var startIndex: Index { return products.startIndex }
    var endIndex: Index { return products.endIndex }

    // Required subscript, based on a dictionary index
    subscript(index: Index) -> Iterator.Element {
        get { return products[index] }
    }

    // Method that returns the next index when iterating
    func index(after i: Index) -> Index {
        return products.index(after: i)
    }
}

上述代码使用Swift 4,这使定义自定义集合变得更加简单,这要归功于范型约束的改进(我们将在未来的帖子中更仔细地研究这些改进以及如何使用类型约束)。

我们现在有一个自定义集合,可以作为内置集合之一使用。例如,我们可以通过它进行遍历:

for (category, productsInCategory) in products {
    ...
}

或者在上面使用像map这样的操作:

let categories = productCollection.map { $0.key }

自定义集合API

我们有了一个基础集合,现在开始向其添加一些API,这可以让产品处理代码更好。我们从重载自定义下标subscript开始。该重载允许获取或设置产品数组,而无需处理可选项:

extension ProductCollection {
    subscript(category: Category) -> [Product] {
        get { return products[category] ?? [] }
        set { products[category] = newValue }
    }
}

让我们添加一个方便的API,以轻松地将Product新产品插入我们的集合中:

extension ProductCollection {
    mutating func insert(_ product: Product) {
        var productsInCategory = self[product.category]
        productsInCategory.append(product)
        self[product.category] = productsInCategory
    }
}

现在可以回到原始产品处理代码,并更新它,使其更好。如更好的读代码:

let dairyProducts = products[.dairy]

if dairyProducts.isEmpty {
    renderEmptyView()
} else {
    render(dairyProducts)
}

以及更好的写代码:

class ShoppingCart {
    private(set) var products = ProductCollection()

    func add(product: Product) {
        products.insert(product)
    }
}

支持更具表达的字面量

好的,是时候收获红利了!由于我们的自定义集合基本上只是围绕Dictionary的包装器,我们可以轻松地添加对使用字典字面量初始化字典的支持。这样做将使我们能够编写这样的代码:

let products: ProductCollection = [
    .dairy: [
        Product(name: ”Milk“, category: .dairy),
        Product(name: ”Butter“, category: .dairy)
    ],
    .vegetables: [
        Product(name: ”Cucumber“, category: .vegetables),
        Product(name: ”Lettuce“, category: .vegetables)
    ]
]

很酷!这不仅有助于减少冗长枯燥的代码,还将使测试中设置模拟的产品集合变得简单得多。

要实现上述情况,我们所要做的就是符合ExpressibleByDictionaryLiteral协议,这要求我们实现一个采用字面量的初始化器,像这样:

extension ProductCollection: ExpressibleByDictionaryLiteral {
    typealias Key = Category
    typealias Value = [Product]

    init(dictionaryLiteral elements: (Category, [Product])...) {
        for (category, productsInCategory) in elements {
            products[category] = productsInCategory
        }
    }
}

小结

自定义集合是一种真正强大工具,可以让我们以更可预测和易于使用的方式处理一组值。 虽然在处理多个值时,它可能并不总是首选解决方案,但在正确的情况下,它确实可以帮助您编写更清晰的代码。

了解数据结构如集合等幕后是如何工作的,对于调试时非常有帮助,也可以让你了解如何优化处理集合相关的代码。 还有什么比创建自己的集合更好的方法来学习集合更多的知识呢? 😄

你有什么看法呢? 您是否已经使用自定义集合,还是会尝试创建一个?,或者你可能有任何问题、评论或反馈- 请在推特@johnsundell上联系我。

感谢您的阅读!