不透明的类型(Opaque Types)

2,939 阅读10分钟

原文

有一个不透明返回类型的函数或者方法隐藏它的返回值的类型信息。替代给函数的返回类型提供一个实体类型,返回的值用它支持的协议的方式描述。需要在模块和调用模块的代码之间的边界上隐藏类型信息,因为返回值的根本的类型可以保持private。不像返回一个类型是协议类型的值,不透明类型隐藏类型的身份--编译器可以访问类型信息,但是模块的用户不可以。

不透明类型解决的问题(The Problem That Opaque Types Solve)

例如,假设你在写一个描画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>,像下面代码展示的,结果是像把一个翻转的triangle和另外一个triangle结合在一起的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艺术模块的公共接口的类型泄露。模块中的代码可以用不同的方式建立一样的形状,在使用shape的模块之外的代码不应该要负责转换的列表的实现细节。封装像JoinedShape和FlippedShape的类型不影响模块的使用者,他们不应该是可见的。模块的公共接口由像拼接和翻转形状的操作组成,并且这些操作返回另外一个Shape值。

返回一个不透明类型(Returning an Opaque Type)

可以把opaque类型想成泛型的逆向。泛型使调用函数的代码用从函数实现中抽象出来的方式为函数的参数和返回值用一个类型。例如下面代码中的函数返回一个依赖他的调用者的类型:

func max<T>(_ x: T, _ y: T) -> T where T: Comparable { ... }

调用max(_:_:)的代码为x和y选值,这些值的类型决定T的具体的类型。调用代码可以使用任何遵循Comparable协议的类型。函数中的代码用泛型的方式写,所以他可以处理任何调用者提供的类型。max(_:_:)的实现只是用Comparable类型共享的功能。

这些功能用一个不透明的返回类型都取消了。不透明类型是函数实现用从调用函数的代码中欧冠抽象出来的方式为他返回的值提供了类型。例如,下面例子中的函数不用展示那个shape的基础类型返回一个trapezoid。

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()使你表示公共接口的基础的方面--他返回的值是一个shape--不需要把那个shape的特定的类型写在公共接口中。这个是实现使用两个triangle是和一个square,但是不改变它的返回类型就可以重写函数来用许多其他的方式来画一个trapezoid。

这个例子强调一个不透明返回类型像是泛型类型的逆转。makeTrapezoid()中的代码可以返回任何他需要的类型,只要那个类型遵循Shape协议,像为一个泛型函数调用代码一样。调用函数的代码需要用通用的方式写,像一个泛型函数的实现,所以你可和任何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值和这个章节中之前The Problem That Opaque Types Solve部分中的泛型例子中的joinedTriangles一样。不过,不像那个例子中的值,flip(_:)和join(_:_:)把泛型shape操作返回的基础类型封装在了一个不透明的返回类型中,防止这些类型被看到。函数是泛型的,因为他们依赖的类型是泛型,函数的类型参数传递FlippedShape和JoinedShape需要的类型信息。

如果一个有从多个不同的地方返回不透明类型的函数,全部可能的返回值必需有一样的类型。对于泛型函数,返回值了已使用泛型类型参数,但是必须是单一的类型。例如,这里是包含一个square的特殊情况的shape-flipping函数的无效的版本:

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(_:)成为无效代码。修复invalidFlip(_:)的一种方式是吧squares的特殊情况移到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:不管传给他什么shape,repeat(shape:count:)创建并返回一个那个shape的数组。不过,返回值通常有一样的[T]基础类型,所以它遵循有不透明返回类型的函数必需只返回一种类型的值的要求。

不透明类型和协议类型的不同(Differences Between Opaque Types and Protocol Types)

返回不透明类型看起来和用协议类型作为函数的返回类型很相似,但是这两中返回类型在他们是否保存类型信息上不同。一个不透明类型引用一个特别的类型,即使函数调用者不能看哪种类型;一个协议类型可以引用任何遵循协议的类型。通常来讲,协议类型给你更多关于他们存储的值的基础类型的灵活性,不透明类型势必更能保证这些基础类型。

例如,这个例子返回一个协议类型的值代替使用不透明返回类型:

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的实例,依靠传递进来的shape是什么。两个由这个函数返回的flipped shapes可能有完全不同的类型。当翻转相同shape的多个实例的时候其他这个版本有效的版本可以返回不同类型的值。protoFlip(_:)更少的指定返回的类型信息意思是一些依靠类型信息的操作在返回值上不能获取到。例如,不可能写一个这个函数返回的==操作符对比的结果。

let protoFlippedTriangle = protoFlip(smallTriangle)
let sameThing = protoFlip(smallTriangle)
protoFlippedTriangle == sameThing  // Error

在最后一行上的错误因为多个原因产生。现在的问题是shape没有在它的协议要求中包含一个==操作符。如果你尝试增加一个,你讲遇到的下一个问题是==操作符需要知道它的左边和右边的参数的类型。这中操作符通常采用Self类型的参数,匹配任何采用协议的实际类型,但是给协议增加一个Self要求不能使类型确保当你使用协议作为类型的时候发生。

为一个函数用一个协议类型做返回类型使你可以灵活的返回任何遵循协议的类型。不过,灵活度的代价是一些操作不能在返回值上使用。例子展示了==操作符如何不能使用--它依靠通过协议类型没有保存的特殊的类型信息。

这种方法的另一个问题是shape转换不能内嵌。翻转一个triangle的结果是一个Shape类型的值,protoFlip(_:)函数采用一个遵循shape协议类型的参数。不过,一个协议类型的值没有遵循那个协议;protoFlip(_:)返回的值没有遵循shape。这意味着像采用多个转换的protoFlip(protoFlip(smallTriange))的代码是无效的,因为反转的shape对protoFlip(_:)不是有效的参数。

相对的,不透明类型保存基础类型的信息。swift可以推导关联的类型,让你使用不透明的返回值代替协议类型不能作为返回类型的地方。例如,这里是 Generics中Container协议的版本:

rotocol 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]
}

使用不透明类型some COntainer作为返回类型表示期望的API约定--函数返回一个container,但是拒绝来指定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:)的实现中,不透明container的基础类型是[T]。这个情况中,T是Int,所以返回值是一个整型的数组并且Item关联类型推导为Int。Container的下标返回Item,意味着twelve的类型也推导为Int。