首页 / Swift 编程语言教程 / 类型嵌套

Swift 编程语言教程

类型嵌套

本教程共 93 篇 · 第 74 篇 · 更新于 2026-08-08 · 约 8 分钟阅读

SwiftSwift 编程语言教程nested types类型嵌套嵌套枚举嵌套结构体作用域命名空间

本节目标:学会把一个枚举、结构体或协议定义在另一个类型内部,把”只服务于某类型”的小类型收进同一作用域,让代码更整洁、命名更短。

你写代码时常遇到这种情况:有个枚举,纯粹是为了配合某个类或结构体而存在。比如”扑克牌的花色”只服务于”扑克牌”这一种类型,“订单状态”只服务于”订单”。把它们单独丢在顶层,名字容易撞、语义也散。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 自身只有 ranksuit 两个属性,再加一个 description 计算属性拼出描述。因为结构体没写自定义初始化器,它自动获得成员逐一初始化器,可以直接这么用:

let theAceOfSpades = BlackjackCard(rank: .ace, suit: .spades)
print(theAceOfSpades.description)
// suit is ♠, value is 1 or 11

即便 RankSuit 是嵌套在里面的,编译器能从上下文推断出它们的类型,所以初始化时直接用 .ace.spades 这种简短写法即可。

1-3 在外部引用嵌套类型

要在定义上下文之外使用嵌套类型,要在它名字前面加上容器类型名,用点连接:

let heartsSymbol = BlackjackCard.Suit.hearts.rawValue
// heartsSymbol 是 "♡"

BlackjackCard.Suit.hearts 这种”容器名.嵌套名”的写法,等于给嵌套类型加了个命名空间前缀。好处是:你在外面可以放心的把嵌套类型起短名(SuitRankValues),因为它们天然被 BlackjackCard 限定了,不会和别处的同名类型冲突。这比在顶层硬起 BlackjackSuitBlackjackRank 清爽得多。

用容器名限定的好处不只是名字短。它还把嵌套类型”归属谁”直接写进了语法:任何读到 BlackjackCard.Suit 的人,不用看注释就知道”花色是扑克牌的概念”。这种”命名即文档”的特性,是类型嵌套比顶层类型更耐读的原因。当项目里出现多个 StatusKind 这类通用小名时,嵌套能天然避免重名冲突,省去你硬憋一长串前缀的麻烦。

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
    }
}

不写那个 publicInner 对外就看不见,只能在模块内部用。这条规则和”public 类型的成员默认 internal”一脉相承,都是为了防止你无意间把内部细节暴露成公共 API。

1-6 什么时候该用类型嵌套

类型嵌套不是越多越好,它适合”小类型只为某一类型服务”的场景:

  • 一个枚举的 case 只在该类型内部有意义,比如状态码、模式、分类。
  • 一个辅助结构体只在容器内部用来打包数据,比如上面的 Values
  • 一个协议只被某个类型当作委托或数据源使用,比如 DiceGame.Delegate

如果那个类型将来会被多处独立使用、或语义上本就独立,那就该放到顶层,不要硬塞进嵌套。滥用嵌套反而会让层级变深、可读性下降。一句话:嵌套用来”收抽屉”,不是用来”叠箱子”。

1-7 小结

类型嵌套把一个枚举、结构体或协议定义在另一个类型内部,把”只服务于某类型”的配角收进同一作用域。它能隔离作用域、缩短命名,用法就是直接写在容器类型的大括号里,外部引用时加”容器名.”前缀。协议也能嵌套(外层不能是泛型),扩展里同样能加嵌套类型。注意 public 容器的嵌套类型默认是 internal,要对外公开需显式标 public。嵌套适合”小类型专为某一类型服务”的场景,语言本身到此,模块八面向对象编程也告一段落。