Swift之不透明类型

不透明类型

具有不透明返回类型的函数或者方法会隐藏返回值的类型信息,函数不在提供具体的类型组委返回值,而是根据它所支持的协议来描述。在处理模块和代码调用之间的关系时,隐藏类型信息非常有用,因为他返回的底层数据类型仍然保持私有。而且不同于返回协议类型,不透明类型能保持类型一致性---编译器能获取类型信息,而模块使用者却获取不到。

不透明类型解决的问题

鸡舍你正在写一个模块,用来绘制ASCII符号构成的几何图形。它的基本特征是有一个draw()方法,会返回一个代表几何图形的字符串,你可以使用包含这个方法的shape协议来描述。

protocol Shape {
    func draw() -> String
}
struct Triangle: Shape {
    var size: Int
    func draw() -> String {
        var result = [String]()
        for lenth in 1...size {
            result.append(String(repeating: "*", count: lenth))
        }
        return result.joined(separator: "\n")
    }
}

利用泛型来实现竖直翻转操作和拼接操作,代码如下:

struct FlippedShape<T: Shape>: Shape {
    var shape: T
    func draw() -> String {
        let lines = shape.draw().split(separator: "\n")
        return lines.reversed().joined(separator: "\n")
    }
}
struct JoinedShape<U: Shape, T: Shape>: Shape {
    var top: U
    var bottom: T
    func draw() -> String {
        return top.draw() + "\n" + bottom.draw()
    }
}
let flipTriangle = FlippedShape(shape: smallTriangle)
let joinedShape = JoinedShape(top: smallTriangle, bottom: flipTriangle)

限制在于:

  • 翻转操作的结果FlippedShape<Triangle>暴露了用来构造翻转图形的泛型类型Triangle
  • 由于需要声明完整的返回值类型,暴露图形创建细节可能导致原本不应该成为公共接口的类型暴露。模块内部可以采用多种方法构造同样的图形,而外部使用时,应该与内部各种变换顺序的实现逻辑无关。包装类型FlippedSahpeJoinedSahpe和模块的使用者无关,它们不应该对使用者可见。模块的公共接口应该由翻转和拼接等基础操作组成,这些操作也应该返回独立的Shape类型的值。

返回不透明类型

可以认为不透明类型和泛型相反。泛型允许调用方法时,为这个函数的形参和返回值制定一个与实现无关的类型。如下,参数xy的类型决定了函数中T的具体类型,调用的代码可以使用任何遵循Comparable协议的类型。函数的内部也以通用方式实现,所以可以应对调用者传入的各种类型。max(_:_)的实现仅使用了所有Comparable类型的共有特性

func max<T>(_ x: T, _ y: T) -> T where T: 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)
    return JoinedShape(top: top, bottom: JoinedShape(top: middle, bottom: bottom))
}

这个例子中,makeTrapezoid()函数返回值类型定义为some Shape;因此该函数返回遵循Shape协议的给定类型,而不需要指定任何具体类型。这样写makeTrapezoid()函数,也可以表明其公共接口的基本性质---返回一个几何图形---而不是具体公共接口生成的具体类型。
这个例子凸显了不透明返回类型和泛型的相反之处。makeTrapezoid()中代码可以返回任何它需要的类型,只要这个类型遵循Shape协议,就像低啊用泛型函数时可以使用任何需要的类型一样。这个函数的调用代码要采用通用的方式,就像泛型函数的实现代码一样,这样才能让makeTrapezoid()返回任何Shape类型的值都能正常使用。

可以把返回不透明类型和泛型结合起来。

func flip<T: Shape>(_ shape: T) -> some Shape {
    return FlippedShape(shape: shape)
}
func join<U: Shape, T: Shape>(_ top: U, _ bottom: T) -> some Shape {
    return JoinedShape(top: top, bottom: bottom)
}
let opaqueJoinedTriangle = join(smallTriangle, flip(smallTriangle))

这个例子中opaqueJoinedTriangle与前文的joinedTriangles完全一样。不同之处在于,flip(_:)join(_:_:)对泛型参数的操作结果包装成了不透明类型,保证了结果中泛型参数类型不可见。两个函数都是泛型函数,依赖于泛型参数,泛型参数又将FlippedShapeJoinedShape所需类型信息传递给他们。

如果函数中多个地方使用了泛型参数,那么所有的返回值必须是同一类型

func invalidFlip<T: Shape>(_ shape: T) -> some Shape{
    if shape is Squre {
        //Function declares an opaque return type, but the return statements in its body do not have matching underlying types
        return shape
    }
    //Function declares an opaque return type, but the return statements in its body do not have matching underlying types
    return FlippedShape(shape: shape)
}

解决办法是把对Sqaure的特殊操作转移到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)
}

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

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

看一下协议版本的protocolFlip函数,它对API调用者约束更加松散,保留了返回多种不同类型的灵活性。

//返回不同类型, Squre 和 FlippedShape
func protocolFlip<T : Shape>(_ shape: T) -> Shape {
    if shape is Square {
        return shape
    }
    return FlippedShape(shape: shape)
}

修改后的代码根据代表形状的参数的不同,可以可能返回Square实例和FlippedShape实例,索引同样的函数可能返回两个完全不同的类型。protocolFlip(_:)返回类型的不确定性,意味着许多依赖于类型的操作无法执行。比如不能使用==运算符。

let protocolFlippedTriangle = protocolFlip(smallTriangle)
let someThing = protocolFlip(smallTriangle)
protocolFlippedTriangle == someThing//ERROR

错误的原因在于,Shhape并未提供==运算符的实现,即使你添加了一个实现,也无法使用,因为==需要知道左右两侧参数类型。这类运算符通常采用self类型作为参数,用来匹配符合协议的具体类型,由于协议作为类型使用时会发生类型擦除,所以并不能给协议加上对self的实现要求。另外嘉定让协议遵循Equatable协议,那么protocolFlip(_:)函数将会报错,原因是:Protocol 'Shape' can only be used as a generic constraint because it has Self or associated type requirements

这种方法的另外一个问题在于,protocolFlip(_:)函数无法嵌套使用,诸如protocolFlip(protocolFlip(smallTriangle)),因为Value of protocol type 'Shape' cannot conform to 'Shape'; only struct/enum/class types can conform to protocols。相比之下不透明类型则保留了底层类型的唯一性Swift能够推断出关联类型。这个特性使得作为函数返回值,不透明类型比协议类型有更多的使用场景。

protocol Container {
    associatedtype Item
    var count: Int { get }
    subscript(i:Int) -> Item { get }
}
extension Array: Container{}

以下两种都是错误的使用方法:

//Protocol 'Container' can only be used as a generic constraint because it has Self or associated type requirements
func makeProtocolContainer<T>(item: T) -> Container {
    return [item]
}

// Cannot convert return expression of type '[T]' to return type 'C'
func makeProtocolContainer<T, C: Container>(item: T) -> C {
    return [item]
}

使用不透明类型some Container作为返回类型,就能满足API的要求---返回一个结合类型,但不指定具体类型。

func makeOpaqueContainer<T>(item: T) -> some Container {
    return [item]
}

let opaqueContanierInt = makeOpaqueContainer(item: 23)
print(type(of: opaqueContanierInt) )//Array<Int>
print(type(of: opaqueContanierInt[0]) )//Int
let opaqueContanierSquare = makeOpaqueContainer(item: Square(size: 4))
print(type(of: opaqueContanierSquare) )//Array<Square>

上述例子表明类型推断适用于不透明类型makeOpaqueContainer<T>(item: )函数中,底层类型是不透明集合[T],此时T就是Int类型,所以返回值就是整数数组,关联类型Item也会被推断为TContainer协议中下标方法返回Item,意味着opaqueContanierInt[0]也会被推断为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