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

Ado*_*els 8 type-theory swift

我正在阅读Swift Evolution 提案 244(不透明结果类型),但不明白以下含义:

... 存在类型 ...

可以通过使用存在类型Shape 而不是泛型参数来组合这些转换,但这样做将意味着比预期更多的动态性和运行时开销。

Max*_*tov 13

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

protocol Shape {
  func draw(to: Surface)
}
Run Code Online (Sandbox Code Playgroud)

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

protocol Shape {
  func draw(to: Surface)
}
Run Code Online (Sandbox Code Playgroud)

与使用泛型参数相反Other

func collides(with: Shape) -> Bool
Run Code Online (Sandbox Code Playgroud)

重要的是要注意,Shape协议本身不是一个存在类型,仅在上面的“协议作为类型”上下文中使用它“创建”一个存在类型。请参阅Swift 核心团队成员的这篇文章

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

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

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

func collides<Other: Shape>(with: Other) -> Bool
Run Code Online (Sandbox Code Playgroud)

更深的潜水

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

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
Run Code Online (Sandbox Code Playgroud)

这大致相当于这个 Swift 片段(如果我们假设 Swift 的协议或多或少代表 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
}
Run Code Online (Sandbox Code Playgroud)

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

extension String: Buffer {}
extension Data: Buffer {}
Run Code Online (Sandbox Code Playgroud)

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

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

如果您想要更深入的了解,请查看具有关联类型 (PAT) 的协议,由于各种原因,目前无法将其用作存在项。还有一些或多或少有规律地提出的Generalized Existentials提案,但从 Swift 5.3 开始,没有任何具体内容。实际上,OP 链接的原始Opaque Result Types提案可以解决使用 PAT 导致的一些问题,并显着缓解 Swift 中缺乏泛化存在的问题。

  • 好吧,我们现在有了不透明的结果类型,但它们并没有减轻太多的影响。 (2认同)
  • 当然,如果没有不透明的结果类型,SwiftUI API 会很麻烦,同样,这个功能在使用合并时有时非常方便。 (2认同)

Hon*_*ney 13

简答

\n

我确信您之前已经多次使用过存在主义而没有注意到。

\n

Max 重新措辞的答案是:

\n
var rec: Shape = Rectangle() // Example A\n
Run Code Online (Sandbox Code Playgroud)\n

只能Shape访问属性。

\n

而对于:

\n
func addShape<T: Shape>() -> T // Example B\n
Run Code Online (Sandbox Code Playgroud)\n

的任何属性都T可以访问。因为T采用了Shape所有的属性Shape,所以也可以访问 的

\n

第一个例子是存在主义的,第二个例子则不是。存在性的另一种命名是“协议作为类型”

\n

正如马克斯在回答中所说:

\n
\n

“这里需要注意的是,Shape 协议本身并不是一种存在类型,仅在上面的“协议作为类型”上下文中使用它,从中“创建”一个存在类型”

\n
\n

真实代码示例:

\n
protocol Shape {\n  var width: Double { get }\n  var height: Double { get }\n}\n\nstruct Rectangle: Shape {\n  var width: Double\n  var height: Double\n  var area: Double\n}\n\nlet rec1: Shape = Rectangle(width: 1, height: 2, area: 2)\n\nrec1.area // \xe2\x9d\x8c\n
Run Code Online (Sandbox Code Playgroud)\n

然而:

\n
let rec2 = Rectangle(width: 1, height: 2, area: 2)\nfunc addShape<T: Shape>(_ shape: T) -> T {\n    print(type(of: shape)) // Rectangle\n    return shape\n}\nlet rec3 = addShape(rec2)\n\nprint(rec3.area) // \xe2\x9c\x85\n
Run Code Online (Sandbox Code Playgroud)\n
\n

我认为对于大多数 Swift 用户来说,我们都理解抽象类和具体类。这些额外的术语让人有点困惑。

\n

棘手的是,对于第二个示例,对于编译器,您返回的类型不是Shape,而是Rectangle函数签名转换为:

\n
func addShape(_ shape: Rectangle) -> Rectangle {\n
Run Code Online (Sandbox Code Playgroud)\n

这只是由于(受限的)泛型而成为可能成为可能。

\n

然而对于rec: Shape = Whatever()编译器来说,类型是Shape与分配类型无关。<-- 盒式

\n

tldr 当谈到协议时,你要么使用

\n
    \n
  • 受限泛型
  • \n
  • 存在主义的
  • \n
\n

大多数情况下,您并不关心两者之间的差异。只是在一些更底层的 Swift 协议讨论中,术语“存在性”的出现是为了更具体地说明某些 Swift 协议的使用。

\n

为什么叫存在主义?

\n

计算机科学和编程中的“存在”一词借用自哲学,指的是存在和存在的概念。在编程上下文中,“存在”用于描述表示任何特定类型的存在的类型,而不指定它是什么类型。

\n

该术语用于反映这样的想法:通过将值包装在存在类型中,您可以抽象出其特定类型并仅确认其存在

\n

换句话说,存在类型提供了一种以统一的方式处理不同类型值的方法。的值的方法,而忽略它们的具体类型信息\xe2\x80\xa0。这允许您以更通用和灵活的方式使用值,这在许多情况下都很有用,例如在创建异构值的集合时,或者在使用未知或动态类型的值时。

\n

有一天,我带孩子去了一家商店。她问你吃什么,我不知道我选的是什么口味,所以我没有说是草莓味的,也没有说巧克力味的,我只是说“我在吃冰淇淋”。她对我的回答不满意...

\n

我只是明确指出这是一种冰淇淋,但没有说明它的味道。我女儿再也无法确定它是红色还是棕色。是否有水果味。我给了她类似存在主义的信息。

\n

如果我告诉她这是巧克力,那么我就会向她提供具体信息。那么这不是存在的。

\n

简而言之,存在意味着我们对类型有所了解,但不是全部。事情有点模糊/不完整。就像我们不知道它的关联类型或静态类型信息一样。

\n

\xe2\x80\xa0:在示例 B 中,我们没有忽略特定的类型信息。

\n

测验

\n
1. var x: [SomeProtocol]\n2. var y: SomeProtocol = SomeClass()\n3. func jump(item: SomeProtocol) {...}\n4. func jump(item: any SomeProtocol) {...}\n5. func hype<T: SomeProtocol) {...}\n
Run Code Online (Sandbox Code Playgroud)\n
\n

1,2,3,4 是存在的,因为someProtocol被用作类型。
\n\n 4 是显式存在主义。请参阅此提案了解更多信息
\n\n 5 是约束(通用)类型。请参阅泛型 - 类型约束

\n
\n

读完我的答案后,我建议你去重新阅读Max的答案。一段时间内可能需要5-6次才能完全理解。

\n

特别感谢帮助我找到这个答案的朋友

\n


小智 9

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

在我看来,关键是,如果你有一个变量作为参数传递或在本地使用等,并且你定义了变量的类型,那么ShapeSwift 必须在幕后做一些事情来使它有效,这就是他们(间接)所指的。

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

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

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

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

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

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

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

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

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

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