Swift中的类型占位符介绍

272 阅读4分钟

Swift的类型推理能力从一开始就是语言的核心部分,它极大地减少了我们在声明有默认值的变量和属性时手动指定类型的需要。例如,表达式var number = 7 ,不需要包括任何类型注释,因为编译器能够推断出值7 是一个Int ,我们的number 变量应该相应地被类型化。

作为Xcode 13.3的一部分发布的Swift 5.6,通过引入 "类型占位符 "的概念,继续扩展这些类型推断能力,这在处理集合和其他通用类型时非常有用。

例如,假设我们想创建一个Combine的 CurrentValueSubject的实例,其默认值为整数。关于如何做到这一点,最初的想法可能是简单地将我们的默认值传递给该主题的初始化器,然后将结果存储在本地let (就像在创建一个普通的Int 值一样)。然而,这样做会给我们带来以下编译器错误:

// Error: "Generic parameter 'Failure' could not be inferred"
let counterSubject = CurrentValueSubject(0)

这是因为CurrentValueSubject 是一个通用类型,不仅需要用Output 类型来专门化,还需要用Failure 类型来专门化--这是主体能够抛出的错误类型。

由于我们不希望我们的主体在这种情况下抛出任何错误,我们将给它一个失败类型Never (这是使用Combine和Swift并发时的一个常见惯例)。但为了做到这一点,在Swift 5.6之前,我们需要明确指定我们的Int 输出类型--像这样:

let counterSubject = CurrentValueSubject<Int, Never>(0)

但从Swift 5.6开始,这种情况就不存在了--因为我们现在可以为我们的主题的输出类型使用一个类型占位符,这让我们再次利用编译器为我们自动推断该类型,就像在声明一个普通的Int

let counterSubject = CurrentValueSubject<_, Never>(0)

这很好,但可以说这不是世界上最大的改进。毕竟,我们用_ 替换Int 只节省了两个字符,而且手动指定一个简单的类型如Int 并不是一开始就有问题的。

但现在让我们看看这个功能如何扩展到更复杂的类型,这是它真正开始发光的地方。例如,假设我们的项目包含以下函数,让我们加载一个用户注解的PDF文件:

func loadAnnotatedPDF(named: String) -> Resource<PDF<UserAnnotations>> {
    ...
}

上述函数使用一个相当复杂的泛型作为它的返回类型,这可能是因为我们在多个域中重复使用我们的Resource 类型,也因为我们选择使用*幻象类型*来指定我们当前处理的是哪一种PDF。

现在让我们看一下,如果我们在创建主题时调用上述函数,而不是仅仅使用一个简单的整数,那么我们之前基于CurrentValueSubject 的代码会是什么样子:

// Before Swift 5.6:
let pdfSubject = CurrentValueSubject<Resource<PDF<UserAnnotations>>, Never>(
    loadAnnotatedPDF(named: name)
)

// Swift 5.6:
let pdfSubject = CurrentValueSubject<_, Never>(
    loadAnnotatedPDF(named: name)
)

这是一个相当大的进步基于 Swift 5.6 的版本不仅为我们节省了一些输入,而且由于pdfSubject 的类型现在完全来自loadAnnotatedPDF 函数,这可能会使该函数(及其相关代码)的迭代变得更加容易--因为如果我们改变该函数的返回类型,需要更新的手动类型注释将会减少。

不过值得指出的是,在上述情况下,还有一种方法可以利用Swift的类型推理能力--那就是使用类型别名,而不是类型占位符。例如,我们可以在这里定义一个UnfailingValueSubject 类型别名,我们可以用它来轻松地创建不可能抛出任何错误的主体:

typealias UnfailingValueSubject<T> = CurrentValueSubject<T, Never>

有了上面的方法,我们现在就可以在没有任何通用类型注释的情况下创建我们的pdfSubject --因为编译器能够推断出T 指的是什么类型,而且失败类型Never 已经被硬编码到我们新的类型别名中:

let pdfSubject = UnfailingValueSubject(loadAnnotatedPDF(named: name))

但这并不意味着类型别名普遍比类型占位符好,因为如果我们要为每种特定情况定义新的类型别名,那么也会使我们的代码库变得更加复杂。有时,在内联中指定所有的东西(比如使用类型占位符时)绝对是个好办法,因为这可以让我们定义完全独立的表达式。

在我们总结之前,让我们也来看看类型占位符如何与集合字头一起使用--例如在创建字典时。在这里,我们选择手动指定我们的 dictionary 的Key 类型(为了能够使用点语法来指代 enum 的各种情况),同时为该 dictionary 的值使用一个类型占位符:

enum UserRole {
    case local
    case remote
}

let latestMessages: [UserRole: _] = [
    .local: "",
    .remote: ""
]

这就是类型占位符--Swift 5.6中引入的一项新功能,在处理稍微复杂的通用类型时,它可能真的很有用。但值得指出的是,这些占位符只能在调用处使用,而不能在指定函数或计算属性的返回类型时使用。

谢谢你的阅读!