25.3. 不透明类型和协议类型的区别

虽然使用不透明类型作为函数返回值,看起来和返回协议类型非常相似,但这两者有一个主要区别,就在于是否需要保证类型一致性。一个不透明类型只能对应一个具体的类型,即便函数调用者并不能知道是哪一种类型;协议类型可以同时对应多个类型,只要它们都遵循同一协议。总的来说,协议类型更具灵活性,底层类型可以存储更多样的值,而不透明类型对这些底层类型有更强的限定。

比如,这是 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 也被推断出为 IntContainer 协议中的 subscipt 方法会返回 Item,这也意味着 twelve 的类型也被能推断出为 Int