Swift 中的类型擦除(上)—— 为什么 & 怎么做

Swift 中的类型擦除(上)—— 为什么 & 怎么做

探索类型擦除的原理和实现,常见的类型橡皮擦有:AnyView、AnyShape、AnyCollection…

1
2
3
4
protocol Tool {
associatedtype S: StringProtocol
func name() -> S
}

通过这个协议(或者叫接口)可以创建一个 Tool

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
struct Pen: Tool {
func name() -> some StringProtocol {
"Pen"
}
}

struct Pencil: Tool {
func name() -> some StringProtocol {
"Pencil"
}
}

struct Ruler: Tool {
func name() -> some StringProtocol {
"Ruler"
}
}

接下来,我要创建一个 ToolController 来管理我的“工具们”,

1
2
3
class ToolController {

}

我们先来获取所有工具,但是遇到一个问题,

这里虽然只有三个工具,可以一个个写,

但是这只是个例子,在实际应用中这里的数量是未知的,

应该如何获取所有的工具呢?大致思路是这样的。

1
let tools: [Tool] = [Pen(), Pencil(), Ruler()]

但是,Tool 中有 associatedtype,因此 Tool 不能当作类型来使用

好消息是,在 Swift 5.7 或更新版本中,可以使用 any Tool 来作为类型,

但是之后的所有结果都带有 any,可能不会是你预期的结果。

比如 any View 不符合 View 协议…

除了这样的数组创建和使用上可能会受阻,在多分支返回是也会出问题。

1
2
3
4
5
6
7
8
9
10
11
12
class ToolController {
var currentTool = "pen"

// ❌ Function declares an opaque return type 'some Tool', but the return statements in its body do not have matching underlying types.
func getCurrentTool() -> some Tool {
switch currentTool {
case "pen": return Pen()
case "ruler": return Ruler()
default: return Pencil()
}
}
}

some 关键字表示不透明的类型(opaque type),

我们只需要知道他是一个符合 Tool 协议的东西,

但是具体是什么不知道,也不关心是什么,交给编译器去推断。

因为在三个 case 中返回的结果类型不一致,编译时无法推断 Tool 的类型,

而三个分支中使用的 Tool 是完全不同的东西,不能直接替换。

如果还有更多分支的话情况会更加复杂,

因此,若想统一类型,不妨创建一个类型橡皮擦来擦除原本的类型,

这时候每一个分支返回的类型都是“橡皮擦”的类型,也就没问题了。

类型擦除的原理以及实现

  • 创建橡皮擦(以下称之为 Eraser),Eraser 要符合你的 Protocol

橡皮擦的名称一般是 Any + 协议名

1
2
3
4
5
struct AnyTool: Tool {
func name() -> some StringProtocol {

}
}
  • 初始化中传入一个要被擦除类型的 Tool (下面称之为Type)
1
2
3
init<T: Tool>(erasing tool: T) {

}
  • 创建中间变量来传递 Type 中的必要的方法和属性
1
2
3
4
5
internal let _name: S

init<T: Tool>(erasing tool: T) {
_name = (tool.name() as? S) ?? "IDK."
}
  • 最后,用 name 来填补 Eraser 中需要的方法
1
2
3
4
5
6
7
8
9
10
11
12
struct AnyTool: Tool {
typealias S = String // This must be a type that conforms to StringProtocol.
internal let _name: S

init<T: Tool>(erasing tool: T) {
_name = (tool.name() as? S) ?? "IDK."
}

func name() -> S {
_name
}
}

在这里,我们将类型从 some StringProtocol 变成了 String

如果是 View,一般会把他替换成 AnyView

这里如果不能将 tool.name() 转换成 String 的话就返回 IDK.

这样一来,我们就可以将 PenPencilRuler 都变成 AnyTool,解决了以上两个问题。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class ToolController {
var allTools: [AnyTool] = []
var currentTool = "pen"

func getAllTools() {
let tools = [AnyTool(erasing: Pen()), AnyTool(erasing: Pencil()), AnyTool(erasing: Ruler())]
allTools = tools
}

func getCurrentTool() -> some Tool {
switch currentTool {
case "pen": return AnyTool(erasing: Pen())
case "ruler": return AnyTool(erasing: Ruler())
default: return AnyTool(erasing: Pencil())
}
}
}

实现原理参考 Approaches to Type Erasure in Swift

Swift 中的类型擦除(上)—— 为什么 & 怎么做

https://liyanan2004.github.io/type-erasure-in-swift/

作者

LiYanan

发布于

2023-01-13

更新于

2023-01-16

许可协议

评论