本节摘要:本节讲 Swift 类型系统的登记规则——var 与 let、类型推断、基本数据类型、类型安全,以及全书最重要的角色"可选值"。结论先行:可选值是把"数据尚未到位"写进类型系统的机制,界面出生到数据加载完成之间的每一秒,你都在和它打交道。
上一节舞台已经通电,这一节在后台登记处排队:排片界面出生前的每一份数据,都要先在这里领一张类型身份牌。
阅读完本节,你应当能够:
排片界面的一生里有大量"此刻还没有"的时刻:网络请求没回来,场次是空的;用户没登录,昵称是空的;图片没下载完,海报是空的。老语言(包括 Objective-C)的处理办法是用一个特殊值 NULL 或 nil 混进任何类型,结果就是"给不存在的东西发消息"这类运行时崩溃——戏演到一半,发现台上少了个演员。
Swift 的答案很决绝:把"缺席"做成一种类型状态。一个值要么是 Int,要么是"Int 或缺席"(写作 Int?),两者在类型系统里是两种不同的东西。想用前者就必须先证明它存在。于是"没考虑数据未到位"从运行时崩溃降级成编译错误——事故在开演前就被拦下。
let hallName = "3 号厅" // 常量:登记后不可改名换人 var seatsLeft = 40 // 变量:余票数随售出而变化 seatsLeft -= 2 // 合法:var 允许重新赋值 // hallName = "IMAX 厅" // 编译错误:Cannot assign to value: 'hallName' is a 'let' constant
我更倾向默认全用 let,编译器报错说"这个该是 var"时再改。理由:let 读起来是承诺——看一眼就知道这个值全程不变,排查状态问题时可以直接跳过它。
let filmTitle = "雾中灯塔" // 推断为 String let duration = 118 // 推断为 Int let rating = 8.7 // 推断为 Double(带小数点默认 Double,不是 Float) let price: Double = 45 // 显式标注:初值看不出类型时必须写 let hallCount: UInt8 = 6 // 显式标注小整数类型时,范围会参与检查
let runtimeMinutes = 118 // let hours = runtimeMinutes / 60.0 // 编译错误:Int 与 Double 不能直接相除 let hours = Double(runtimeMinutes) / 60.0 // 正确:先把 Int 显式转成 Double,结果 1.966... // let wrong: Double = runtimeMinutes // 编译错误:不能把 Int 类型的值赋给 Double
Swift 不做隐式类型转换,所有跨类型运算都要写明转换。多敲几个字符,换来的是"永远不会在毫不知情时被截断精度"。

var runtime: Int? = nil // 片长未知:请求还没回来 runtime = 118 // 数据到位,盒子装进 118 // let bad = runtime + 5 // 编译错误:可选值必须先解包 if let minutes = runtime { print("片长 \(minutes) 分钟") // 输出:片长 118 分钟 } else { print("片长未知") // runtime 为 nil 时走这里 }
// 姿势一:if let——两种分支都要处理时用 func showRuntime(_ runtime: Int?) { if let minutes = runtime { print("片长 \(minutes) 分钟") } else { print("暂无片长信息") } } // 姿势二:guard let——条件不满足就提前退场(第2章函数登场后它会大量出现) func discountedPrice(vipLevel: Int?) -> Double { guard let level = vipLevel else { return 60.0 // 没有等级信息,按原价收场 } return 60.0 * (1 - 0.1 * Double(level)) // 之后 level 一直可用 } // 姿势三:nil 合并——给缺席一个默认替身 let displayRuntime = runtime ?? 0 print("余量 \(displayRuntime)") // runtime 为 nil 时显示 0
⚠️ 常见坑:看到
runtime!(强制解包)觉得省事。它等于跟编译器签免责协议:"我保证有值,炸了算我的"。数据来自网络或用户输入时没人能打这个包——强制解包只留给"编译器已证明有值"的场景(比如两行前刚判空成功)。
把铁律放到真实场景里走一遍。背景:排片界面顶部要显示"今日推荐语",数据来自运营配置接口;用户昵称来自登录态。两者的共同点是"可能永远不回来"。
struct BoardHeader { let cinemaName: String // 影院名:打进安装包,必然存在 var slogan: String? // 推荐语:运营配置,可能缺席 var nickname: String? // 昵称:未登录则缺席 // 组装问候语:用 nil 合并提供兜底文案 func greeting() -> String { let name = nickname ?? "影迷" return "\(name),欢迎来到\(cinemaName)" } // 组装推荐语:guard let 保证主流程干净 func headline() -> String { guard let text = slogan, !text.isEmpty else { return "今日好片,等你开场" // 缺席或空串都兜底 } return text } } var header = BoardHeader(cinemaName: "拾光影院·滨江店", slogan: nil, nickname: "老周") print(header.greeting()) // 输出:老周,欢迎来到拾光影院·滨江店 print(header.headline()) // 输出:今日好片,等你开场 header.slogan = "周五悬疑夜:五部连映" print(header.headline()) // 输出:周五悬疑夜:五部连映
结果解读:cinemaName 用 let 且非可选,因为它由本地决定;slogan 与 nickname 用 var + 可选,因为它们来自外部世界。变式:把 nickname 改成登录后必有的值,类型可以收窄成 String——类型越窄,编译器帮你检查得越多,这就是"用类型表达业务事实"的思路。
再补一张对照表,把容易混淆的"空"摆在一起:
| 写法 | 含义 | 适用场景 |
|---|---|---|
""(空串) |
存在,但内容为零 | 用户清空了搜索框 |
0 |
存在,数值为零 | 余票真的卖完了 |
空数组 [] |
存在,没有元素 | 今日场次尚未排 |
nil(可选) |
缺席,压根没有 | 请求未返回、未登录 |
把"卖完了"和"还没查到"混在一个 0 里,是列表页"幽灵场次"bug 的经典来源。
💡 关键直觉:可选值问号不是负担,而是文档。一个函数签名里每个问号都在告诉你:"此处会出现数据未到位的时刻。"读别人的代码时,先扫问号,就能复原出这个界面的加载时序。
下一节走进道具间:登记好的角色们(一部部影片、一场场放映)需要收纳工具——数组管顺序、字典管键值、字符串管文案,1.3 节清点这三种道具。