类型嵌套
本教程共 93 篇 · 第 74 篇 · 更新于 2026-08-08 · 约 8 分钟阅读
本节目标:学会把一个枚举、结构体或协议定义在另一个类型内部,把”只服务于某类型”的小类型收进同一作用域,让代码更整洁、命名更短。
你写代码时常遇到这种情况:有个枚举,纯粹是为了配合某个类或结构体而存在。比如”扑克牌的花色”只服务于”扑克牌”这一种类型,“订单状态”只服务于”订单”。把它们单独丢在顶层,名字容易撞、语义也散。Swift 的类型嵌套(nested types)让你把这些”配角类型”直接放进”主角类型”的大括号里,像抽屉一样收好。
1-1 什么是类型嵌套
类型嵌套,就是在一个类型的内部再定义另一个类型。被嵌套的可以是枚举、结构体,甚至协议;嵌套的层级没有硬限制,可以一层套一层。外层类型叫”容器类型”,内层叫”嵌套类型”。
它的价值主要两点。第一是作用域隔离:嵌套类型只在容器类型内部(及其遵循者等相关上下文)有意义,放进内部就明确宣示了”我是为它服务的”。第二是命名更短:因为使用时会被容器名自动限定,嵌套类型本身不用前缀一大串,名字可以起得很简洁。
1-2 一个完整的例子
下面这个 BlackjackCard 结构体模拟一张二十一点扑克牌。它内部嵌套了两个枚举 Suit(花色)和 Rank(点数),而 Rank 里面又嵌了个结构体 Values:
struct BlackjackCard {
// 嵌套的花色枚举
enum Suit: Character {
case spades = "♠", hearts = "♡", diamonds = "♢", clubs = "♣"
}
// 嵌套的点数枚举
enum Rank: Int {
case two = 2, three, four, five, six, seven, eight, nine, ten
case jack, queen, king, ace
struct Values {
let first: Int, second: Int?
}
var values: Values {
switch self {
case .ace:
return Values(first: 1, second: 11)
case .jack, .queen, .king:
return Values(first: 10, second: nil)
default:
return Values(first: self.rawValue, second: nil)
}
}
}
let rank: Rank, suit: Suit
var description: String {
var output = "suit is \(suit.rawValue),"
output += " value is \(rank.values.first)"
if let second = rank.values.second {
output += " or \(second)"
}
return output
}
}
这里 Suit 描述四种花色,用 Character 原始值存符号;Rank 描述十三种点数,用 Int 原始值存面值。注意 Rank 自己又嵌套了 Values 结构体——因为扑克牌里”大多只有一种值,但 A 有两种(1 或 11)“,用 Values 把”第一值 + 可选第二值”封装起来最贴切。
BlackjackCard 自身只有 rank 和 suit 两个属性,再加一个 description 计算属性拼出描述。因为结构体没写自定义初始化器,它自动获得成员逐一初始化器,可以直接这么用:
let theAceOfSpades = BlackjackCard(rank: .ace, suit: .spades)
print(theAceOfSpades.description)
// suit is ♠, value is 1 or 11
即便 Rank 和 Suit 是嵌套在里面的,编译器能从上下文推断出它们的类型,所以初始化时直接用 .ace、.spades 这种简短写法即可。
1-3 在外部引用嵌套类型
要在定义上下文之外使用嵌套类型,要在它名字前面加上容器类型名,用点连接:
let heartsSymbol = BlackjackCard.Suit.hearts.rawValue
// heartsSymbol 是 "♡"
BlackjackCard.Suit.hearts 这种”容器名.嵌套名”的写法,等于给嵌套类型加了个命名空间前缀。好处是:你在外面可以放心的把嵌套类型起短名(Suit、Rank、Values),因为它们天然被 BlackjackCard 限定了,不会和别处的同名类型冲突。这比在顶层硬起 BlackjackSuit、BlackjackRank 清爽得多。
用容器名限定的好处不只是名字短。它还把嵌套类型”归属谁”直接写进了语法:任何读到 BlackjackCard.Suit 的人,不用看注释就知道”花色是扑克牌的概念”。这种”命名即文档”的特性,是类型嵌套比顶层类型更耐读的原因。当项目里出现多个 Status、Kind 这类通用小名时,嵌套能天然避免重名冲突,省去你硬憋一长串前缀的麻烦。
1-4 在扩展里嵌套,以及协议也能嵌套
嵌套不只在类型原始定义里写。你也能在扩展里给已有类型加嵌套类型,效果一样。标准库甚至给 Int 在扩展里加过嵌套枚举来表达”正负零”。
协议同样可以被嵌套。第 67 章的委托例子里,DiceGame 类就在自己内部嵌套了一个 Delegate 协议:
class DiceGame {
let sides: Int
weak var delegate: Delegate?
init(sides: Int) { self.sides = sides }
protocol Delegate: AnyObject {
func gameDidStart(_ game: DiceGame)
func gameDidEnd(_ game: DiceGame)
}
}
因为 Delegate 只在”掷骰子游戏”这个语境里用,把它放进 DiceGame 内部,语义和位置都恰到好处。嵌套协议和嵌套枚举、结构体一样,用”容器名.协议名”引用。
Note协议可以嵌套的前提是:外层声明不是泛型类型。给泛型类型里套协议会有约束上的麻烦,日常写嵌套类型多用枚举和结构体就够。
1-5 访问控制与嵌套类型
嵌套类型会受容器类型的访问级别影响。一般规则是:嵌套类型的访问级别默认和容器类型相同。但有个特例——如果容器是 public,嵌套类型默认会是 internal,不会自动跟着变成 public。
也就是说,想让一个 public 类型内部的嵌套类型也对外部公开,必须显式把它标成 public:
public struct Outer {
public enum Inner {
case a, b
}
}
不写那个 public,Inner 对外就看不见,只能在模块内部用。这条规则和”public 类型的成员默认 internal”一脉相承,都是为了防止你无意间把内部细节暴露成公共 API。
1-6 什么时候该用类型嵌套
类型嵌套不是越多越好,它适合”小类型只为某一类型服务”的场景:
- 一个枚举的 case 只在该类型内部有意义,比如状态码、模式、分类。
- 一个辅助结构体只在容器内部用来打包数据,比如上面的
Values。 - 一个协议只被某个类型当作委托或数据源使用,比如
DiceGame.Delegate。
如果那个类型将来会被多处独立使用、或语义上本就独立,那就该放到顶层,不要硬塞进嵌套。滥用嵌套反而会让层级变深、可读性下降。一句话:嵌套用来”收抽屉”,不是用来”叠箱子”。
1-7 小结
类型嵌套把一个枚举、结构体或协议定义在另一个类型内部,把”只服务于某类型”的配角收进同一作用域。它能隔离作用域、缩短命名,用法就是直接写在容器类型的大括号里,外部引用时加”容器名.”前缀。协议也能嵌套(外层不能是泛型),扩展里同样能加嵌套类型。注意 public 容器的嵌套类型默认是 internal,要对外公开需显式标 public。嵌套适合”小类型专为某一类型服务”的场景,语言本身到此,模块八面向对象编程也告一段落。