... 存在类型 ...
可以使用存在类型Shape组合这些转换,而不是使用泛型参数,但这样做会导致更多的动态性和运行时开销,可能不太理想。
... 存在类型 ...
可以使用存在类型Shape组合这些转换,而不是使用泛型参数,但这样做会导致更多的动态性和运行时开销,可能不太理想。
可以用作存在类型的协议示例在演进提案中已经给出:
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
}
forall
量词。您可以将其解释为“对于符合Buffer
类型类(Swift中的“协议”)的所有类型,只要它们的X
和Y
类型参数相同,那么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中广义存在的缺乏。EnvironmentObject
。这意味着我必须传递具体实现,基本上破坏了“面向协议编程”。 - flopshot我相信你之前已经经常使用存在论,只是没有注意到它。
马克斯的重新表述答案是:
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) // ✅
func addShape(_ shape: Rectangle) -> Rectangle {
rec: Shape = Whatever()
中的类型都是Shape
。 <-- 盒子类型Shape
,那么Swift必须在幕后做一些事情让它工作,这就是他们(间接地)所指的。public func myFunction(shape1: Shape, shape2: Shape, shape1Rotation: CGFloat?) -> Shape
……想象一下它(可选地)旋转shape1,在某种程度上“添加”它到shape2(我把细节留给你的想象力),然后返回结果。从其他面向对象的语言中来看,我们本能地认为我们理解这是如何工作的……该函数必须仅使用在Shape
协议中可用的成员实现。class Dinosaur: Shape
和class CupCake: Shape
。作为定义这些新类的一部分,他们将不得不编写protocol Shape
中所有方法的实现,这可能是类似于func getPointsIterator() -> Iterator<CGPoint>
的东西。这对于类来说完全没有问题。调用代码定义这些类,实例化对象,并将它们传递到您的函数中。您的函数必须有一个像Shape
协议这样的vtable(我认为Swift称其为witness table),它表示“如果你给我一个Shape对象的实例,我可以告诉你确切地在哪里找到getPointsIterator
函数的地址”。实例指针将指向堆栈上的一块内存块,其开头是指向类元数据(vtables,witness tables等)的指针。因此,编译器可以推断如何找到任何给定方法的实现。myFunction
以使其能够与任何代码中定义的未来可能出现的值类型/结构体配合使用呢?据我所知,这就是“存在容器”发挥作用的地方。Shape
的参数形式接受指向此类盒子的指针(遵循所有常规的ARC保留/释放/自动释放池/所有权规则)。Shape
方法就必须包括一种接受该类型“存放于盒子中”的方式。你的myFunction
永远都会通过处理这个盒子来处理这些“存在类型”,从而使一切正常。我猜想,如果C#和Java对非类类型(Int等)有同样的问题,它们也会采取这种(装箱)做法吧?myFunction
可以为Shape
协议中的任何函数找到地址(与类的情况一样)。如果你的结构体/枚举大于12字节,则4个指针值指向一个值类型的盒子。显然,这被认为是一个比较折衷的方案,并且看起来合理...它会在大多数情况下通过4个寄存器传递或通过4个堆栈插槽传递。我认为另一个更基本的含义是,只有当协议没有要求协议或Self时,才能将其用作参数... 它们不是通用的。否则,您就会进入通用函数定义的范畴,这是不同的。这就是为什么我们有时需要更改如下内容的原因:func myFunction(shape: Shape, reflection: Bool) -> Shape
,变成像这样的东西:func myFunction<S:Shape>(shape: S, reflection: Bool) -> S
。它们在底层以非常不同的方式实现。