Swift 5.x - 不透明类型(中文文档)

823 阅读10分钟

引言

继续学习Swift文档,从上一章节:泛型,我们学习了Swift协议的内容,主要有泛型代码使您能够编写灵活、可重用的函数和类型,这些函数和类型可以根据您定义的需求使用任何类型。您可以编写避免重复并以清晰、抽象的方式表达其意图的代码。现在,我们学习Swift不透明类型的相关内容。由于篇幅较长,这里分篇来记录,接下来,开始吧!

不透明类型

具有不透明返回类型的函数或方法隐藏其返回值的类型信息。不是提供一个具体的类型作为函数的返回类型,而是根据它支持的协议来描述返回值。隐藏类型信息在模块和调用模块的代码之间的边界处很有用,因为返回值的底层类型可以保持私有。与返回类型为协议类型的值不同,不透明类型保留类型标识,编译器可以访问类型信息,但模块的客户端没有。

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<flipedshape<\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这样的包装类型对于模块的用户来说并不重要,它们不应该是可见的。模块的公共接口由连接和翻转形状等操作组成,这些操作返回另一个形状值。

2 返回不透明类型

可以将不透明类型视为泛型类型的反面。泛型类型允许调用函数的代码为该函数的参数和返回值选择类型,这种方法是从函数实现中抽象出来的。例如,以下代码中的函数返回依赖于调用方的类型:

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

调用max的代码选择x和y的值,这些值的类型决定了T的具体类型。调用代码可以使用任何符合Comparable协议的类型。函数内部的代码是以通用方式编写的,因此它可以处理调用方提供的任何类型。max(::)的实现只使用所有Comparable类型共享的功能。

对于具有不透明返回类型的函数,这些角色是相反的。不透明类型允许函数实现为它返回的值选择类型,这种方法是从调用函数的代码中抽象出来的。例如,下例中的函数返回一个梯形,而不公开该形状的基础类型。

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())
// *
// **
// **
// **
// **
// *

此示例中的makedrapeoid()函数将其返回类型声明为某种形状;因此,该函数返回符合形状协议的某个给定类型的值,而不指定任何特定的具体类型。以这种方式编写makedrapeoid()可以让它表达其公共接口的基本方面,它返回的值是一个形状,而不必生成由其公共接口的一部分组成的形状的特定类型。这个实现使用两个三角形和一个正方形,但是可以重写该函数以其他各种方式绘制梯形,而不必更改其返回类型。

这个例子强调了不透明返回类型与泛型类型相反的方式。makedrapeoid()中的代码可以返回它需要的任何类型,只要该类型符合Shape协议,就像调用泛型函数的代码一样。调用函数的代码需要以通用的方式编写,例如泛型函数的实现,这样它就可以处理makedrapeoid()返回的任何形状值。

还可以将不透明返回类型与泛型结合使用。以下代码中的函数都返回符合形状协议的某种类型的值。

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(:)将泛型形状操作返回的基础类型包装为不透明的返回类型,这将阻止这些类型可见。这两个函数都是泛型函数,因为它们依赖的类型是泛型的,函数的类型参数传递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
}

如果用正方形调用此函数,则返回正方形;否则,返回翻转形状。这违反了只返回一种类型的值的要求,并使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而变化:不管传递的是什么形状,都要重复(shape:count:)创建并返回该形状的数组。然而,返回值始终具有相同的基础类型[T],因此它遵循了具有不透明返回类型的函数必须只返回单个类型的值的要求。

3 不透明类型和协议的区别

返回不透明类型看起来与使用协议类型作为函数的返回类型非常相似,但这两种返回类型在是否保留类型标识方面有所不同。不透明类型引用一个特定的类型,尽管函数的调用方无法看到哪个类型;协议类型可以引用任何符合协议的类型。一般来说,协议类型为它们存储的值的底层类型提供了更大的灵活性,而不透明类型使您能够对这些基础类型做出更有力的保证。

例如,以下是flip(:)的一个版本,它使用协议类型作为其返回类型,而不是不透明的返回类型:

func protoFlip<T: Shape>(_ shape: T) -> Shape {
    return FlippedShape(shape: shape)
}

此版本的protoFlip(:)与flip(:)具有相同的主体,并且始终返回相同类型的值。与flip(:)不同,protoFlip(:)返回的值不需要总是具有相同的类型,只需符合Shape协议。换句话说,protoFlip(:)与调用方的API契约比flip(:)要宽松得多。它保留了返回多种类型值的灵活性:

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需求不允许将协议用作类型时发生的类型。

使用协议类型作为函数的返回类型,可以灵活地返回符合协议的任何类型。但是,这种灵活性的代价是某些操作无法对返回的值执行。这个例子展示了==操作符是如何不可用的——它依赖于特定的类型信息,而这些信息不是通过使用协议类型来保存的。

这种方法的另一个问题是形状变换不嵌套。协议的类型是一个三角形,它的结果是一个三角形。但是,协议类型的值不符合该协议;protoFlip(:)返回的值不符合Shape。这意味着应用多个转换的类似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"

十二的类型被推断为Int,这说明了类型推断与不透明类型一起工作的事实。在makeOpaqueContainer(item:)的实现中,不透明容器的底层类型是[T]。在本例中,T是Int,因此返回值是一个整数数组,而Item关联的类型被推断为Int。Container上的下标返回Item,这意味着12的类型也被推断为Int。

参考文档:Swift - Opaque Types