Generics 泛型

90 阅读4分钟

原文:Generics | Swift by Sundell

Swift 使我们能够创建不依赖于任何特定具体类型的泛型类型、协议和函数——而是可以与满足给定要求的任何类型一起使用。

作为一门非常强调类型安全的语言,泛型是 Swift 许多方面的核心——包括它的标准库,它大量使用泛型。看看它的一些基本数据结构,比如 ArrayDictionary,它们都是泛型。

泛型使相同的类型、协议或函数能够专门用于大量用例。例如,由于 Array 是一个泛型,它允许我们为任何类型(例如字符串)创建它的专用实例:

var array = ["One", "Two", "Three"]
array.append("Four")

// 这不会编译,因为上面的数组是专门用于字符串的,不能插入其他类型值
array.append(5)

// 当我们从数组中获取一个元素时,我们仍然可以把它当作一个普通字符串处理,
// 因为我们有完整的类型安全。
let characterCount = array[0].count

要创建我们自己的泛型,我们只需定义我们的泛型类型是什么,并选择性地为它们附加约束。例如,这里我们正在创建一个可以包含任何值和一个 Date 日期的 Container 类型:

struct Container<Value> {
    var value: Value
    var date: Date
}

就像我们能够创建专门的 ArrayDictionary 一样,我们可以将上面的 Container 用于任何类型的值,例如字符串或整数:

let stringContainer = Container(value: "Message", date: Date())
let intContainer = Container(value: 7, date: Date())

请注意,我们不需要指定我们在上面专门为 Container 指定了哪些具体类型——Swift 的类型推断会自动确定 stringContainerContainer<String> 实例,而 intContainerContainer<Int> 的实例。

**当我们编写可应用于不同类型的代码时,泛型特别有用。**例如,我们可以使用上面的 Container 来实现一个通用的 Cache,它可以为任何类型的 key 存储任何类型的 value。在这种情况下,我们还添加了一个约束,要求 Key 符合 Hashable 协议,以便我们可以将它与 Dictionary 一起使用——如下所示:

class Cache<Key: Hashable, Value> {
    private var values = [Key: Container<Value>]()

    func insert(_ value: Value, forKey key: Key) {
        let expirationDate = Date().addingTimeInterval(1000)

        values[key] = Container(
            value: value,
            date: expirationDate
        )
    }

    func value(forKey key: Key) -> Value? {
        guard let container = values[key] else {
            return nil
        }

        // If the container's date is in the past, then the
        // value has expired, and we remove it from the cache.
        guard container.date > Date() else {
            values[key] = nil
            return nil
        }

        return container.value
    }
}

有了上述内容,我们现在可以为我们的任何类型创建类型安全的缓存——例如用户或搜索结果:

class UserManager {
    private var cachedUsers = Cache<User.ID, User>()
    ...
}

class SearchController {
    private var cachedResults = Cache<Query, [SearchResult]>()
    ...
}

上面我们确实需要指定我们专门针对 Cache 的类型,因为编译器无法从调用栈点推断出该信息。

单个函数也可以是泛型,无论它们是在哪里定义的。例如,这里我们扩展 String(它不是泛型类型)来添加一个泛型函数,让我们可以轻松地将所有元素的 ID 附加到 Identifiable 值数组中:

extension String {
    mutating func appendIDs<T: Identifiable>(of values: [T]) {
        for value in values {
            append(" \(value.id)")
        }
    }
}

甚至协议也可以是泛型的!事实上,上面的 Identifiable 协议就是一个例子,因为它使用关联类型来使其能够专用于任何类型的 ID 类型——就像这样:

protocol Identifiable {
    associatedtype ID: Equatable & CustomStringConvertible

    var id: ID { get }
}

上述方法能够为符合 Identifiable 的每个单独类型决定它想要使用哪种 ID ——同时仍然能够充分利用我们为 Identifiable 类型编写的所有泛型代码(例如我们的上面的字符串扩展名)。

例如,Article 类型可以使用 UUID 值作为 ID,而 Tag 类型可以简单地使用整数:

struct Article: Identifiable {
    let id: UUID
    var title: String
    var body: String
}

struct Tag: Identifiable {
    let id: Int
    var name: String
}

当我们需要某些数据模型使用特定类型的 ID 时,上述技术非常有用,例如与另一个系统兼容,例如服务器后端

同样,编译器将为我们完成上述大部分繁重的工作,因为它会自动推断 Article.ID 表示 UUID,而 Tag.ID 表示 Int——基于每个单独类型的 id 属性。现在 ArticleTag 都可以传递给任何接受符合 Identifiable 的值的函数,同时仍然保持不同的类型,甚至使用它们自己的不同类型的标识符。

这就是泛型的全部力量,它使我们能够编写更易重用的代码,同时仍然支持本地的专业化。算法、数据结构和工具类方法通常是泛型的绝佳候选者——因为它们通常只需要它们使用的类型来满足一组特定的要求,而不是与特定的具体类型绑定。

谢谢阅读!🚀

参考