【问题标题】:What does "existential type" mean in Swift?Swift 中的“存在类型”是什么意思?
【发布时间】:2022-03-25 13:22:38
【问题描述】:

我正在阅读Swift Evolution proposal 244 (Opaque Result Types),但不明白以下内容是什么意思:

...存在类型...

可以通过以下方式组合这些转换 使用 existential type Shape 而不是泛型参数,但是 这样做将意味着更多的活力和运行时开销 想要的。

【问题讨论】:

标签: swift type-theory


【解决方案1】:

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

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" 上下文中使用它会从中“创建”一个存在类型。见this post from the member of Swift Core Team:

此外,协议目前作为存在类型的拼写具有双重职责,但这种关系一直是混淆的常见来源。

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

区分协议类型和存在类型的最佳方法是查看上下文。问问自己:当我看到对像 Shape 这样的协议名称的引用时,它是出现在类型级别还是值级别?回顾一些早期的例子,我们看到:

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 sn-p(如果我们假设 Swift 的协议或多或少代表 Haskell 的类型类):

protocol Buffer {}

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

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

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

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

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

如果您想更深入地了解,请查看Protocols with Associated Types (PATs),由于各种原因,它目前不能用作存在主义。还有一些Generalized Existentials 提案或多或少定期提出,但截至 Swift 5.3 没有具体内容。事实上,由 OP 链接的原始 Opaque Result Types 提案可以解决使用 PAT 引起的一些问题,并显着缓解 Swift 中缺乏泛化存在的问题。

【讨论】:

  • 好吧,我们现在有了不透明的结果类型,但它们并没有减轻任何问题。
  • 当然,如果没有不透明的结果类型,SwiftUI API 会很麻烦,同样,这个功能在使用 Combine 时非常方便。
  • @MaxDesiatov 是真的,但它基本上仍然是围绕缺乏存在主义的一种黑客行为。协议不能符合EnvironmentObjetct 真的很令人沮丧。这意味着我必须通过具体的实现,基本上会扼杀“面向协议的编程”。
  • @MaxDesiatov 这是一个很好解释的答案,谢谢。在没有广义存在论的情况下(直到今天),类型擦除是唯一的解决方案吗?如果类型擦除“解决方法”有其自身的限制,例如无法将 init 和/或静态方法添加到类型擦除的 PAT,该怎么办?
  • 我认为你应该看看协议见证方法,这可能是缺乏普遍存在的合适解决方法,在本演示文稿中描述:youtube.com/watch?v=3BVkbWXcFS4
【解决方案2】:

我觉得值得补充一下为什么这个短语在 Swift 中很重要。特别是,我认为 Swift 几乎总是在谈论“存在容器”。他们谈论“存在类型”,但实际上只是参考“存储在存在容器中的东西”。那么什么是“存在容器”呢?

在我看来,关键是,如果您有一个变量作为参数传递或在本地使用等,并且您将变量的类型定义为Shape,那么 Swift 必须做一些事情引擎盖下的东西使它工作,这就是他们(间接地)指的。

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

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

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

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

最简单的方法是,任何接受这些“存在类型”(仅通过符合协议定义的类型)参数的函数都必须坚持调用代码“装箱”它存储的值类型......当函数采用Shape 类型的参数时,堆上的特殊引用计数“框”中的值并将指向此的指针(使用所有常用的 ARC 保留/释放/自动释放/所有权规则)传递给您的函数。

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

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

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

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

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

【讨论】:

    【解决方案3】:

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

    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 用户来说,我们都了解 Abstract 类和 Concrete 类,真的不知道为什么会有这个额外的和奇怪的术语。我不明白区分它们有什么好处。

    【讨论】:

      猜你喜欢
      • 2014-12-07
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2011-07-11
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2016-08-07
      相关资源
      最近更新 更多