关键的混淆在于 Swift 有两个拼写相同的概念,因此通常是模棱两可的。其中一个是struct T: A {},意思是“T符合协议 A”,另一个是var a: A,意思是“变量a的类型是存在的 的 A。”
遵守协议不会改变类型。 T 仍然是 T。它只是碰巧符合一些规则。
“存在”是编译器生成的包装协议的盒子。这是必要的,因为符合协议的类型可能是不同的大小和不同的内存布局。存在是一个盒子,它为任何符合协议的东西在内存中提供一致的布局。存在和协议是相关的,但不是一回事。
因为existential 是一个可以容纳任何类型的运行时框,所以涉及到一些间接性,这可能会影响性能并阻止某些优化。
另一个常见的混淆是理解类型参数的含义。在函数定义中:
func f<T>(param: T) { ... }
这定义了一系列函数f<T>(),它们是在编译时根据您作为类型参数传递的内容创建的。例如,当您以这种方式调用此函数时:
f(param: 1)
在编译时会创建一个名为f<Int>() 的新函数。这是与f<String>() 或f<[Double]>() 完全不同的功能。每个都有自己的功能,原则上是f()中所有代码的完整副本。 (实际上,优化器非常聪明,可能会消除一些复制。还有一些与跨越模块边界的事物相关的其他细微之处。但这是考虑正在发生的事情的一种相当不错的方式。)
由于为每个传递的类型创建了通用函数的专用版本,因此理论上它们可以更加优化,因为函数的每个版本将只处理一种类型。权衡是他们可以添加代码膨胀。不要假设“泛型比协议快”。泛型可能比协议更快是有原因的,但您必须实际查看代码生成和配置文件才能在任何特定情况下了解。
所以,看看你的例子:
func direct(a: A) {
// Doesn't work
let _ = A.init(someInt: 1)
}
协议 (A) 只是类型必须遵守的一组规则。你不能构造“一些符合这些规则的未知事物”。将分配多少字节的内存?它将为规则提供哪些实现?
func indirect<T: A>(a: T) {
// Works
let _ = T.init(someInt: 1)
}
为了调用这个函数,你必须传递一个类型参数,T,并且那个类型必须符合A。当你用一个特定的类型调用它时,编译器会创建一个indirect的新副本,它具体是旨在与您通过的 T 一起使用。由于我们知道 T 有一个适当的 init,我们知道编译器将能够在需要时编写此代码。但indirect 只是编写函数的模式。它本身不是一个函数。直到你给它一个 T 才能使用。
let a: A = B(someInt: 0)
// Works
direct(a: a)
a 是 B 的存在包装器。direct() 需要一个存在包装器,因此您可以传递它。
// Doesn't work
indirect(a: a)
a 是 B 的存在包装器。存在包装器不符合协议。它们需要符合协议的东西才能创建它们(这就是为什么它们被称为“existentials”;您创建了一个事实证明这样的值确实存在)。但它们本身并不符合协议。如果他们这样做了,那么您可以执行您在direct() 中尝试执行的操作,并说“创建一个存在包装器的新实例,而无需确切知道其中的内容。”没有办法做到这一点。现有包装器没有自己的方法实现。
在某些情况下,存在的可以符合它自己的协议。只要没有init或static的要求,原则上其实是没有问题的。但 Swift 目前无法处理这个问题。因为它不能用于 init/static,所以 Swift 目前在所有情况下都禁止它。