27 Opaque Types 不透明类型

这是 Swfit 5.0.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<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 art模块公共接口的类型,因为需要声明完整的返回类型。模块内部的代码可以以多种方式构建相同的形状,使用该形状的模块外部的其他代码不应该考虑关于转换列表的实现细节。像JoinedShape和FlippedShape这样的包装器类型对模块的用户并不重要,它们不应该是可见的。模块的公共接口由连接和翻转形状等操作组成,这些操作返回另一个形状值。

返回不透明类型

您可以将不透明类型看作泛型类型的反面。泛型类型允许调用函数的代码为该函数的参数选择类型,并以一种与函数实现分离的方式返回值。例如,下面代码中的函数返回依赖于调用者的类型:

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

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

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

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()函数将其返回类型声明为某种形状;因此,函数返回一个符合Shape协议的给定类型的值,而不指定任何特定的具体类型。以这种方式编写makeTrapezoid()可以让它表达其公共接口的基本方面(它返回的值是一个形状),而不需要生成形状由其公共接口的一部分构成的特定类型。这个实现使用了两个三角形和一个正方形,但是可以重写这个函数,以多种其他方式绘制梯形,而不改变它的返回类型。

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

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

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

本例中opaquejoinedtriangle的值与本章前面不透明类型解决的问题一节中泛型示例中的joinedtriangle的值相同。但是,与该示例中的值不同,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
}

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

不透明类型和协议类型之间的差异

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

例如,这是flip(_:)的一个版本,它返回协议类型的值,而不是使用不透明的返回类型:

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

这个版本的protoFlip(:)具有与flip(:)相同的主体,并且总是返回相同类型的值。与flip(:)不同,protoFlip(:)返回的值不需要总是具有相同的类型——它只需要符合Shape协议。换句话说,与flip(:)相比,protoFlip(:)与调用者之间的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  // Error

示例的最后一行出现错误有几个原因。当前的问题是,该形状没有将==操作符作为其协议要求的一部分。如果您尝试添加一个,您将遇到的下一个问题是==操作符需要知道其左参数和右参数的类型。这种类型的操作符通常接受Self类型的参数,匹配采用协议的任何具体类型,但是在协议中添加Self需求并不允许将协议用作类型时发生的类型擦除。

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

这种方法的另一个问题是形状转换不嵌套。翻转三角形的结果是Shape类型的值,protoFlip(:)函数接受某种类型的参数,该参数符合Shape协议。但是,协议类型的值不符合该协议;protoFlip(:)返回的值不符合形状。这意味着像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"

2的类型被推断为Int,这说明了类型推断与不透明类型一起工作的事实。在makeOpaqueContainer(item:)的实现中,不透明容器的底层类型是[T]。在本例中,T是Int,所以返回值是一个整数数组,并且推断出与项目相关的类型为Int.容器上的下标返回项目,这意味着12的类型也推断为Int。

<<返回目录

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,293评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,604评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,958评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,729评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,719评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,630评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,000评论 3 397
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,665评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,909评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,646评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,726评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,400评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,986评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,959评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,197评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,996评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,481评论 2 342

推荐阅读更多精彩内容

  • Swift1> Swift和OC的区别1.1> Swift没有地址/指针的概念1.2> 泛型1.3> 类型严谨 对...
    cosWriter阅读 11,084评论 1 32
  • 案例代码下载 不透明的类型 函数或方法返回的不透明类型会隐藏其返回值的类型信息。不是提供具体类型作为函数的返回类型...
    酒茶白开水阅读 1,321评论 0 1
  • 这是16年5月份编辑的一份比较杂乱适合自己观看的学习记录文档,今天18年5月份再次想写文章,发现简书还为我保存起的...
    Jenaral阅读 2,729评论 2 9
  • 时论高雅而莫谈国事,时论低俗而莫谈友情。利酒而歌者幸也,利酒而醉者愚也,利酒而醒者智也,利酒而话心者情也。
    A锦瑟华年阅读 183评论 0 2
  • 今天没有睡到自然醒昨晚孩爷爷在家住下的今天要做坐车回老家要早起来做饭,闺女想吃米饭那有早上吃米饭的这不还是焖的米饭...
    王怡婷妈妈阅读 148评论 0 2