Swift5.4 语言指南(二十五) 不透明类型
★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★
➤微信公众号:山青咏芝(shanqingyongzhi)
➤博客园地址:山青咏芝(https://www.cnblogs.com/strengthen/)
➤GitHub地址:https://github.com/strengthen/LeetCode
➤原文地址:https://www.cnblogs.com/strengthen/p/9739902.html
➤如果链接不是山青咏芝的博客园地址,则可能是爬取作者的文章。
➤原文已修改更新!强烈建议点击原文地址阅读!支持作者!支持原创!
★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★
返回值类型不透明的函数或方法将隐藏其返回值的类型信息。与其提供具体类型作为函数的返回类型,不如根据其所支持的协议来描述返回值。隐藏类型信息在模块和调用该模块的代码之间的边界处很有用,因为返回值的基础类型可以保持私有。与返回类型为协议类型的值不同,不透明类型保留类型标识-编译器可以访问类型信息,而模块的客户端则不能。
不透明类型解决的问题
例如,假设您正在编写一个绘制ASCII艺术形状的模块。ASCII艺术形状的基本特征是一个draw()
函数,该函数返回该形状的字符串表示形式,您可以将其用作Shape
协议的要求:
- protocol Shape {
- func draw() -> String
- }
- struct Triangle: Shape {
- var size: Int
- func draw() -> String {
- var result = [String]()
- for length in 1...size {
- result.append(String(repeating: "*", count: length))
- }
- return result.joined(separator: "\n")
- }
- }
- let smallTriangle = Triangle(size: 3)
- print(smallTriangle.draw())
- // *
- // **
- // ***
您可以使用泛型来实现诸如垂直翻转形状的操作,如下面的代码所示。但是,此方法有一个重要限制:翻转的结果会公开用于创建它的确切泛型类型。
- struct FlippedShape<T: Shape>: Shape {
- var shape: T
- func draw() -> String {
- let lines = shape.draw().split(separator: "\n")
- return lines.reversed().joined(separator: "\n")
- }
- }
- let flippedTriangle = FlippedShape(shape: smallTriangle)
- print(flippedTriangle.draw())
- // ***
- // **
- // *
定义这种将两个形状垂直连接在一起的结构的方法,如下面的代码所示,其结果类似于将翻转的三角形与另一个三角形连接在一起。JoinedShape<T: Shape, U: Shape>
JoinedShape<FlippedShape<Triangle>, Triangle>
- struct JoinedShape<T: Shape, U: Shape>: Shape {
- var top: T
- var bottom: U
- func draw() -> String {
- return top.draw() + "\n" + bottom.draw()
- }
- }
- let joinedTriangles = JoinedShape(top: smallTriangle, bottom: flippedTriangle)
- print(joinedTriangles.draw())
- // *
- // **
- // ***
- // ***
- // **
- // *
公开有关形状创建的详细信息将使那些不希望成为ASCII art模块的公共接口一部分的类型泄漏,因为需要声明完整的返回类型。模块内的代码可以通过多种方式构建相同的形状,而使用该形状的模块外的其他代码则不必考虑有关转换列表的实现细节。包装类型,如JoinedShape
和FlippedShape
不事到模块的用户,他们不应该是可见的。模块的公共接口由诸如连接和翻转形状之类的操作组成,这些操作返回另一个Shape
值。
返回不透明类型
您可以将不透明类型想像成与通用类型相反的类型。泛型类型使调用函数的代码可以从函数实现中抽象出来的方式为该函数的参数选择类型并返回值。例如,以下代码中的函数返回取决于其调用者的类型:
- func max<T>(_ x: T, _ y: T) -> T where T: Comparable { ... }
码的呼叫max(_:_:)
选择的值x
和y
,和这些值的类型决定的具体类型T
。调用代码可以使用符合Comparable
协议的任何类型。函数内部的代码以通用方式编写,因此它可以处理调用者提供的任何类型。max(_:_:)
仅使用所有Comparable
类型共享的功能的实现。
对于具有不透明返回类型的函数,这些角色是相反的。不透明类型使函数实现可以从调用函数的代码中抽象出来的方式为返回的值选择类型。例如,以下示例中的函数返回梯形而不暴露该形状的基础类型。
- struct Square: Shape {
- var size: Int
- func draw() -> String {
- let line = String(repeating: "*", count: size)
- let result = Array<String>(repeating: line, count: size)
- return result.joined(separator: "\n")
- }
- }
- func makeTrapezoid() -> some Shape {
- let top = Triangle(size: 2)
- let middle = Square(size: 2)
- let bottom = FlippedShape(shape: top)
- let trapezoid = JoinedShape(
- top: top,
- bottom: JoinedShape(top: middle, bottom: bottom)
- )
- return trapezoid
- }
- let trapezoid = makeTrapezoid()
- print(trapezoid.draw())
- // *
- // **
- // **
- // **
- // **
- // *
makeTrapezoid()
本例中的函数将其返回类型声明为;结果,该函数将返回某个符合协议的给定类型的值,而无需指定任何特定的具体类型。通过这种方式编写,可以表达其公共接口的基本方面(它返回的值是一个形状),而无需做出由其公共接口的一部分构成形状的特定类型。此实现使用两个三角形和一个正方形,但是可以重写函数以多种其他方式绘制梯形而无需更改其返回类型。some Shape
Shape
makeTrapezoid()
此示例突出显示了不透明返回类型类似于通用类型的反向方式。内部代码makeTrapezoid()
可以返回所需的任何类型,只要该类型符合Shape
协议,就像调用代码对泛型函数所做的那样。调用函数的代码需要以通用方式编写,例如通用函数的实现,以便它可以与Shape
所返回的任何值一起使用makeTrapezoid()
。
您还可以将不透明的返回类型与泛型结合使用。以下代码中的函数均返回符合Shape
协议的某种类型的值。
- func flip<T: Shape>(_ shape: T) -> some Shape {
- return FlippedShape(shape: shape)
- }
- func join<T: Shape, U: Shape>(_ top: T, _ bottom: U) -> some Shape {
- JoinedShape(top: top, bottom: bottom)
- }
- let opaqueJoinedTriangles = join(smallTriangle, flip(smallTriangle))
- print(opaqueJoinedTriangles.draw())
- // *
- // **
- // ***
- // ***
- // **
- // *
opaqueJoinedTriangles
此示例中的值与本章前面的“不透明类型解决的问题”部分joinedTriangles
中的泛型示例相同。然而,与在该示例中的值,并且包裹的底层类型的通用形状的操作在一个不透明的返回类型,这防止这些类型的从可见返回。这两个函数都是通用的,因为它们所依赖的类型是通用的,并且函数的类型参数传递了和所需的类型信息。flip(_:)
join(_:_:)
FlippedShape
JoinedShape
如果返回类型不透明的函数从多个位置返回,则所有可能的返回值都必须具有相同的类型。对于泛型函数,该返回类型可以使用函数的泛型类型参数,但它仍必须是单个类型。例如,以下是形状翻转功能的无效版本,其中包括正方形的特殊情况:
- func invalidFlip<T: Shape>(_ shape: T) -> some Shape {
- if shape is Square {
- return shape // Error: return types don't match
- }
- return FlippedShape(shape: shape) // Error: return types don't match
- }
如果使用a调用此函数Square
,则返回a Square
。否则,返回FlippedShape
。这违反了仅返回一种类型的值的要求,并且使invalidFlip(_:)
代码无效。修复的一种方法invalidFlip(_:)
是将正方形的特殊情况移到的实现中FlippedShape
,这使该函数始终返回一个FlippedShape
值:
- struct FlippedShape<T: Shape>: Shape {
- var shape: T
- func draw() -> String {
- if shape is Square {
- return shape.draw()
- }
- let lines = shape.draw().split(separator: "\n")
- return lines.reversed().joined(separator: "\n")
- }
- }
始终返回单个类型的要求并不妨碍您在不透明的返回类型中使用泛型。这是一个函数的示例,该函数将其类型参数合并到其返回的值的基础类型中:
- func `repeat`<T: Shape>(shape: T, count: Int) -> some Collection {
- return Array<T>(repeating: shape, count: count)
- }
在这种情况下,返回值的基础类型取决于T
:无论传递什么形状,它repeat(shape:count:)
都会创建,返回该形状的数组。尽管如此,返回值始终具有相同的基础类型[T]
,因此它遵循以下要求:具有不透明返回类型的函数必须仅返回单个类型的值。
不透明类型和协议类型之间的差异
返回不透明类型看起来与使用协议类型作为函数的返回类型非常相似,但是这两种返回类型在是否保留类型标识方面有所不同。不透明类型是指一种特定类型,尽管函数的调用者看不到哪种类型。协议类型可以指符合协议的任何类型。一般而言,协议类型为您提供它们存储的值的基础类型的更多灵活性,而不透明类型使您可以对这些基础类型做出更强的保证。
例如,以下flip(_:)
是使用协议类型作为其返回类型而不是不透明返回类型的版本:
- func protoFlip<T: Shape>(_ shape: T) -> Shape {
- return FlippedShape(shape: shape)
- }
此版本的protoFlip(_:)
正文与相同flip(_:)
,并且始终返回相同类型的值。与不同的flip(_:)
是,protoFlip(_:)
返回的值不必始终具有相同的类型-只需遵守Shape
协议即可。换句话说,protoFlip(_:)
与调用者签订的API合同要比make宽松得多flip(_:)
。它保留返回多种类型的值的灵活性:
- func protoFlip<T: Shape>(_ shape: T) -> Shape {
- if shape is Square {
- return shape
- }
- return FlippedShape(shape: shape)
- }
该代码的修订版返回的实例Square
或的实例FlippedShape
,具体取决于传入的形状。此函数返回的两个翻转的形状可能具有完全不同的类型。当翻转相同形状的多个实例时,此函数的其他有效版本可能返回不同类型的值。来自的不太具体的返回类型信息protoFlip(_:)
意味着,依赖于类型信息的许多操作在返回值上不可用。例如,不可能编写一个==
运算符来比较此函数返回的结果。
- let protoFlippedTriangle = protoFlip(smallTriangle)
- let sameThing = protoFlip(smallTriangle)
- protoFlippedTriangle == sameThing // Error
该示例最后一行的错误是由于多种原因而发生的。直接的问题是,Shape
不将==
运算符作为其协议要求的一部分。如果尝试添加一个,那么您将遇到的下一个问题是==
操作员需要知道其左手参数和右手参数的类型。这种类型的运算符通常采用type的参数Self
,匹配采用该协议的任何具体类型,但是向协议中添加Self
要求不允许使用协议作为类型时发生类型擦除。
通过使用协议类型作为函数的返回类型,可以灵活地返回符合协议的任何类型。但是,这种灵活性的代价是无法对返回的值进行某些操作。该示例说明了如何使用==
运算符-它取决于使用协议类型不能保留的特定类型信息。
这种方法的另一个问题是形状转换不会嵌套。翻转三角形的结果是type的值Shape
,并且该protoFlip(_:)
函数采用符合Shape
协议的某种类型的参数。但是,协议类型的值不符合该协议。返回的值protoFlip(_:)
不符合Shape
。这意味着protoFlip(protoFlip(smallTriange))
应用多次转换的代码无效,因为翻转的形状不是的有效参数protoFlip(_:)
。
相反,不透明类型保留了基础类型的标识。Swift可以推断关联的类型,这使您可以在协议类型不能用作返回值的地方使用不透明的返回值。例如,这是Generics的Container
协议版本:
- protocol Container {
- associatedtype Item
- var count: Int { get }
- subscript(i: Int) -> Item { get }
- }
- extension Array: Container { }
您不能将其Container
用作函数的返回类型,因为该协议具有关联的类型。您也不能将其用作泛型返回类型中的约束,因为在函数体外部没有足够的信息来推断泛型类型需要是什么。
- // Error: Protocol with associated types can't be used as a return type.
- func makeProtocolContainer<T>(item: T) -> Container {
- return [item]
- }
- // Error: Not enough information to infer C.
- func makeProtocolContainer<T, C: Container>(item: T) -> C {
- return [item]
- }
使用不透明类型作为返回类型表示所需的API合同-该函数返回一个容器,但拒绝指定该容器的类型:some Container
- func makeOpaqueContainer<T>(item: T) -> some Container {
- return [item]
- }
- let opaqueContainer = makeOpaqueContainer(item: 12)
- let twelve = opaqueContainer[0]
- print(type(of: twelve))
- // Prints "Int"
的类型twelve
推断为Int
,这说明了类型推断适用于不透明类型的事实。在的实现中makeOpaqueContainer(item:)
,不透明容器的基础类型为[T]
。在这种情况下,T
is Int
,因此返回值是一个整数数组,并且Item
推断出关联的类型为is Int
。上标Container
返回Item
,这意味着的类型twelve
也被推断为Int
。