# Swift 基础

Source: https://codewiki.com/zh/swift/fundamentals/

> - **what**: Swift 是静态类型语言：每个值都有类型，编译器会检查值如何经过声明、表达式、函数和集合。
> - **trap**: 类型推断不会执行隐式数值转换，也不会让不安全的输入自动变得有效；数组下标和强制解包仍可能在运行时触发错误。
> - **fix**: 优先使用 `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` 保存有序序列，允许重复元素。
- `Set` 保存唯一且可哈希的元素，不提供应用层面的顺序契约。
- `Dictionary<Key, Value>` 把唯一且可哈希的键关联到值；键可能不存在，因此查找结果是可选值。

同一集合中的所有元素都具有声明的元素类型。
字面量的上下文通常可以提供该类型，例如 `let ports: [Int] = [80, 443]`。
空字面量需要上下文：空数组写作 `[String]()`，空集合写作 `Set()`，空字典则写作 `[String: Int]()`，或在有类型标注时写作 `[:]`。

条件和循环把各项操作组合起来。
`if` 与 `switch` 选择路径，`for`-`in` 遍历序列，`while` 则在布尔条件为 true 时重复执行。
`switch` 必须穷尽，因此每个可能输入都要进入一个分支或有意设置的 `default`。

函数为一项计算提供带类型的名称。
参数与返回值都属于函数类型的一部分，除非声明为 `inout`，否则参数是常量。
Swift 默认使用实参标签；在参数名称前写 `_` 可以让调用点省略标签。

编译器会从内向外检查这套模型。
字面量与周围上下文确定类型，运算符约束操作数，函数调用则必须匹配参数和结果类型。
依赖数据的边界仍需运行时检查，例如解析、整数运算、集合索引与可选值解包。

## 示例

### 绑定值并作出判断

这个结账程序用整数分表示金额，避免意外混合整数数量与浮点价格。
局部类型都由编译器推断。
只有 `subtotal` 使用 `var`，因为折扣分支会重新给它赋值。

<!-- quick -->

```swift
// file: checkout.swift
// # 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)")
```

```text
Not executed here: Swift toolchain is not installed.
```

<!-- /quick -->

三元表达式从两个分支中为 `shipping` 选择一个值，所以该绑定可以保持为常量。
所有运算都使用 `Int`，名称也保留了单位决策。
生产环境中的金额应使用整数最小货币单位或领域类型，而不是含义不明的 `Double`。

### 解析边界数据而不强制解包

字典查找与整数解析都会返回可选值。
`parseSeats(_:)` 通过一个 `guard` 依次要求字段存在、可以解析且位于允许范围内，然后才返回普通 `Int`。

```swift
// file: seat_requests.swift
// # 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")
    }
}
```

```text
Not executed here: Swift toolchain is not installed.
```

调用方无法区分字段缺失、格式错误与超出范围，因为这个小函数有意把三种情况都映射为 `nil`。
如果调用方需要不同的恢复或诊断信息，应使用结果类型或可抛出函数。
因此，无效输入不会作为数值人数进入预订计算。

### 按契约选择集合

数组保留标签的输入顺序和重复项。
集合去除重复项，随后 `sorted()` 为显示创建顺序可预测的数组。
字典为每个唯一标签保存一个派生值。

```swift
// file: tag_summary.swift
// # 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: ", "))")
```

```text
Not executed here: Swift toolchain is not installed.
```

显式排序属于输出契约；直接遍历 `uniqueTags` 无法为调用方提供有意义的顺序。
`Dictionary(uniqueKeysWithValues:)` 要求每个键都唯一。
这里的键值对来自集合，因此满足该前置条件；生成代码却经常把这个初始化器直接用于未经检查的输入，并在遇到重复键时触发错误。

### 观察值语义

Swift 数组和字典具有值语义（value semantics）。
函数可以把输入集合复制到局部变量，只修改这个局部值，然后将其返回，而不改变调用方的原始值。

```swift
// file: collection_copies.swift
// # 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"])
```

```text
Not executed here: Swift toolchain is not installed.
```

虽然 `result` 最初包含相同元素，`queuedJobs` 仍然没有改变。
`profile` 发生修改后，`profile` 与 `savedProfile` 也遵循相同的分离规则。
Swift 标准集合可能在写入前共享内部存储，但这种优化不会改变可以观察到的值行为。

## 陷阱

> **陷阱:** 把推断类型的声明当作动态类型，会产生无法编译的赋值。
执行 `var count = 0` 后，绑定类型就是 `Int`；之后赋值 `"zero"` 并不会改变其类型。

**修复方法：** 为每个声明选择一种稳定类型。
如果字面量推断可能掩盖预期表示，应添加显式标注；传入值则在命名清楚的边界完成转换，不要让强制转换散落在算法中。

> **陷阱:** 数值转换并不表示可以忽略范围、精度或单位。
`Int` 与 `Double` 不会隐式混合；当值可以表示时，把浮点值转换成 `Int` 会有意丢弃小数部分。

**修复方法：** 先确定哪种类型负责这项计算，验证来源后再集中转换。
转换必须精确保留值时使用 `Int(exactly:)`，并在标识符中明确保留单位，例如 `priceInCents`。

> **陷阱:** `array[index]` 不会返回可选值。
负数或越界索引会触发错误，就像可选值为 `nil` 时执行 `optional!` 一样。

**修复方法：** 任务允许时优先使用遍历、`first` 或 `last`。
确实要用外部整数选择元素时，先检查 `array.indices.contains(index)`；在边界检查之前，还要先解包解析与字典查找结果。

> **陷阱:** 集合与字典的迭代顺序不是展示契约。
某次运行中看似稳定的输出，可能只是意外依赖构造方式或实现细节。

**修复方法：** 只要顺序可被观察，就要显式排序键或值，包括快照、命令输出、有顺序要求的编码数据和测试。
如果真正需要的是唯一性或查找能力，则仍应保留无序集合。

> **陷阱:** Swift `String` 不是 UTF-16 码元、字节或可用整数索引的字符数组。
用户感知的字符可能由多个 Unicode 标量组成，因此 `text[0]` 这类生成代码无法编译。

**修复方法：** 遍历 `Character` 值，或使用 `String.Index` 操作。
只有协议确实要求码元时，才选择 `utf8` 之类的正确底层视图；不要把字节数当作显示字符数。

<!-- deep -->

## 值语义可以被观察

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 文件：

1. 找出每个外部字符串、可选字段、数值单位以及对集合顺序的假设。
2. 检查每个绑定是否具有预期静态类型，并且只开放必要的可变性。
3. 沿着每种无效输入跟踪到明确的拒绝、后备或向外传播的失败。
4. 检查可观察输出是否具有确定顺序，并遵循正确的 Unicode 边界。

编译器会证明类型兼容性、确定初始化与 `switch` 穷尽性。
哪些输入可信、哪些默认值真实、哪些修改属于共享状态，仍要由作者决定。
这些决策在源码中清楚可见后，编译器诊断可以覆盖类型错误，测试则可以覆盖剩余的依赖数据路径。

<!-- /deep -->

[检查点: swift/fundamentals](https://codewiki.com/zh/swift/fundamentals/#checkpoint)

## 延伸阅读

- [Swift 编程语言：基础部分](https://docs.swift.org/swift-book/documentation/the-swift-programming-language/thebasics/)
- [Swift 编程语言：集合类型](https://docs.swift.org/swift-book/documentation/the-swift-programming-language/collectiontypes/)
- [Swift 编程语言：字符串与字符](https://docs.swift.org/swift-book/documentation/the-swift-programming-language/stringsandcharacters/)
- [Swift 编程语言：函数](https://docs.swift.org/swift-book/documentation/the-swift-programming-language/functions/)
