Swift编程二十四(不透明类型)

案例代码下载

不透明的类型

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

不透明类型解决的问题

例如,假设正在编写一个绘制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>这种将两个形状垂直连接在一起的结构的方法(如下面的代码所示)会产生类似于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()
    }
}
print(JoinedShape(top: smallTriangle, bottom: flippedTriangle).draw())

/* 
打印结果:
*
**
***
***
**
*
*/

公开有关允许创建形状的类型的详细信息不属于ASCII艺术模块公共接口泄漏的一部分,因为需要声明完整的返回类型。模块内部的代码可以以各种方式构建相同的形状,并且模块外部的其他代码使用该形状不应该考虑有关转换列表的实现细节。模块的使用者不关心类似JoinedShape与FlippedShape的包装类型,因为它们不应该是可见的。模块的公共接口包括连接和翻转形状等操作,这些操作返回另一个Shape值。

返回不透明类型

可以将opaque类型视为与泛型类型相反的类型。泛型类型是允许调用函数的代码选择该函数的参数和返回值类型,从函数实现中抽象出来的方式。例如,以下代码中的函数返回一个依赖于其调用者的类型:

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

调用代码max(::)选择x和y的值,并根据这些值的类型来确定具体的类型T。调用代码可以使用遵守Comparable协议的任何类型。函数内部的代码以泛型方式编写,因此它可以处理调用者提供的任何类型。实现max(::)仅使用所有Comparable类型公共功能。

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

struct Square: Shape {
    var size: Int
    func draw() -> String {
        let line = String(repeating: "*", count: size)
        let result = Array(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)
    return JoinedShape(top: top, bottom: JoinedShape(top: middle, bottom: bottom))
}
let trapezoid = makeTrapezoid()
print(trapezoid.draw())

/* 
打印结果:
*
**
**
**
**
*
*/

此示例中的makeTrapezoid()函数将其返回类型声明为some Shape;因此,该函数返回遵守Shape协议的某个给定类型的值,而不指定任何特定的具体类型。以这种方式编写makeTrapezoid()可以表达其公共接口的基本方面 - 它返回的值是一个形状 - 而不是使特定类型的形状作为其公共接口的一部分。此实现使用两个三角形和一个正方形,并且可以在不改变其返回类型的情况下以各种其他方式绘制梯形来重写该函数。

此示例突出显示了返回opaque类型像泛型类型相反的方式。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 {
    return 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 // 错误:返回类型不匹配
    }
    
    return FlippedShape(shape: shape) // 错误:返回类型不匹配
}

如果用Square调用此函数,则返回Square;否则,它返回一个FlippedShape。这违反了仅返回一种类型的值的要求使invalidFlip(:)代码无效。invalidFlip(:)的一种解决方法是将正方形的特殊情况移动到实现中FlippedShape,这使得此函数始终返回一个FlippedShape值:

struct FlippedShape<T: Shape>: Shape {
    var shape: Shape
    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],因此它遵循具有不透明返回类型的函数必须返回单个类型的值的要求。

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

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

例如,这里的flip(_:)版本返回协议类型的值而不是使用opaque返回类型:

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 someThing = protoFlip(smallTriangle)
protoFlippedTriangle == someThing // 错误

几个原因造成示例最后一行的出现错误。当前的问题是,Shape不包括 == 运算符作为其协议要求的一部分。如果尝试添加一个,将遇到的下一个问题是==操作员需要知道其左边和右边参数的类型。这种类型的运算符通常采用Self类型的参数,匹配任何遵守协议具体类型,但要求对Self添加协议不允许在将协议用作类型时发生的类型擦除。

使用协议类型作为函数的返回类型,可以灵活地返回符合协议的任何类型。但是,这种灵活性的代价是在返回值上无法进行某些操作。该示例显示了== 运算符为何不可用 - 它取决于使用协议类型未保留的特定类型信息。

这种方法的另一个问题是形状变换不能嵌套。翻转三角形的结果是Shape类型的值,protoFlip(:)函数采用遵守Shape协议的某种类型的参数。但是,协议类型的值不遵守该协议; protoFlip(:)返回的值不遵守Shape。这意味着类似protoFlip(protoFlip(smallTriange))的代码应用多个转换是无效的,因为翻转的形状不是protoFlip(_:)的有效参数。

相反,opaque类型保留了底层类型的标识。Swift可以推断关联类型,这可以在协议类型不能用作返回值的位置使用不透明的返回值。例如,这是Container协议的Generics版本:

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

使用opaque类型some Container作为返回类型表示所需的API契约 - 该函数返回一个容器,但拒绝指定容器的类型:

func makeProtocolContainer<T>(item: T) -> some Container {
    return [item]
}
let opaqueContainer = makeProtocolContainer(item: 12)
let twelve = opaqueContainer[0]
print(type(of: twelve))

/* 
打印结果:
Int
*/

twelve推断的类型是Int,它说明了类型推断适用于不透明类型的事实。在makeOpaqueContainer(item:)实现中,不透明容器的实际类型是[T]。在这种情况下,T是Int,所以返回值是一个整数数组,关联类型Item推断出是Int类型。Container的下标返回Item,这意味着twelve也推断出是Int类型。

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

推荐阅读更多精彩内容