"existential type"在Swift中是什么意思?

What does "existential type" mean in Swift?

我正在阅读 Swift Evolution proposal 244 (Opaque Result Types) 但不明白以下内容的含义:

... existential type ...

One could compose these transformations by using the existential type Shape instead of generic arguments, but doing so would imply more dynamism and runtime overhead than may be desired.

进化提案本身给出了一个存在类型的例子:

protocol Shape {
  func draw(to: Surface)
}

使用 protocol Shape 作为存在类型的示例如下所示

func collides(with: any Shape) -> Bool

相对于使用通用参数 Other:

func collides<Other: Shape>(with: Other) -> Bool

重要的是要注意这里 Shape 协议本身不是一个存在类型,只在 "protocols-as-types" context as above "creates" an existential type from it. See this post from the member of Swift Core Team:

中使用它

Also, protocols currently do double-duty as the spelling for existential types, but this relationship has been a common source of confusion.

此外,引用 Swift Generics Evolution 文章(我建议阅读整篇文章,其中更详细地解释了这一点):

The best way to distinguish a protocol type from an existential type is to look at the context. Ask yourself: when I see a reference to a protocol name like Shape, is it appearing at a type level, or at a value level? Revisiting some earlier examples, we see:

func addShape<T: Shape>() -> T
// Here, Shape appears at the type level, and so is referencing the protocol type

var shape: Shape = Rectangle()
// Here, Shape appears at the value level, and so creates an existential type

深入探讨

为什么叫“存在主义”?我从未看到对此的明确确认,但我认为该功能的灵感来自具有更高级类型系统的语言,例如考虑 Haskell's existential types:

class Buffer -- declaration of type class `Buffer` follows here

data Worker x y = forall b. Buffer b => Worker {
  buffer :: b, 
  input :: x, 
  output :: y
}

大致相当于这个 Swift 片段(如果我们假设 Swift 的协议或多或少代表 Haskell 的类型 classes):

protocol Buffer {}

struct Worker<X, Y> {
  let buffer: Buffer
  let input: X
  let output: Y
}

请注意,Haskell 示例在此处使用了 forall quantifier。您可以将其理解为“对于符合 Buffer 类型 class(Swift 中的“协议”)的所有类型,Worker 类型的值将具有与只要它们的 XY 类型参数相同”。因此,给定

extension String: Buffer {}
extension Data: Buffer {}

Worker(buffer: "", input: 5, output: "five")Worker(buffer: Data(), input: 5, output: "five") 将具有完全相同的类型。

这是一个强大的功能,它允许诸如异构集合之类的东西,并且可以在更多需要“擦除”值的原始类型并将其“隐藏”在现有类型下的地方使用.像所有强大的功能一样,它可能会被滥用,并且会降低代码的类型安全性,因此应谨慎使用。

如果您想更深入地了解,请查看 OP 链接的 Protocols with Associated Types (PATs), which currently can't be used as existentials for various reasons. There are also a few proposals being pitched more or less regularly, but nothing concrete as of Swift 5.3. In fact, the original Opaque Result Types 提案可以解决使用 PAT 引起的一些问题,并显着缓解 Swift 中缺乏广义存在性。

特别感谢帮助我得出这个答案的朋友:

Max 的总结回答是:

var rec: Shape = Rectangle()

只能访问 Shape 个属性。而对于:

func addShape<T: Shape>() -> T
可以访问

Shape 属性以及 T

的任何其他 属性

第一个例子是存在主义的,第二个不是。

真实代码示例:

protocol Shape {
  var width: Double { get }
  var height: Double { get }
}

struct Rectangle: Shape {
  var width: Double
  var height: Double
  var area: Double
}

let rec: Shape = Rectangle(
  width: 1,
  height: 2,
  area: 2
)

rec.area // ❌

let rec1 = Rectangle(
  width: 1,
  height: 2,
  area: 2
)


func addShape<T: Shape>(_ shape: T) -> T {
    print(type(of: shape)) // Rectangle
    return shape
}
let rec2 = addShape(rec1)

print(rec2.area) // ✅

我认为对于大多数 Swift 用户来说,我们都理解抽象 class 和具体 class,真的不知道为什么会有这个额外的和奇怪的命名行话。我不明白区分它们有什么好处。

我觉得有必要添加一些关于为什么该短语在 Swift 中很重要的内容。特别是,我认为 Swift 几乎总是在谈论“存在的容器”。他们谈论“存在类型”,但实际上只是指“存储在存在容器中的东西”。那么什么是“存在容器”呢?

在我看来,关键是,如果您有一个变量作为参数传递或在本地使用等,并且您将变量的类型定义为 Shape,那么 Swift 必须在幕后做一些事情才能使其正常工作,这就是他们(间接地)指的是什么。

如果您考虑在您正在编写的公开可用的 library/framework 模块中定义一个函数,并以参数 public func myFunction(shape1: Shape, shape2: Shape, shape1Rotation: CGFloat?) -> Shape 为例...想象它(可选)旋转 shape1 ,以某种方式将它“添加”到 shape2(我将细节留给你想象)然后 returns 结果。来自其他 OO 语言,我们本能地认为我们理解它是如何工作的......该功能必须仅使用 Shape 协议中可用的成员来实现。

但是对于编译器来说,参数在内存中是如何表示的呢?再一次,我们本能地认为……没关系。当有人在将来某个时候编写一个使用您的函数的新程序时,他们决定将自己的形状传入并将它们定义为 class Dinosaur: Shapeclass CupCake: Shape。作为定义这些新 classes 的一部分,他们将不得不编写 protocol Shape 中所有方法的实现,这可能类似于 func getPointsIterator() -> Iterator<CGPoint>。这适用于 classes。调用代码定义了那些 classes,从中实例化对象,将它们传递给您的函数。你的函数必须有类似 vtable 的东西(我认为 Swift 称它为 witness table),用于 Shape 协议说“如果你给我一个 Shape 的实例对象,我可以准确地告诉你在哪里可以找到 getPointsIterator 函数的地址”。实例指针将指向堆栈上的一块内存,其开始是指向 class 元数据的指针(vtables,witness tables 等)所以编译器可以推理如何找到任何给定的方法实现。

但是值类型呢?结构和枚举在内存中几乎可以有任何格式,从一个字节的 Bool 到一个 500 字节的复杂嵌套结构。这些通常在堆栈上传递或在函数调用时注册以提高效率。 (当 Swift 确切知道类型时,所有代码都可以在知道数据格式的情况下编译并在堆栈或寄存器等中传递)

现在你可以看到问题了。 Swift 如何编译函数 myFunction 以便它可以与任何代码中定义的 any 可能的未来值 type/struct 一起工作?据我了解,这就是“存在容器”的用武之地。

最简单的方法是任何采用这些“存在类型”(仅通过符合协议定义的类型)之一的参数的函数必须坚持调用代码“框”值类型......它将值存储在堆上一个特殊的引用计数“框”中,并在函数采用 Shape 类型的参数时将指向它的指针(使用所有常用的 ARC retain/release/autorelease/ownership 规则)传递给您的函数.

然后,当将来某个代码作者编写了一个新的、奇怪的、奇妙的类型时,编译 Shape 的方法必须包括一种接受该类型的“盒装”版本的方法。您的 myFunction 将始终通过处理框来处理这些“存在类型”,并且一切正常。如果 C# 和 Java 对非 class 类型(Int 等)有同样的问题,我猜他们会做这样的事情(装箱)?

问题是对于很多值类型,这可能是非常低效的。毕竟,我们主要针对 64 位架构进行编译,因此几个寄存器可以处理 8 个字节,对于许多简单结构来说已经足够了。所以 Swift 想出了一个折衷方案(同样,我对此可能有点不准确,我给出了我对该机制的想法......请随时纠正)。他们创建了大小始终为 4 个指针的“存在容器”。 “正常”64 位架构上的 16 个字节(现在大多数 CPU 运行 Swift)。

如果你定义了一个符合协议的结构,并且它包含12个字节或更少,那么它直接存储在存在容器中。最后一个 4 字节指针是指向 information/witness tables/etc 类型的指针。这样 myFunction 就可以在 Shape 协议中找到任何函数的地址(就像上面 classes 的情况一样)。如果您的 struct/enum 大于 12 个字节,则 4 指针值指向值类型的盒装版本。显然,这被认为是最佳折衷方案,而且似乎是合理的……在大多数情况下,它将在 4 个寄存器中传递,如果“溢出”,则将在 4 个堆栈槽中传递。

我认为 Swift 团队最终向更广泛的社区提及“存在容器”的原因是因为它会对使用 Swift 的各种方式产生影响。一个明显的含义是性能。如果结构的大小大于 12 个字节,以这种方式使用函数时性能会突然下降。

我认为另一个更基本的含义是协议只有在没有协议或自我要求的情况下才能用作参数……它们不是通用的。否则,您将进入不同的通用函数定义。这就是为什么我们有时需要将 func myFunction(shape: Shape, reflection: Bool) -> Shape 更改为 func myFunction<S:Shape>(shape: S, reflection: Bool) -> S 之类的内容。它们在后台以非常不同的方式实现。