这是 Swfit 5.0.1新增的特性
具有不透明返回类型的函数或方法隐藏其返回值的类型信息。返回值不是作为函数的返回类型提供具体的类型,而是根据它所支持的协议来描述的。隐藏类型信息在模块和调用模块的代码之间的边界很有用,因为返回值的底层类型可以保持私有。与返回类型为协议类型的值不同,不透明类型保留类型标识—编译器可以访问类型信息,但是模块的客户机不能。
不透明类型解决的问题
例如,假设您正在编写一个绘制ASCII艺术图形的模块。ASCII艺术形状的基本特征是一个draw()函数,它返回该形状的字符串表示形式,您可以将其用作形状协议的要求:
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这样的包装器类型对模块的用户并不重要,它们不应该是可见的。模块的公共接口由连接和翻转形状等操作组成,这些操作返回另一个形状值。
返回不透明类型
您可以将不透明类型看作泛型类型的反面。泛型类型允许调用函数的代码为该函数的参数选择类型,并以一种与函数实现分离的方式返回值。例如,下面代码中的函数返回依赖于调用者的类型:
func max<T>(_ x: T, _ y: T) -> T where T: Comparable { ... }
调用max(::)的代码选择x和y的值,这些值的类型决定了t的具体类型。调用代码可以使用任何符合可比协议的类型。函数内部的代码以一种通用的方式编写,因此它可以处理调用者提供的任何类型。max(::)的实现只使用所有可比较类型共享的功能。
对于具有不透明返回类型的函数,这些角色是相反的。不透明类型允许函数实现为它返回的值选择类型,这种方法是从调用函数的代码中抽象出来的。例如,下面示例中的函数返回一个梯形,但不暴露该形状的底层类型。
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()函数将其返回类型声明为某种形状;因此,函数返回一个符合Shape协议的给定类型的值,而不指定任何特定的具体类型。以这种方式编写makeTrapezoid()可以让它表达其公共接口的基本方面(它返回的值是一个形状),而不需要生成形状由其公共接口的一部分构成的特定类型。这个实现使用了两个三角形和一个正方形,但是可以重写这个函数,以多种其他方式绘制梯形,而不改变它的返回类型。
本例突出显示了不透明返回类型与泛型类型的相反之处。makeTrapezoid()中的代码可以返回它需要的任何类型,只要该类型符合Shape协议,就像调用泛型函数的代码一样。调用函数的代码需要以通用的方式编写,就像实现泛型函数一样,这样它就可以处理makeTrapezoid()返回的任何形状值。
还可以将不透明的返回类型与泛型组合。下面代码中的函数都返回符合形状协议的某种类型的值。
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())
// *
// **
// ***
// ***
// **
// *
本例中opaquejoinedtriangle的值与本章前面不透明类型解决的问题一节中泛型示例中的joinedtriangle的值相同。但是,与该示例中的值不同,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
}
如果你用一个正方形调用这个函数,它会返回一个正方形;否则,它返回一个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协议。换句话说,与flip(:)相比,protoFlip(:)与调用者之间的API契约要宽松得多。保留返回多种类型值的灵活性:
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
示例的最后一行出现错误有几个原因。当前的问题是,该形状没有将==操作符作为其协议要求的一部分。如果您尝试添加一个,您将遇到的下一个问题是==操作符需要知道其左参数和右参数的类型。这种类型的操作符通常接受Self类型的参数,匹配采用协议的任何具体类型,但是在协议中添加Self需求并不允许将协议用作类型时发生的类型擦除。
使用协议类型作为函数的返回类型,可以灵活地返回任何符合协议的类型。然而,这种灵活性的代价是无法对返回的值执行某些操作。这个例子显示了==操作符是如何不可用的——它取决于使用协议类型不能保存的特定类型信息。
这种方法的另一个问题是形状转换不嵌套。翻转三角形的结果是Shape类型的值,protoFlip(:)函数接受某种类型的参数,该参数符合Shape协议。但是,协议类型的值不符合该协议;protoFlip(:)返回的值不符合形状。这意味着像protoFlip(protoFlip(smallTriange))这样应用多个转换的代码是无效的,因为翻转的形状不是protoFlip(_:)的有效参数。
相反,不透明类型保留底层类型的标识。Swift可以推断相关类型,这让您可以在协议类型不能用作返回值的地方使用不透明的返回值。例如,下面是来自泛型的容器协议的一个版本:
protocol Container {
associatedtype Item
var count: Int { get }
subscript(i: Int) -> Item { get }
}
extension Array: 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]
}
使用不透明类型some Container作为返回类型表达了所需的API契约——函数返回一个容器,但是拒绝指定容器的类型:
func makeOpaqueContainer<T>(item: T) -> some Container {
return [item]
}
let opaqueContainer = makeOpaqueContainer(item: 12)
let twelve = opaqueContainer[0]
print(type(of: twelve))
// Prints "Int"
2的类型被推断为Int,这说明了类型推断与不透明类型一起工作的事实。在makeOpaqueContainer(item:)的实现中,不透明容器的底层类型是[T]。在本例中,T是Int,所以返回值是一个整数数组,并且推断出与项目相关的类型为Int.容器上的下标返回项目,这意味着12的类型也推断为Int。