虽然使用不透明类型作为函数返回值,看起来和返回协议类型非常相似,但这两者有一个主要区别,就在于是否需要保证类型一致性。一个不透明类型只能对应一个具体的类型,即便函数调用者并不能知道是哪一种类型;协议类型可以同时对应多个类型,只要它们都遵循同一协议。总的来说,协议类型更具灵活性,底层类型可以存储更多样的值,而不透明类型对这些底层类型有更强的限定。
比如,这是 flip(_:)
方法不采用不透明类型,而采用返回协议类型的版本:
func protoFlip<T: Shape>(_ shape: T) -> Shape {
return FlippedShape(shape: shape)
}
这个版本的 protoFlip(_:)
和 flip(_:)
有相同的函数体,并且它也始终返回唯一类型。但不同于 flip(_:)
,protoFlip(_:)
返回值其实不需要始终返回唯一类型 —— 返回类型只需要遵循 Shape
协议即可。换句话说,protoFlip(_:)
比起 flip(_:)
对 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 // 错误
上面的例子中,最后一行的错误来源于多个原因。最直接的问题在于,Shape
协议中并没有包含对 == 运算符的声明。如果你尝试加上这个声明,那么你会遇到新的问题,就是 == 运算符需要知道左右两侧参数的类型。这类运算符通常会使用 Self
类型作为参数,用来匹配符合协议的具体类型,但是由于将协议当成类型使用时会发生类型擦除,所以并不能给协议加上对 Self
的实现要求。
将协议类型作为函数的返回类型能更加灵活,函数只要返回遵循协议的类型即可。然而,更具灵活性导致牺牲了对返回值执行某些操作的能力。上面的例子就说明了为什么不能使用 == 运算符 —— 它依赖于具体的类型信息,而这正是使用协议类型所无法提供的。
这种方法的另一个问题在于,变换形状的操作不能嵌套。翻转三角形的结果是一个 Shape
类型的值,而 protoFlip(_:)
方法的则将遵循 Shape
协议的类型作为形参,然而协议类型的值并不遵循这个协议;protoFlip(_:)
的返回值也并不遵循 Shape
协议。这就是说 protoFlip(protoFlip(smallTriange))
这样的多重变换操作是非法的,因为经过翻转操作后的结果类型并不能作为 protoFlip(_:)
的形参。
相比之下,不透明类型则保留了底层类型的唯一性。Swift 能够推断出关联类型,这个特点使得作为函数返回值,不透明类型比协议类型有更大的使用场景。比如,下面这个例子是 第二十四章:泛型 中讲到的 Container
协议:
protocol Container {
associatedtype Item
var count: Int { get }
subscript(i: Int) -> Item { get }
}
extension Array: Container { }
你不能将 Container
作为方法的返回类型,因为此协议有一个关联类型。你也不能将它用于对泛型返回类型的约束,因为函数体之外并没有暴露足够多的信息来推断泛型类型。
// 错误:有关联类型的协议不能作为返回类型。
func makeProtocolContainer<T>(item: T) -> Container {
return [item]
}
// 错误:没有足够多的信息来推断 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))
// 输出 "Int"
twelve
的类型可以被推断出为 Int
, 这说明了类型推断适用于不透明类型。在 makeOpaqueContainer(item:)
的实现中,底层类型是不透明集合 [T]
。在上述这种情况下,T
就是 Int
类型,所以返回值就是整数数组,而关联类型 Item
也被推断出为 Int
。Container
协议中的 subscipt
方法会返回 Item
,这也意味着 twelve
的类型也被能推断出为 Int
。