一个带有不透明类型的函数或方法都会隐藏其返回值类型的信息。返回值描述在支持的协议中,隐藏类型信息在模块和调用模块的代码之间的边界上很有用,因为返回值的基础类型可以保持私有。与返回类型为协议类型的值不同,不透明类型保留类型标识 - 编译器可以访问类型信息,但模块的客户端不能访问类型信息。
举个例子,假设我们要写一个用来画ASCII艺术形状的时候,这些ASCII艺术形状最基本的一个特性就是用draw()
函数来表达的。返回的字符串用来展示这个形状。我们可以使用Shape协议。
protocol Shape {
func draw() -> String
}
// struct triangle conforms the protocol shape
struct Triangle: Shape {
var size: Int // if we define a var that means we are going to change it later
func draw() -> String {
var result = [String]()
for length in 1...size {
result.append(String(repeating: "*", count: length))
}
// struct a string as the return value
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
,该结构体垂直地联接或组合两个图形,就像下面代码写的那样,结果就好像是JoinedShape
翻转过来的一样。
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艺术形状公共借口模块的一部分泄漏出来。因为该结构体全部的返回类型可以看出。在模块的代码中可以有多个方法来创建该形状,使用形状的模块外部的其他代码不应考虑有关转换列表的实现详细信息。包装器类型(wrapper type)就像JoinedShape
和FlippedShape
对模块的使用者来说并不重要,模块的公共接口是由联接和翻转形状的操作组成,所以这两个操作会返回另一个Shape
的值。
我们可以把不透明类型看作是泛型类型的颠倒。泛型类型允许代码调用函数然后选择函数参数的类型,然后以在函数的实现中提取的方法来返回返回值。举个例子 下面的函数代码返回的值必须依靠与它的调用者(caller)
func max<T>(_ x: T, _ y: T) -> T where T: Comparable { ... }
调用这段代码的函数将会在类型T里面选择x和y的值,而返回的类型是一段带有泛型where子句的类型T,指明了该返回值T where T: Comparable
作为一个组合值,其中类型T遵循Comparable协议的部分作为类型T的返回值。
带有不透明类型的函数对于这些规则都是颠倒的。不透明类型允许函数的实现以从调用函数的代码提取的方式为它返回的值选择类型。举个例子,下面例子中的函数返回的是一个梯型而不公开该梯型的形状,
// 定义构建该图形的架构
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)
// 翻转top来实现bottom
let bottom = FlippedShape(shape: top)
// 联接top和bottom
let trapezoid = JoinedShape(
top: top,
// bottom是一个联接的图形(联接middle和bottom组成该trapezoid的bottom)
bottom: JoinedShape(top: middle, bottom: bottom)
)
return trapezoid
}
// 实例化并输出
let trapezoid = makeTrapezoid()
print(trapezoid.draw())
输出:
*
**
**
**
**
*
上面例子中的函数makeTrapezoid()
声明来它的返回类型some Shape
作为返回的结果。函数返回一个给出类型的值,给出的类型遵循了Shape协议,这样写函数makeTrapezoid()
只是简单表达了模型公共接口的基本层面。返回的值是一个图形。该实现使用的是两个长方形和一个正方形,但是函数可以以一个不改变返回类型的方式来复写一个梯型。
这个例子着重突出了不透明类型可以当作是泛型类型的颠倒来使用。只要类型遵循Shape
协议,函数makeTrapezoid()
中的代码可以返回它需要的任何类型。就像代码为调用泛型类型所做的那样一样,所以用泛型实现的方法老写调用函数的代码。所以它实用任何于函数makeTrapezoid()所返回的Shape值。
我们可以把泛型和不透明返回类型组合起来,下面的函数都是返回值的类型遵循了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(_:_:)
封装泛型形状操作在不透明返回类型中返回的基础类型,wrap the underlying types that the generic shape operations return in an opaque return type。该封装的基础类型用来预防这些类型的可见性。也就是说封装过后的这些基础类型是不可见的。这两个函数都是泛型类型的因为类型都要依赖于泛型,并且传入给函数的类型参数都会添加上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
}
如果我们用Square来调用这个函数的话,返回的是一个Square,否则它返回的是FlippedShape
,这个违反了仅返回一种类型值的要求,并且会使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
,所以它遵循了不透明返回类型的函数必须只返回单个类型的值的要求。