第24章 类型系统补全:Any、Self、KeyPath 与类型擦除

第二十四章:类型系统补全:Any、Self、KeyPath 与类型擦除

前面的协议和泛型已经足够搭出大多数抽象。但 Swift 的类型工具箱里还有一些“小而关键”的工具:Any、元类型、Self、键路径、可调用类型和类型擦除。它们不是每天都用,却经常出现在框架和库的底层。

24.1 Any 与 AnyObject

Any 能装任意类型的值:

1
2
3
4
5
6
7
let values: [Any] = [1, "two", 3.0]
for value in values {
    print(type(of: value), value)
}
// prints: Int 1
// prints: String two
// prints: Double 3.0

AnyObject 只能装类实例:

1
2
final class Item {}
let objects: [AnyObject] = [Item()]

Any 很方便,也会擦除类型信息。取出后通常要 as? 转回来,因此它更适合异构集合、调试和桥接层,不适合当主要数据模型。

AnyHashable 则能装任意可哈希值——它把"这个键原本是什么类型"这件事也一起装了进去,所以不同类型的键可以共存于同一个字典:

1
2
3
let mixed: [AnyHashable: String] = [1: "一", "two": "二"]
print(mixed[AnyHashable(1)] ?? "无")
// prints: 一

这种"混合键字典"在解析外部 JSON、处理数据库返回值时才常见;自己设计的数据结构最好不要用到它。

24.2 元类型:类型本身也是值

T.self 得到类型本身,类型写作 T.Type:

1
2
3
let type: Int.Type = Int.self
print(type)
// prints: Int

动态获取某个值的类型:

1
2
3
let value = 42
print(type(of: value) == Int.self)
// prints: true

元类型可以用于注册表、工厂和动态创建:

1
2
3
4
5
6
7
protocol DefaultConstructible {
    static func makeDefault() -> Self
}

func makeDefault<T: DefaultConstructible>(_ type: T.Type) -> T {
    type.makeDefault()
}

这类写法要谨慎:一旦大量依赖元类型,类型信息就从编译期滑向运行期,错误也更容易在运行时才暴露。

24.3 Self:当前动态类型

协议里的 Self 表示“遵循该协议的具体类型”:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
protocol Duplicable {
    func duplicate() -> Self
}

struct Document: Duplicable {
    var title: String
    func duplicate() -> Document {
        Document(title: title)
    }
}

print(Document(title: "A").duplicate().title)
// prints: A

这里的协议故意叫 Duplicable,因为 Copyable 已经是标准库里用于值复制语义的协议名;教学示例不要和它抢名字。

在类型内部,Self 还可以作为返回值或工厂:

1
2
3
4
5
6
struct Point {
    var x = 0, y = 0
    static func origin() -> Self { Self() }
}
print(Point.origin().x)
// prints: 0

Self 比具体类型名更灵活,它让方法在子类中仍然描述“调用者的实际类型”。

24.4 Never:没有正常返回

Never 是没有任何值的类型,表示函数永远不返回:

1
2
3
func fail(_ message: String) -> Never {
    fatalError(message)
}

Never 也可以作为返回值出现在那些必然抛错、崩溃或陷入无限循环的函数里。泛型返回 Never 时,函数调用点被编译器视为“此处之后的代码不可达”。

24.5 键路径:把属性访问变成值

键路径用反斜杠加属性名表示:

1
2
3
4
5
6
7
8
9
struct User {
    var name: String
    var age: Int
}

let nameKey = \User.name
let user = User(name: "Mia", age: 28)
print(user[keyPath: nameKey])
// prints: Mia

键路径可以方便地用于映射:

1
2
3
4
5
let users = [User(name: "A", age: 20), User(name: "B", age: 30)]
print(users.map(\.name))
// prints: ["A", "B"]
print(users.map(\.age).reduce(0, +))
// prints: 50

可写键路径类型是 WritableKeyPath,可以修改值:

1
2
3
4
var mutable = user
mutable[keyPath: \User.age] = 29
print(mutable.age)
// prints: 29

键路径也是有类型的值,所以它们能进数组、能当参数,也能被擦除。PartialKeyPath 抹掉了"读写能力"这一层,让不同属性的键路径住进同一个数组;AnyKeyPath 连根类型也一起抹掉:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
struct User { var name: String; var age: Int }
final class Account { var balance = 0 }

let partials: [PartialKeyPath<User>] = [\User.name, \User.age]
print(partials.count, type(of: partials[0]))
// prints: 2 WritableKeyPath<User, String>

let anyPath: AnyKeyPath = \Account.balance
print(type(of: anyPath))
// prints: ReferenceWritableKeyPath<Account, Int>

let account = Account()
account[keyPath: \Account.balance] = 100
print(account.balance)
// prints: 100

注意 Account 是类,它的键路径类型是 ReferenceWritableKeyPath——“可写"这件事对值类型和引用类型的含义不同(第 17、18 章)。PartialKeyPath、AnyKeyPath 牺牲了类型信息,换来"把一堆不同的键路径装在一起"的能力,数据绑定、排序规则和响应式框架都靠它。

24.6 callAsFunction:让实例像函数一样调用

实现 callAsFunction 后,实例可以加括号调用:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
struct Multiplier {
    let factor: Int

    func callAsFunction(_ value: Int) -> Int {
        value * factor
    }
}

let triple = Multiplier(factor: 3)
print(triple(4))
// prints: 12

它可以有多个重载和不同参数。这个特性很适合“配置加行为”的轻量对象,但不要让调用语法隐藏太多真实工作。

24.7 动态成员查找:点语法接住任意名字

@dynamicMemberLookup 允许把未知属性名转成方法调用:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
@dynamicMemberLookup
struct DynamicBag {
    private var values: [String: String] = [:]

    subscript(dynamicMember key: String) -> String? {
        get { values[key] }
        set { values[key] = newValue }
    }
}

var bag = DynamicBag()
bag.name = "Mia"
print(bag.name ?? "nil")
// prints: Mia

动态成员查找常用于 JSON、脚本接口和动态配置。代价是拼写错误可能只在运行时暴露,所以静态模型更清晰的场景不要为了少写代码而使用它。

与它相邻的还有 @dynamicCallable,它把参数列表交给 dynamicallyCall 方法:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
@dynamicCallable
struct Sum {
    func dynamicallyCall(withArguments values: [Int]) -> Int {
        values.reduce(0, +)
    }
}

let sum = Sum()
print(sum(1, 2, 3))
// prints: 6

动态调用适合脚本桥接和高度动态的接口;它牺牲的同样是编译期参数检查。

24.8 类型擦除:把具体类型藏进一个稳定的盒子

存在类型 any P 能直接擦除类型,但有时你需要把多个泛型能力统一成一个具体包装类型。典型做法是用闭包保存操作:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
protocol Animal {
    associatedtype Food
    func eat(_ food: Food) -> String
}

struct AnyAnimal: Animal {
    private let eatClosure: (Any) -> String

    init<A: Animal>(_ animal: A) {
        eatClosure = { food in
            guard let food = food as? A.Food else { return "食物类型不对" }
            return animal.eat(food)
        }
    }

    func eat(_ food: Any) -> String {
        eatClosure(food)
    }
}

这个例子的类型安全被削弱了,但换来一个可以统一存放的 AnyAnimal。现代 Swift 在许多场景下可以优先使用 any P 或 some P;需要手写类型擦除时,再引入这样的包装类型。

24.9 Codable:把模型变成可传输的数据

Encodable 表示能编码,Decodable 表示能解码,Codable 是两者的组合。纯数据模型通常可以让编译器自动合成实现:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
import Foundation

struct UserDTO: Codable, Equatable {
    let id: Int
    var name: String
}

let user = UserDTO(id: 1, name: "Mia")
let data = try JSONEncoder().encode(user)
let decoded = try JSONDecoder().decode(UserDTO.self, from: data)
print(decoded == user)
// prints: true

当 JSON 字段名和 Swift 属性名不一致时,用 CodingKeys 指定映射:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
import Foundation

struct Article: Codable {
    let title: String
    let authorName: String

    enum CodingKeys: String, CodingKey {
        case title
        case authorName = "author_name"
    }
}

Codable 不负责网络、文件或数据库,它只负责“值 ↔ 编码格式”的转换。日期策略、键名策略和错误处理都应该在边界层明确配置。

24.10 常用标准协议

类型系统里有一些协议几乎每天都会遇到:

协议用途典型做法
Equatable判断值相等实现 ==
Hashable作为字典键、集合元素实现 hash(into:)
Comparable排序实现 <
Codable编码解码合成 Encodable/Decodable
Identifiable稳定身份提供 id
CustomStringConvertible自定义打印提供 description
CaseIterable枚举遍历提供 allCases
OptionSet位集合选项组合多个标志
RawRepresentable原始值与类型互转提供 rawValue 和 init?(rawValue:)
Sequence / Collection遍历与集合能力自定义容器时遵循

这些协议看起来零散,却构成了 Swift 数据建模的公共语言。看到一个类型声明遵循它们,调用者就能立刻知道它能做哪些事。

表里那些"实现某某"平时都由编译器合成(第 15B.16 节),但你需要知道手写版长什么样。Hashable 的手写版尤其值得看一眼,因为它有条不能违反的规矩:相等的值必须有相同的哈希值。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
struct Point: Hashable {
    var x: Int
    var y: Int

    func hash(into hasher: inout Hasher) {
        hasher.combine(x)          // 参与哈希的字段
        hasher.combine(y)
    }

    static func == (lhs: Point, rhs: Point) -> Bool {
        lhs.x == rhs.x && lhs.y == rhs.y
    }
}

print(Set([Point(x: 1, y: 2), Point(x: 1, y: 2)]).count)
// prints: 1
print(Point(x: 1, y: 2).hashValue == Point(x: 1, y: 2).hashValue)
// prints: true

一旦你写了自己版本的 hash(into:) 和 ==,编译器就不再替你合成——所以"参与比较的字段"和"参与哈希的字段"必须对得上,否则会出现"两个值相等,却在 Set 里同时存在"这种灵异事件。

24.11 反射:Mirror

Swift 不鼓励依赖运行时反射,但标准库给了一个只读的 Mirror,用来查看任意值的内部结构。调试日志、通用打印和工具代码里偶尔会用到它:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
struct Point {
    var x: Int
    var y: Int
}

let p = Point(x: 3, y: 4)
let mirror = Mirror(reflecting: p)

print(mirror.displayStyle == .struct)
// prints: true
for child in mirror.children {
    print(child.label ?? "?", child.value)
}
// prints: x 3
// prints: y 4

mirror.children 是"每个存储属性一个元素"的序列,每项有 label(属性名)和 value(值);label 是可选的,因为有些布局拿不到名字,所以要 ?? "?" 兜底。

两个限制必须记住:Mirror 只读,改不了属性;它也不承诺暴露类型的全部内部存储,优化后的布局可能看不到。所以它适合“展示和诊断”,不适合当序列化或依赖注入的底座——那类需求应该交给 Codable 或显式协议。

24.12 自定义字面量

42、"hi"、[1, 2] 这些字面量背后其实是一组协议:ExpressibleByIntegerLiteral、ExpressibleByStringLiteral、ExpressibleByArrayLiteral 等。让自己的类型遵循它们,调用点就能写得像内建类型一样自然:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
struct RGB: ExpressibleByArrayLiteral, CustomStringConvertible {
    var values: [Int]

    init(arrayLiteral elements: Int...) {
        values = elements
    }

    var description: String { "RGB\(values)" }
}

let color: RGB = [255, 128, 0]
print(color)
// prints: RGB[255, 128, 0]

(values 是数组,字符串插值会带上方括号,所以连起来是 RGB[255, 128, 0]。想让输出更像 RGB(255, 128, 0),自己拼一下 values.map(String.init).joined(separator: ", ") 即可。)

每个字面量协议只要求一个初始化器。好处是调用点更干净,同时类型安全一点没丢:let c: RGB = "abc" 会被编译器直接拒绝。

24.13 本章小结

工具用途
Any / AnyObject装任意值 / 任意类实例
元类型把类型本身当作值传递
Self表示当前实际类型
Never表示永远不会正常返回
KeyPath把属性访问变成可传递的值
callAsFunction让实例像函数一样调用
@dynamicMemberLookup支持动态点语法
类型擦除用统一包装隐藏具体泛型类型
Mirror只读地查看值的内部结构
字面量协议让自定义类型接受 42、"hi"、[1, 2] 这类写法

24.14 本章易错点速查

易错点正确理解
到处用 Any会丢掉编译期类型信息,尽量用泛型或协议
以为 type(of:) 返回静态类型它返回运行时动态类型
把 Self 当成简单别名它表达当前实际类型,和动态派发有关
滥用键路径清晰的直接属性访问通常更易读
依赖动态成员查找拼写拼写错误可能只在运行时暴露
手写类型擦除过早先考虑 some/any 是否够用
用 Mirror 做序列化它只读且不一定覆盖全部存储,序列化请用 Codable

24.15 下章预告

当项目变大,类型和模块的边界就需要规则。下一章讲访问控制、模块、条件编译和可用性检查:谁能看见什么、什么代码只在某些平台存在、怎样在不破坏兼容性的情况下标记旧接口。

最后修改 September 19, 2026: 更新 (3489033b1)