理解Swift中的 some 不透明返回类型

ReyZhang
移动开发领域新星创作者
2023-05-31 10:07:57

一、概述

1.1 不透明类型 Opaque Return Types

不透明类型是指我们被告知对象的功能而不知道对象具体是什么类型

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

1.2 协议类型作为返回值

协议类型作为返回值时,返回对象仅能调用协议中定义的属性方法。与返回值类型是否是不透明返回值类型没有关系

protocol MyProtocol {
    ///属性
    var  nameOfProtocol:String {get set}
    ///方法
    func doSomeTingOfProtocol () -> Void
}

struct MyStruct:MyProtocol {
    ///协议相关
    var nameOfProtocol: String = "协议属性"
    func doSomeTingOfProtocol() {
        print("学习")
    }
    
    ///自定义相关
    var nameOfSelf:String = "自定义属性"
    func doSomeTingOfSelf() {
        print("打游戏")
    }
}

func testMethods() -> some MyProtocol { ///返回遵守MyProtocol协议的类型
    return MyStruct()
}
let object = testMethods()

调用属性:

在这里插入图片描述


调用方法:

在这里插入图片描述

1.3 语法

不透明类型可以被认为是“实现某个协议的具体类型”。 它的语法: some Protocol

举个栗子:

func makeA() -> some Equatable { ///返回类型是 some + Equatable协议 的不透明类型
    "A"  ///单表达式函数的隐式返回,此处隐藏了return
}

尽管具体类型永远不会暴露给函数的调用者,但返回值仍保持强类型。 这样做的原因是,编译器知道具体的类型:

let a = makeA()
let anotherA = makeA()

print(a == anotherA) // ✅ The compiler knows that both values are strings

下面让我们测试遵循相同协议不同类型的不透明类型是否相等:

func makeOne() -> some Equatable { ///返回类型是 some + Equatable协议 的不透明类型
    1        ///单表达式函数的隐式返回,此处隐藏了return
}
let one = makeOne()
print(a == one) // ❌ Compilation error: `a` and `one` are of different types, although both conform to `Equatable`

编译器会认为两个不透明类型不相等:

var x: Int = 0
x = makeOne() // ❌ Compilation error: Cannot assign value of type 'some Equatable' to type 'Int'

该函数每次必须返回相同的不透明类型

func makeOneOrA(_ isOne: Bool) -> some Equatable {
    isOne ? 1 : "A" // ❌ Compilation error: Cannot convert return expression of type 'Int' to return type 'some Equatable'
} 

这能让调用者依靠不透明类型在运行时保持类型一致性

从编译器的角度来看,不透明类型与其基础类型等效。 编译器将其抽象化,仅将类型公开为符合给定约束集的某种形式。

二、不透明类型和泛型

不透明类型是一种特殊的泛型

泛型是用于类型级别抽象的Swift语言特性。 它允许将一种类型与满足给定约束集的任何其他类型以相同的方式使用。

2.1 泛型受调用者约束

func foo<T: Equatable>() -> T { ... }

let x: Int = foo()    // T == Int, chosen by caller
let y: String = foo()  // T == String, chosen by caller

2.2 不透明类型受被调用者约束

func bar() -> some Equatable { ... }
let z = bar() // z is abstracted to Equatable. Concrete type is chosen by bar() implementation

不透明类型有时称为“反向泛型”

三、不透明类型和协议

不透明类型看起来像协议,行为也很像协议。 因此,重要的是要证明它们之间的差异

3.1 不能从函数返回带有Selfassociatedtype要求的协议。 相反,不透明类型却可以:

// Equatable protocol declaration from the Swift standard library
public protocol Equatable {
    static func == (lhs: Self, rhs: Self) -> Bool
}

func makeTwo() -> Equatable { 2 } // ❌ Protocol 'Equatable' can only be used as a generic constraint because it has Self or associated type requirements
func makeThree() -> some Equatable { 3 } // ✅

SwiftUI中你会发现大量使用someassociatedtype修饰的语法。所以了解一下还是很有必要的。

3.2 一个函数可以返回不同的协议类型。 相反,它每次必须返回相同的不透明类型

///自定义协议
protocol P {}
///拓展基础类型
extension Int: P {}
extension String: P {}
///区别
func makeIntOrString(_ isInt: Bool) -> P { isInt ? 1 : "1" } // ✅
func makeIntOrStringOpaque(_ isInt: Bool) -> some P { isInt ? 1 : "1" } // ❌ Compilation error

还是体现了不透明类型的特点: 类型一致性

四、不透明类型解决的问题

先来看一段代码,它展现了原来 protocol 能力上的缺陷:

///协议:Shape
protocol Shape {}

/// Rectangle
struct Rectangle: Shape {}
/// Union
struct Union<A: Shape, B: Shape>: Shape {
    var a: Shape
    var b: Shape
}

/// Transformed
struct Transformed<S: Shape>: Shape {
    var shape: S
}

///协议:GameObject
protocol GameObject {
    associatedtype ShapeType: Shape
    var shape: ShapeType { get }
}
/// EightPointedStar
struct EightPointedStar: GameObject {
    var shape: Union<Rectangle, Transformed<Rectangle>> {
        return Union(a:Rectangle(), b:Transformed(shape: Rectangle()))
    }
}

缺陷有两方面:

  1. 上述代码是可以编译通过的,但是 EightPointedStar 的 shape 返回类型又臭又长,被暴露了出去;如果换成 Shape 则编译不通过,原因是 associatedtype ShapeType 要求必须指定具体的类型,而 Shape 不实现 Shape 本身。
  2. 假如 Shape 协议中含有 Self 或者 associatedtype,无法作为函数的返回参数。这是 Swift 泛型系统长久以来的一个问题。

不透明型解决了上述问题,它为 protocol 作为返回类型提供以下能力:

  1. 语法上隐藏具体类型,所以叫做不透明结果类型
  2. 强类型:类型参数不丢失
  3. 允许带有 Self 或者 associatedtypeprotocol作为返回类型

在 Swift 5.1 中,将返回类型改成 some + protocol 的形式:

struct EightPointedStar: GameObject {
    var shape: some Shape {
        return Union(a:Rectangle(), b:Transformed(shape: Rectangle()))
    }
}

这类的泛型特性也被称作“反向泛型”,因为具体的类型参数是由“实现部分”指定并隐藏起来的,而一般的泛型是由“调用者”所指定的。

上面这个例子中:语法上隐藏具体类型很明显,再举一个例子说明其它 2 个特性:

func foo<T: Equatable>(x: T, y: T) -> some Equatable {
    let condition = x == y
    return condition ? 42 : 11
}

let x = foo(x: "apples", y: "bananas")
let y = foo(x: "apples", y: "oranges")

print(x == y) // 这里可以被调用是因为泛型系统保留了强类型

这个例子显示了不透明结果类型的三个特性:

既对外隐藏了具体的 Equatable 类型;又保留了强类型(使得 x == y)可以比较;还支持了 Equatable 这个带 Self 的泛型约束。

不透明结果类型对于函数实现有一个增强的要求:

函数实现必须返回同一个具体类型,以上述代码为例:不能返回 Equatable 或者是 不同类型的 Equatable 的实现。

这里还有一个小问题:

既然 x 和 y 可以直接比较,那么它们可否直接赋值给 var i: Int 呢?答案是对于静态类型系统是不可以的,它保留了 some Equatable 的具体类型隐藏功能,但是如果使用动态类型判断 as? Int,则可以转换成 Int。

五、何时使用some关键字

当设计通用代码时,some关键字特别有用,例如库或特定领域的语言。 底层类型永远不会暴露给使用者,尽管他们可以利用其静态特性。 由于它们是在不透明类型上解析的,因此可以利用具有关联类型(associated types)和Self要求的协议。

不透明类型可以把库的使用者和库的内部实现分隔开。

六、总结

以下是有关Swift不透明类型和some关键字的总结:

  1. 不透明类型可以被认为是具有私有基础类型的协议。
  2. 不透明类型是由函数实现者定义的,而不是由调用者定义的。
  3. 一个函数每次必须返回相同的不透明类型。
  4. 允许带有Self或者 associatedtypeprotocol 作为返回类型
...全文
成就一亿技术人!
拼手气红包 5.00元
600 回复 打赏 收藏 转发到动态 举报
写回复
用AI写文章
回复
切换为时间正序
请发表友善的回复…
发表回复

421

社区成员

发帖
与我相关
我的任务
社区描述
专注移动ios平台的软件开发,多年的一线研发经验,实战经验丰富,只为你呈现有价值的信息。
iosflutterandroid 技术论坛(原bbs) 山东省·青岛市
社区管理员
  • ReyZhang
加入社区
  • 近7日
  • 近30日
  • 至今
社区公告
暂无公告

试试用AI创作助手写篇文章吧