Swift 中 Enum 与 Struct:如何为状态建模选择最合适的工具

在日常开发中,我们经常需要为「状态」或「配置」建模。Swift 提供了两种最常见的值类型:

  • enum(枚举)
  • struct(结构体)

它们都能表达"一组相关的数据",但设计理念完全不同。选错工具往往会让后续迭代寸步难行

核心区别速览

维度 enum struct
值集合 有限、封闭(编译期已知全部 case) 开放、可扩展(运行时可任意实例化)
穷举检查 ✅ 编译器强制 switch覆盖全部 case ❌ 永远需要 default
关联值 ✅ 每个 case 可附带不同类型的数据 ❌ 属性统一声明
可扩展性 ❌ 新增 case 必须改源码 ✅ 随时创建新实例
表达意图 描述多选一的状态 描述配置/样式的组合值

Enum:为"有限状态"而生

典型场景:LoadingState

swift 复制代码
/// 业务模型
struct SomeModel { var title: String }

/// 1️⃣ 定义有限且互斥的加载状态
enum LoadingState {
    case idle
    case loading
    case success(SomeModel)   // 关联值
    case failure(Error)
}

/// 2️⃣ 在 View 中穷举所有可能
struct ContentView: View {
    @State var loadingState: LoadingState = .idle

    var body: some View {
        VStack {
            Text("Details")
            switch loadingState {          // 编译器会检查是否全覆盖
            case .idle:
                Text("Ready to load")
            case .loading:
                ProgressView("Loading...")
            case .success(let model):
                Text("Success: \(model.title)")
            case .failure(let error):
                Text("Error: \(error.localizedDescription)")
                    .foregroundColor(.red)
            }
        }
        .task { loadData() }
    }

    private func loadData() {
        loadingState = .loading
        do {
            let model = try ...           // 实际网络请求
            loadingState = .success(model)
        } catch {
            loadingState = .failure(error)
        }
    }
}

把 Enum 本身变成 View(扩展)

swift 复制代码
extension LoadingState: View {
    @ViewBuilder
    var body: some View {
        switch self {
        case .idle:        Text("Ready to load")
        case .loading:     ProgressView("Loading...")
        case .success(let m): Text("Success: \(m.title)")
        case .failure(let e): Text("Error: \(e.localizedDescription)").foregroundColor(.red)
        }
    }
}

/// 使用:直接把 state 当 View 用
VStack {
    Text("Details")
    loadingState          // <- 这里
}

个人建议:把 LoadingState 做成独立 LoadingStateView(state:) 会更清晰

Struct:为"可扩展配置"而生

典型场景:MessageStyle

swift 复制代码
/// 1️⃣ 定义一个开放的可配置样式
struct MessageStyle {
    let backgroundColor: Color
    let foregroundColor: Color
    // 可继续添加字体、圆角、阴影......
}

/// 2️⃣ 预置常用样式
extension MessageStyle {
    static let primary = MessageStyle(
        backgroundColor: .blue,
        foregroundColor: .white
    )
    static let secondary = MessageStyle(
        backgroundColor: .gray,
        foregroundColor: .primary
    )
    static let destructive = MessageStyle(
        backgroundColor: .red,
        foregroundColor: .white
    )
}

/// 3️⃣ 扩展计算属性
extension MessageStyle {
    var cornerRadius: Double { 10 }
}

/// 4️⃣ 注入到 Environment
extension EnvironmentValues {
    @Entry var messageStyle = MessageStyle.primary
}

extension View {
    func messageStyle(_ style: MessageStyle) -> some View {
        environment(\.messageStyle, style)
    }
}

/// 5️⃣ 消费样式的自定义 View
struct Message: View {
    let title: String
    @Environment(\.messageStyle) var style

    var body: some View {
        Text(title)
            .foregroundColor(style.foregroundColor)
            .background(
                style.backgroundColor,
                in: .rect(cornerRadius: style.cornerRadius)
            )
    }
}

/// 6️⃣ 使用:系统预设 + 自定义
struct MessageStyleExample: View {
    var body: some View {
        VStack(spacing: 16) {
            Message(title: "Primary").messageStyle(.primary)
            Message(title: "Secondary").messageStyle(.secondary)
            Message(title: "Delete").messageStyle(.destructive)

            // 完全自定义
            Message(title: "Custom")
                .messageStyle(
                    MessageStyle(
                        backgroundColor: .purple,
                        foregroundColor: .white
                    )
                )
        }
    }
}

Struct 的"陷阱":无法穷举

swift 复制代码
struct NetworkState: Equatable {
    let connectionType: String
    let speed: Double?
    
    static let offline   = NetworkState(connectionType:"Offline", speed:nil)
    static let wifi      = NetworkState(connectionType:"WiFi", speed:100)
    static let cellular  = NetworkState(connectionType:"Cellular", speed:25)
}

/// ❌ switch 必须写 default,否则编译不过
struct NetworkStatusView: View {
    @State private var networkState = NetworkState.offline

    var body: some View {
        switch networkState {
        case .offline:  ...
        case .wifi:     ...
        case .cellular: ...
        default:        // 无法避免
            Label("Unknown", systemImage: "questionmark")
        }
    }
}

控制 Struct 的"开放性":私有构造器

若既想要 struct 的扩展能力,又想限制"只有我们定义的值合法",可把构造器设为 private

swift 复制代码
struct Theme: Equatable {
    let primary: Color
    let secondary: Color
    let accent: Color
    
    private init(primary: Color, secondary: Color, accent: Color) {
        self.primary = primary; self.secondary = secondary; self.accent = accent
    }
    
    static let light = Theme(primary: .black, secondary: .gray, accent: .blue)
    static let dark  = Theme(primary: .white, secondary: .gray, accent: .orange)
    static let highContrast = Theme(primary: .black, secondary: .black, accent: .yellow)
}

注意:即使限制了构造器,switch 仍需 default,因为编译器无法证明未来不会新增 static let

混合使用:各取所长

enum 太"重"?嵌套 struct 解耦视图样式

swift 复制代码
enum Theme {
    case light, dark, highContrast
    
    /// 把与 View 相关的属性打包成 struct,避免枚举到处 switch
    struct ViewStyle {
        let primaryColor: Color
        let secondaryColor: Color
        let accentColor: Color
        
        init(primary: Color, secondary: Color, accent: Color) {
            self.primaryColor = primary
            self.secondaryColor = secondary
            self.accentColor = accent
        }
    }
    
    var viewStyle: ViewStyle {
        switch self {
        case .light:        return .init(primary: .black, secondary: .gray, accent: .blue)
        case .dark:         return .init(primary: .white, secondary: .gray, accent: .orange)
        case .highContrast: return .init(primary: .black, secondary: .black, accent: .yellow)
        }
    }
}

struct 太"散"?用 enum 归类

swift 复制代码
struct NetworkState {
    let name: String
    let speed: Double?
    
    enum ConnectionType { case none, slow, fast }
    var type: ConnectionType {
        guard let s = speed else { return .none }
        return s > 25 ? .fast : .slow
    }
    
    var color: Color {
        switch type {
        case .none: .red
        case .slow: .orange
        case .fast: .green
        }
    }
}

选型决策树

  1. 状态是否有限且不会频繁新增?

    ✅ 用 enum(LoadingState、Result、AuthenticationStatus)

  2. 是否允许业务方随时自定义新值?

    ✅ 用 struct(MessageStyle、Theme、Padding)

  3. 是否需要关联不同类型数据?

    ✅ enum 的关联值是唯一选择

  4. 是否需要编译期穷举检查?

    ✅ 只有 enum 能做到

实战扩展场景

场景 推荐方案 理由
表单校验状态 enum ValidationState { case idle, validating, valid(String), invalid(String) } 状态机清晰,穷举安全
暗黑模式/主题包 struct Theme+ 私有构造器 业务方可创建自定义主题,但受限于预设属性
网络层抽象 enum HTTPMethod { case get, post(body: Data) } 关联值表达能力好
富文本样式 struct RichTextStyle 可组合、无限扩展
Redux Store 中的 Action enum AppAction 穷举检查避免遗漏 reducer
组件库 Token struct DesignToken+ 私有构造器 既保证一致性,又允许用户自定义

个人总结

  • enum 像"单选题":选项在编译期钉死,换来极致安全。
  • struct 像"填空题":运行时可随意构造,换来极致灵活。

真正决定使用场景的,不是语法能力,而是需求对"封闭"还是"开放"的偏好。

在大型项目里,两者往往并存:

  • 用 enum 描述业务流程与状态机;
  • 用 struct 暴露给业务方做主题/样式配置;
  • 通过 嵌套 或 组合 消除各自短板。

记住一句话:"有限选 enum,无限选 struct;既要又要,就混用。"

相关推荐
大熊猫侯佩11 小时前
韦爵爷闯荡 Swift 6 江湖:单例秘籍新解(上)
swift·编程语言·apple
大熊猫侯佩11 小时前
韦爵爷闯荡 Swift 6 江湖:单例秘籍新解(中)
swift·敏捷开发·apple
大熊猫侯佩12 小时前
韦爵爷闯荡 Swift 6 江湖:单例秘籍新解(下)
swift·敏捷开发·apple
浅浅一笑^*^12 小时前
ArcGIS 4.x 绘图
开发语言·arcgis·swift
Swift社区15 小时前
Swift 解法详解:LeetCode 368《最大整除子集》
开发语言·leetcode·swift
东坡肘子16 小时前
写给这段旅程,也写给未来的自己 | 肘子的 Swift 周报 #0100
swiftui·swift·apple
zzywxc78719 小时前
苹果WWDC25开发秘鉴:AI、空间计算与Swift 6的融合之道
java·人工智能·python·spring cloud·dubbo·swift·空间计算
YungFan2 天前
iOS26适配指南之Liquid Glass App Icon
ios·swift
大熊猫侯佩2 天前
Apple 开发初学码农必看:一个 SwiftData 离奇古怪的问题(下)
ai编程·swift·apple