1.2 角色登记:变量常量类型与可选值


1.2 角色登记:变量、常量、类型与可选值

本节摘要:本节讲 Swift 类型系统的登记规则——var 与 let、类型推断、基本数据类型、类型安全,以及全书最重要的角色"可选值"。结论先行:可选值是把"数据尚未到位"写进类型系统的机制,界面出生到数据加载完成之间的每一秒,你都在和它打交道。

上一节舞台已经通电,这一节在后台登记处排队:排片界面出生前的每一份数据,都要先在这里领一张类型身份牌。

学习目标

阅读完本节,你应当能够:

  1. 说清 var 与 let 的语义差别,并解释"let 优先"能防住哪类事故;
  2. 对给定的声明写出推断类型,或在需要时显式标注类型;
  3. 列出 Swift 的基本数据类型并说明 Int 与 Double 混用的处理规则;
  4. 用 if let、guard let、nil 合并运算符三种方式安全处理可选值;
  5. 解释可选值与"空字符串""0"的本质区别。

一、问题与直觉:为什么登记处这么严格

排片界面的一生里有大量"此刻还没有"的时刻:网络请求没回来,场次是空的;用户没登录,昵称是空的;图片没下载完,海报是空的。老语言(包括 Objective-C)的处理办法是用一个特殊值 NULL 或 nil 混进任何类型,结果就是"给不存在的东西发消息"这类运行时崩溃——戏演到一半,发现台上少了个演员。

Swift 的答案很决绝:把"缺席"做成一种类型状态。一个值要么是 Int,要么是"Int 或缺席"(写作 Int?),两者在类型系统里是两种不同的东西。想用前者就必须先证明它存在。于是"没考虑数据未到位"从运行时崩溃降级成编译错误——事故在开演前就被拦下。

二、核心原理:登记规则的五条铁律

铁律一:let 优先,var 按需

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 且非可选,因为它由本地决定;slogannicknamevar + 可选,因为它们来自外部世界。变式:把 nickname 改成登录后必有的值,类型可以收窄成 String——类型越窄,编译器帮你检查得越多,这就是"用类型表达业务事实"的思路。

再补一张对照表,把容易混淆的"空"摆在一起:

写法 含义 适用场景
""(空串) 存在,但内容为零 用户清空了搜索框
0 存在,数值为零 余票真的卖完了
空数组 [] 存在,没有元素 今日场次尚未排
nil(可选) 缺席,压根没有 请求未返回、未登录

把"卖完了"和"还没查到"混在一个 0 里,是列表页"幽灵场次"bug 的经典来源。

💡 关键直觉:可选值问号不是负担,而是文档。一个函数签名里每个问号都在告诉你:"此处会出现数据未到位的时刻。"读别人的代码时,先扫问号,就能复原出这个界面的加载时序。

本节要点回顾

  • let 优先:默认常量,需要改变再改 var,读代码时心智负担小一半;
  • 类型推断:初值能看出类型就省略标注,看不出(如转换、空集合)必须写;
  • 不隐式转换:跨类型运算显式写转换,杜绝静默截断;
  • 可选值是类型:Int 与 Int? 是两种身份,用前必须拆包;
  • 三种解包:if let 双分支、guard let 早退、问号问号兜底,按场景选;
  • 空与缺席不同:""、0、[] 是"有,但是零",nil 是"没有",业务上要分开建模。

下一节走进道具间:登记好的角色们(一部部影片、一场场放映)需要收纳工具——数组管顺序、字典管键值、字符串管文案,1.3 节清点这三种道具。


作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U