在Swift中,“存在类型”是什么意思?

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

... 存在类型 ...

可以使用存在类型Shape组合这些转换,而不是使用泛型参数,但这样做会导致更多的动态性和运行时开销,可能不太理想。


2
请查看使用Swift探索存在类型 - Dávid Pásztor
3个回答

48

可以用作存在类型的协议示例在演进提案中已经给出:

protocol Shape {
  func draw(to: Surface)
}

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

func collides(with: any Shape) -> Bool

与使用通用参数Other相反:

func collides(with: some Shape) -> Bool

由于在Swift 5.7中才引入了一些参数类型的关键字,因此在旧版本中,这个通用的“非存在性”版本代码只能以更冗长的方式编写。

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

这里需要注意的是,Shape协议本身并不是一种存在类型,只有在上下文中像"protocols-as-types"这样使用它才会从中“创建”出一种存在类型。请参阅Swift核心团队成员的这篇文章

此外,协议目前还兼作拼写存在类型的方式,但这种关系常常导致混淆。

这也是引入any关键字明确标记存在类型的动机。

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

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: any Shape = Rectangle()
// Here, Shape appears at the value level, and so creates an existential type

更深入的探究

为什么它被称为“存在类型”?我从未看到过明确的确认,但我认为该功能受到了具有更先进类型系统的语言的启发,例如考虑Haskell的存在类型

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 的协议更或多或少代表了 Haskell 的类型类,那么这大致相当于下面的 Swift 代码片段:

protocol Buffer {}

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

请注意,Haskell示例在此处使用了forall 量词。您可以将其解释为“对于符合Buffer类型类(Swift中的“协议”)的所有类型,只要它们的XY类型参数相同,那么Worker类型的值就会具有完全相同的类型”。因此,假设
extension String: Buffer {}
extension Data: Buffer {}

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

这是一个强大的功能,允许使用异构集合,并且可以在需要"抹掉"值原始类型并将其"隐藏"在存在类型下的更多地方使用。像所有强大的功能一样,它可能会被滥用,并且可能使代码不太类型安全和/或性能低下,因此应谨慎使用。

如果您想要更深入地了解,请查看 具有关联类型的协议(PATs),由于各种原因,目前无法用作存在类型。还有一些广义存在类型提案经常被提出,但在 Swift 5.3 中没有任何具体实现。事实上,OP链接的原始不透明结果类型提案可以解决使用PATs引起的某些问题,并显著缓解Swift中广义存在的缺乏。

4
好的,我们现在已经有了不透明的结果类型,但它们并没有减轻太多的压力。 - matt
2
毫无疑问,如果没有不透明的结果类型,SwiftUI API 将会更加繁琐。同样地,在使用 Combine 时,这个特性有时也非常方便。 - Max Desiatov
1
@MaxDesiatov 是的,但它基本上仍然是绕过存在性缺失的一种方法。真的很让人沮丧的是,协议无法符合 EnvironmentObject。这意味着我必须传递具体实现,基本上破坏了“面向协议编程”。 - flopshot
@MaxDesiatov 这是一个非常清晰明了的答案,谢谢。在缺乏广义存在时(直到今天),类型抹消是唯一可行的解决方案吗?如果类型抹消“变通”本身存在限制,例如无法向类型抹消的PAT添加init和/或静态方法怎么办? - Aurelien Porte
1
我认为你应该看一下协议见证方法,这可能是缺少广义存在性的合适解决方法,本演示文稿中有描述:https://www.youtube.com/watch?v=3BVkbWXcFS4 - Max Desiatov
显示剩余3条评论

8

简短回答

我相信你之前已经经常使用存在论,只是没有注意到它。

马克斯的重新表述答案是:

var rec: Shape = Rectangle() // Example A

只有可以访问Shape属性。而对于:
func addShape<T: Shape>() -> T // Example B

任何属性都可以访问T。因为T采用了Shape,所以也可以访问Shape的所有属性。
第一个例子是存在性的,而第二个例子不是。
真实代码示例:
protocol Shape {
  var width: Double { get }
  var height: Double { get }
}

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

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

rec1.area // ❌

然而:
let rec2 = Rectangle(width: 1, height: 2, area: 2)
func addShape<T: Shape>(_ shape: T) -> T {
    print(type(of: shape)) // Rectangle
    return shape
}
let rec3 = addShape(rec2)

print(rec3.area) // ✅

我认为对于大多数Swift用户来说,我们都理解抽象类和具体类。这些额外的术语会让人有点困惑。
棘手的地方在于第二个例子中,对于编译器来说,你返回的类型不是Shape,而是Rectangle,也就是说函数签名“转换”成了这样:
func addShape(_ shape: Rectangle) -> Rectangle {

这仅仅是因为有了“受限制的泛型”才成为可能。
然而,对于编译器来说,无论分配的类型是什么,rec: Shape = Whatever()中的类型都是Shape。 <-- 盒子类型
简而言之,在处理协议时,你要么使用受限制的泛型,要么使用存在类型,并且在大多数情况下,你不需要关心两者之间的区别。只是在一些更低级的 Swift 协议讨论中,存在类型这个术语被用来更具体地描述某些 Swift 协议的使用方式。
为什么它被称为存在类型?
计算机科学和编程中的“存在类型”一词源自哲学,它指的是存在和实在的概念。在编程的上下文中,“存在类型”用于描述表示任何特定类型的存在的类型,而不指定具体是哪种类型。
这个术语用于反映这样一个思想,即通过将值包装在存在类型中,你抽象出了它的具体类型,只承认它的存在。
换句话说,存在类型提供了一种处理不同类型值的方法,以统一的方式处理,同时忽略它们的具体类型信息†。这使您能够以更通用和灵活的方式处理值,在许多情况下都很有用,例如创建异构值集合时或处理未知或动态类型的值时。
前几天我带我的孩子去了一家商店。她问我在吃什么,我不知道我选的是什么口味,所以我没有说是草莓味还是巧克力味,我只是说:“我在吃一个冰淇淋。”她对我的回答不满意...
我只是指明它是一个冰淇淋,而没有说它的口味。我的女儿无法确定它是红色还是棕色。是否含有水果味。我给了她类似存在类型的信息。
如果我告诉她它是巧克力,那么我就会给她具体的信息。这样就不是存在类型了。
†:在B示例中,我们并没有忽略具体的类型信息。 特别感谢帮助我提供这个答案的朋友。

8
我认为值得补充一下为什么这个短语在Swift中很重要。而且,我认为几乎总是,Swift都在谈论“存在容器”。他们谈到“存在类型”,但只是与“存储在存在容器中的东西”有关。那么什么是“存在容器”呢?
据我看来,关键在于,如果你有一个变量作为参数传递或者在本地使用,并将变量类型定义为Shape,那么Swift必须在幕后做一些事情让它工作,这就是他们(间接地)所指的。
如果你考虑在你编写的库/框架模块中定义一个函数,它是公开可用的,并带有例如参数public func myFunction(shape1: Shape, shape2: Shape, shape1Rotation: CGFloat?) -> Shape……想象一下它(可选地)旋转shape1,在某种程度上“添加”它到shape2(我把细节留给你的想象力),然后返回结果。从其他面向对象的语言中来看,我们本能地认为我们理解这是如何工作的……该函数必须仅使用在Shape协议中可用的成员实现。
但问题是对于编译器来说,参数在内存中是如何表示的?同样本能地,我们认为……这并不重要。当有人编写一个在未来某个时候使用您的函数的新程序时,他们决定将自己的形状作为参数传递,并将它们定义为class Dinosaur: Shapeclass CupCake: Shape。作为定义这些新类的一部分,他们将不得不编写protocol Shape中所有方法的实现,这可能是类似于func getPointsIterator() -> Iterator<CGPoint>的东西。这对于类来说完全没有问题。调用代码定义这些类,实例化对象,并将它们传递到您的函数中。您的函数必须有一个像Shape协议这样的vtable(我认为Swift称其为witness table),它表示“如果你给我一个Shape对象的实例,我可以告诉你确切地在哪里找到getPointsIterator函数的地址”。实例指针将指向堆栈上的一块内存块,其开头是指向类元数据(vtables,witness tables等)的指针。因此,编译器可以推断如何找到任何给定方法的实现。
但是值类型呢?结构体和枚举在内存中可以具有任何格式,从一个字节的Bool到一个500字节的复杂嵌套结构。这些通常在函数调用时通过堆栈或寄存器传递以提高效率。(当Swift准确知道类型时,所有代码都可以编译并知道数据格式,并且可以通过堆栈或寄存器传递等方式传递。)
现在你可以看到问题了。Swift如何编译函数myFunction以使其能够与任何代码中定义的未来可能出现的值类型/结构体配合使用呢?据我所知,这就是“存在容器”发挥作用的地方。
最简单的方法是,任何接受这些“存在类型”之一(即只需符合协议即可定义类型)参数的函数都必须坚持调用代码将值类型“装箱”...即将值存储在堆上的特殊引用计数“盒子”中,并在函数以类型Shape的参数形式接受指向此类盒子的指针(遵循所有常规的ARC保留/释放/自动释放池/所有权规则)。
那么,当某个代码作者在未来编写新的奇异、精美绝伦的类型时,编译Shape方法就必须包括一种接受该类型“存放于盒子中”的方式。你的myFunction永远都会通过处理这个盒子来处理这些“存在类型”,从而使一切正常。我猜想,如果C#和Java对非类类型(Int等)有同样的问题,它们也会采取这种(装箱)做法吧?
事实上,对于许多值类型来说,这种方法非常低效。毕竟,我们主要是为64位架构编译,所以几个寄存器就能处理8字节大小的简单结构体了。因此,Swift提出了一个妥协方案(我的描述可能有些不准确,请随意纠正)。它们创建了“存在容器”,这些容器始终具有4个指针大小。在“普通”的64位架构上(现今大部分运行Swift的CPU),大小为16字节。
如果你定义的结构体符合某个协议并且包含12个字节或更少的数据,则直接存储在存在容器中。最后的4字节指针是指向类型信息/目击者表等的指针,以便myFunction可以为Shape协议中的任何函数找到地址(与类的情况一样)。如果你的结构体/枚举大于12字节,则4个指针值指向一个值类型的盒子。显然,这被认为是一个比较折衷的方案,并且看起来合理...它会在大多数情况下通过4个寄存器传递或通过4个堆栈插槽传递。
我认为Swift团队之所以向更广泛的社区提到“存在容器”,是因为它对使用Swift的各种方式都有影响。一个明显的影响是性能。如果结构体大小超过12字节,以这种方式使用函数时就会出现突然的性能下降。

我认为另一个更基本的含义是,只有当协议没有要求协议或Self时,才能将其用作参数... 它们不是通用的。否则,您就会进入通用函数定义的范畴,这是不同的。这就是为什么我们有时需要更改如下内容的原因:func myFunction(shape: Shape, reflection: Bool) -> Shape,变成像这样的东西:func myFunction<S:Shape>(shape: S, reflection: Bool) -> S。它们在底层以非常不同的方式实现。


非常棒的回答,而且详细得很好。 - mfaani

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接