Swift 是静态类型语言:每个值都有类型,编译器会检查值如何经过声明、表达式、函数和集合。
类型推断不会执行隐式数值转换,也不会让不安全的输入自动变得有效;数组下标和强制解包仍可能在运行时触发错误。
优先使用 let,在明确的边界执行转换,用 if let 或 guard let 验证可选结果,并根据顺序与唯一性需求选择集合。
是什么,为什么存在
Swift 的基础规则把名称、值、类型和控制流连接起来。
声明使用 let 或 var 绑定名称,绑定的类型决定允许哪些操作。
函数封装这些操作,数组、集合与字典则组织一组带类型的值。
Swift 是静态强类型语言。
程序运行前,编译器就知道每个表达式的类型,并拒绝不匹配的操作,例如直接把 Int 与 Double 相加。
这能在接近错误源头的位置发现大量问题,但无法证明集合索引一定有效,也无法证明可选值一定包含内容。
大多数声明使用 类型推断(type inference) 。
在 let retryLimit = 3 中,整数字面量及其上下文让编译器推断出 Int,但该声明仍然只有一种固定类型。
如果初始化值不能清楚表达预期类型,或 API 契约应直接可见,可以写出 let timeout: Double = 3 这样的类型标注。
不会再次赋值的绑定使用 let,算法确实需要重新赋值时才使用 var。
这一区分能让代码审查者直接看到可变位置。
不过,它描述的是绑定本身,并不表示通过类引用可达的每个对象都具有深层不可变性。
从命令行脚本到应用程序,每段 Swift 代码都会用到这些规则。 输入以文本或可选数据的形式进入,代码对其转换和验证,控制流选择执行路径,带类型的集合再把结果交给下一层。
工作原理
Swift 声明在被读取前必须具有名称、类型和值。
初始化器既可以提供值,也可以提供足够的类型推断上下文。
没有初始化器的声明需要显式类型,而且所有使用路径都必须先完成初始化;只要编译器能证明之后恰好赋值一次,延后给 let 赋值也是合法的。
常见标量类型包括 Int、Double、Bool、String 与 Character。
整数类型在固定边界内表示整数,浮点类型近似表示实数,条件则直接接受 Bool。
Swift 不会把 0、空字符串或 nil 当作 false。
数值类型不会隐式混合。
应在已经考虑单位和精度的位置构造目标类型,例如 Double(itemCount) * unitPrice。
解析的失败方式不同:任意文本不一定包含有效整数,所以 Int("12") 返回 Int?。
可选值(optional value) 写作 Wrapped?,其中要么有一个包装值,要么是 nil。
使用 if let 进行可选绑定(optional binding)时,只有值存在才会进入对应分支。
如果函数剩余部分都依赖该值,可以使用 guard let,其 else 分支必须离开当前作用域。
Swift 的主要集合类型表达了不同契约:
Array<Element>保存有序序列,允许重复元素。Set<Element>保存唯一且可哈希的元素,不提供应用层面的顺序契约。Dictionary<Key, Value>把唯一且可哈希的键关联到值;键可能不存在,因此查找结果是可选值。
同一集合中的所有元素都具有声明的元素类型。
字面量的上下文通常可以提供该类型,例如 let ports: [Int] = [80, 443]。
空字面量需要上下文:空数组写作 [String](),空集合写作 Set<String>(),空字典则写作 [String: Int](),或在有类型标注时写作 [:]。
条件和循环把各项操作组合起来。
if 与 switch 选择路径,for-in 遍历序列,while 则在布尔条件为 true 时重复执行。
switch 必须穷尽,因此每个可能输入都要进入一个分支或有意设置的 default。
函数为一项计算提供带类型的名称。
参数与返回值都属于函数类型的一部分,除非声明为 inout,否则参数是常量。
Swift 默认使用实参标签;在参数名称前写 _ 可以让调用点省略标签。
编译器会从内向外检查这套模型。 字面量与周围上下文确定类型,运算符约束操作数,函数调用则必须匹配参数和结果类型。 依赖数据的边界仍需运行时检查,例如解析、整数运算、集合索引与可选值解包。
示例
绑定值并作出判断
这个结账程序用整数分表示金额,避免意外混合整数数量与浮点价格。
局部类型都由编译器推断。
只有 subtotal 使用 var,因为折扣分支会重新给它赋值。
// # not executed here: Swift toolchain is not installed.
let unitPrice = 125
let quantity = 3
var subtotal = unitPrice * quantity
let hasMemberDiscount = true
if hasMemberDiscount {
subtotal -= 25
}
let shipping = subtotal >= 300 ? 0 : 40
let total = subtotal + shipping
print("items: \(quantity)")
print("subtotal: \(subtotal)")
print("shipping: \(shipping)")
print("total: \(total)")Not executed here: Swift toolchain is not installed.三元表达式从两个分支中为 shipping 选择一个值,所以该绑定可以保持为常量。
所有运算都使用 Int,名称也保留了单位决策。
生产环境中的金额应使用整数最小货币单位或领域类型,而不是含义不明的 Double。
解析边界数据而不强制解包
字典查找与整数解析都会返回可选值。
parseSeats(_:) 通过一个 guard 依次要求字段存在、可以解析且位于允许范围内,然后才返回普通 Int。
// # not executed here: Swift toolchain is not installed.
func parseSeats(_ fields: [String: String]) -> Int? {
guard
let rawSeats = fields["seats"],
let seats = Int(rawSeats),
(1...8).contains(seats)
else {
return nil
}
return seats
}
let requests = [
["name": "Mina", "seats": "4"],
["name": "Noah", "seats": "many"],
["name": "Iris"],
]
for request in requests {
let name = request["name", default: "anonymous"]
if let seats = parseSeats(request) {
print("\(name): \(seats)")
} else {
print("\(name): invalid")
}
}Not executed here: Swift toolchain is not installed.调用方无法区分字段缺失、格式错误与超出范围,因为这个小函数有意把三种情况都映射为 nil。
如果调用方需要不同的恢复或诊断信息,应使用结果类型或可抛出函数。
因此,无效输入不会作为数值人数进入预订计算。
按契约选择集合
数组保留标签的输入顺序和重复项。
集合去除重复项,随后 sorted() 为显示创建顺序可预测的数组。
字典为每个唯一标签保存一个派生值。
// # not executed here: Swift toolchain is not installed.
let rawTags = ["swift", "ios", "swift", "testing", "ios"]
let uniqueTags = Set(rawTags)
let displayTags = uniqueTags.sorted()
let lengths = Dictionary(
uniqueKeysWithValues: displayTags.map { tag in
(tag, tag.count)
}
)
for tag in displayTags {
print("\(tag): \(lengths[tag, default: 0])")
}
let longTags = displayTags.filter { $0.count >= 5 }
print("long: \(longTags.joined(separator: ", "))")Not executed here: Swift toolchain is not installed.显式排序属于输出契约;直接遍历 uniqueTags 无法为调用方提供有意义的顺序。
Dictionary(uniqueKeysWithValues:) 要求每个键都唯一。
这里的键值对来自集合,因此满足该前置条件;生成代码却经常把这个初始化器直接用于未经检查的输入,并在遇到重复键时触发错误。
观察值语义
Swift 数组和字典具有 值语义(value semantics) 。 函数可以把输入集合复制到局部变量,只修改这个局部值,然后将其返回,而不改变调用方的原始值。
// # not executed here: Swift toolchain is not installed.
func addingPriority(to jobs: [String]) -> [String] {
var result = jobs
result.insert("incident", at: 0)
return result
}
let queuedJobs = ["backup", "report"]
let priorityJobs = addingPriority(to: queuedJobs)
print("queued:", queuedJobs.joined(separator: ", "))
print("priority:", priorityJobs.joined(separator: ", "))
var profile = ["theme": "light"]
let savedProfile = profile
profile["theme"] = "dark"
print("current:", profile["theme", default: "missing"])
print("saved:", savedProfile["theme", default: "missing"])Not executed here: Swift toolchain is not installed.虽然 result 最初包含相同元素,queuedJobs 仍然没有改变。
profile 发生修改后,profile 与 savedProfile 也遵循相同的分离规则。
Swift 标准集合可能在写入前共享内部存储,但这种优化不会改变可以观察到的值行为。
陷阱
修复方法: 为每个声明选择一种稳定类型。 如果字面量推断可能掩盖预期表示,应添加显式标注;传入值则在命名清楚的边界完成转换,不要让强制转换散落在算法中。
修复方法: 先确定哪种类型负责这项计算,验证来源后再集中转换。
转换必须精确保留值时使用 Int(exactly:),并在标识符中明确保留单位,例如 priceInCents。
修复方法: 任务允许时优先使用遍历、first 或 last。
确实要用外部整数选择元素时,先检查 array.indices.contains(index);在边界检查之前,还要先解包解析与字典查找结果。
修复方法: 只要顺序可被观察,就要显式排序键或值,包括快照、命令输出、有顺序要求的编码数据和测试。 如果真正需要的是唯一性或查找能力,则仍应保留无序集合。
修复方法: 遍历 Character 值,或使用 String.Index 操作。
只有协议确实要求码元时,才选择 utf8 之类的正确底层视图;不要把字节数当作显示字符数。
值语义可以被观察
Swift 标准库中的 String、Array、Set 与 Dictionary 都是值。
赋值、传参和返回会产生逻辑上独立的值,所以修改一个变量不会修改从它复制出的另一个值。
用户定义的结构体与枚举也遵循这套模型,不过值内部仍可能包含指向共享类实例的引用。
这是接口保证,并不表示每次赋值都会立即复制每个字节。
应从可观察的修改来理解:如果 let snapshot = current 捕获了一份集合值,之后对 current 的结构性修改不会改变 snapshot。
如果两个集合包含相同的可变类实例,对这些实例内部的修改仍然共享,因为元素本身是引用。
函数签名默认采用这套模型。
集合参数是传入函数的值,参数名称不能被重新赋值,也不能直接修改。
可以把它复制到局部 var 后返回新值;如果 API 有意修改调用方的存储,而且调用点能清楚看出这种所有权,也可以使用 inout。
写时复制是一种优化
标准库值类型可以使用 写时复制(copy-on-write) ,在没有修改需要独占缓冲区时共享存储。 发生修改时,实现会在必要时让存储变为独占,从而保留值语义。 因此,即使第四个示例中的集合在写入前共享后备存储,其行为也如同彼此独立。
不要把写时复制当作同步机制。 两个任务修改同一个变量仍会形成共享可变状态,缓冲区是否共享之类的实现细节也不是 API 保证。 在对真实工作负载进行测量之前,不应提出性能结论,也不应把清楚的值改成手动引用存储。
嵌套值还需要检查元素本身。 复制结构体数组后再修改,会得到独立的元素值;复制类引用数组时,复制的是引用。 在声称快照已经隔离之前,应审查完整对象图。
字符串索引遵循字符边界
Swift Character 表示一个扩展 字素簇(grapheme cluster) ,其中可以包含一个或多个 Unicode 标量。
与字节遍历相比,这套模型能让迭代更接近用户感知的字符。
这也意味着任意整数偏移与字符边界之间不存在固定映射。
String.Index 记录某个字符串视图中的有效位置。
应通过字符串自身的 startIndex、endIndex、index(after:) 或 index(_:offsetBy:) 等操作取得索引;再次使用旧位置或外部位置前要先验证。
移动到较远字符所需的工作可能与距离成正比,因此反复使用偏移索引并不适合完整扫描。
应根据正在实现的边界选择视图。
面向文本的行为使用 Character 迭代,需要区分标量时使用 unicodeScalars,面向字节的格式则使用 utf8。
规范化、与区域设置相关的比较以及显示宽度属于不同问题,单独一个 count 无法同时回答它们。
整数运算存在边界
Swift 的定宽整数类型都有最小值和最大值。 普通运算超过这些边界时会触发错误,即使是发布构建也不会静默回绕。 应选择足以覆盖领域范围的类型,在计算前验证不可信操作数,并把多个最大允许值放在一起测试,而不是逐个测试。
溢出运算符 &+、&- 与 &* 会有意回绕。
它们只适合契约本来就是模运算的算法,不应当用来修补原因不明的溢出。
生成代码有时会替换成这些运算符来阻止崩溃,却把无效交易悄悄变成看似有效的总额。
范围同样编码了边界选择。
1...8 包含两个端点,1..<8 则不包含上界。
空范围或反向范围需要谨慎处理;集合切片应从该集合自身取得边界,而不是假定每种集合都能使用从零开始的整数位置。
可选值的默认值是一项策略
只有左侧可选值为 nil 时,空合运算符才会对右侧求值。
如果缺失确实表示某个领域默认值,这种写法很简洁,例如把缺失的显示昵称设为 "Anonymous"。
如果 nil 同时覆盖格式错误或输入被拒绝,而后备值又看似成功,这种写法就会造成误导。
可选链在遇到第一个缺失环节时停止,并让整体结果变为可选值。
如果一次读取允许把缺失继续向外传递,这种行为很有用。
如果每个缺失环节都需要不同消息,可选链就不那么合适,因为最后的 nil 已经无法说明哪项假设失败。
在拥有后备决策的层级之前,应保留值缺失的原因。
小型解析器可以有意对所有无效格式返回 nil;请求边界则可能需要错误类型,以区分字段缺失、语法错误和值不允许。
类型应当保留调用方所需的信息,不多也不少。
作用域与文件决定名称范围
大括号会为函数、循环与分支创建局部作用域。
声明可以遮蔽外层名称,但作用域嵌套后,反复使用 value 或 result 之类的宽泛名称会让日志与审查难以理解。
尤其在转换边界处,名称应保留领域含义与单位。
Swift 允许在文件作用域声明,命令行源文件也能执行顶层语句。 较大的程序通常把边界工作放在入口点附近,把计算移入具有明确参数和结果类型的函数。 这样就能独立测试格式错误的输入,不必连同进程启动和控制台输出一起测试。
访问控制也是声明契约的一部分。
只服务于一个文件的辅助声明可以使用 private 或 fileprivate,面向模块的声明则可以使用 Swift 默认的 internal。
不要只因为生成代码把所有辅助项放在顶层,就对外暴露这些声明。
按以下顺序审查一份基础 Swift 文件:
- 找出每个外部字符串、可选字段、数值单位以及对集合顺序的假设。
- 检查每个绑定是否具有预期静态类型,并且只开放必要的可变性。
- 沿着每种无效输入跟踪到明确的拒绝、后备或向外传播的失败。
- 检查可观察输出是否具有确定顺序,并遵循正确的 Unicode 边界。
编译器会证明类型兼容性、确定初始化与 switch 穷尽性。
哪些输入可信、哪些默认值真实、哪些修改属于共享状态,仍要由作者决定。
这些决策在源码中清楚可见后,编译器诊断可以覆盖类型错误,测试则可以覆盖剩余的依赖数据路径。
4个问题 · 1 道输出预测题 · 1 道找错题