简介:一套面向苹果生态开发者的 Swift 中文手册,覆盖从变量与常量、数据类型、运算符、控制流到函数重载与闭包、可选类型安全解包、结构体、类继承与初始化器、协议匹配、泛型、枚举及关联值、字符串与 Unicode 处理、数组字典集合、元组、模块与包管理等核心内容,既适合零基础入门,也能作为日常开发的速查参考。压缩包为 7z 格式,共 111 个文件,其中 96 个 Markdown 文档构成主体,按主题分章组织,便于在线阅读、全文检索和本地编辑;另附少量图片、示例脚本和说明文档,整体仅 1.21MB,轻量且目录结构清晰,可快速定位所需章节。目前已有 387 人学习下载;通过手册中对类型安全、高性能以及与 Objective-C 无缝集成等特性的讲解,并衔接 CocoaPods、Swift Package Manager 等生态工具,可帮助读者从语法理解平滑过渡到 iOS、macOS 平台的实际项目开发。
1. Swift中文手册在讲什么:一门比你想的更“独立”的语言
第一次翻开 Swift 中文手册,很多人会被目录劝退:泛型、协议、内存安全、异步函数,每个词都像教科书的章节。但我实际把它从头用到尾之后,Swift 是“上手成本被严重高估”的语言——前提是你把它当工具书,而不是当教材。中文手册真正帮你解决的是语言墙:官方文档里一句话带过的概念,它会给足背景、给足代码、给足报错场景。这篇笔记按“先跑通、再攻语法、最后躲坑”的顺序往下写,新手能一步步跟着编译出第一个程序,写过几年代码的人也能在值类型、协议、并发这几节,把以前零碎的知识点重新串起来。
2. 先跑通再谈语法:用 Swift 工具链写出第一个能编译的程序
2.1 中文手册别从头啃:先装工具链,用 REPL 验证环境
很多人拿到手册第一件事就是翻“基础语法”,结果看到第二个例子就卡在“怎么跑起来”。我的建议是倒过来:先装工具链,再翻语法。Swift 的官方工具链支持单独下载安装,也可以伴随官方图形化 IDE 一起装。安装完成后第一件事不是打开编辑器,而是打开终端验证编译器是否就绪。
# 验证工具链是否就绪 swift --version # 进入交互式解释器 REPL,适合随手验证语法 swift第一条命令会输出正在使用的 Swift 版本号,第二条命令会进入 REPL 交互环境:输入表达式按回车立刻执行,适合验证“某种写法到底对不对”。退出 REPL 输入:quit即可。这一步能过滤掉 80% 的环境问题:版本不对、路径不对、工具链没生效,在这里都会立刻暴露。
有了工具链之后,先认识三个运行环境。命令行直接解释执行是最快的验证路径;REPL 适合单手敲小表达式;官方 IDE 里的游乐场(Playground)则适合需要看中间变量变化的场景。三个环境的取舍我用一张表说明:
| 运行环境 | 启动速度 | 适用场景 | 要不要建工程 |
|---|---|---|---|
| REPL | 最快 | 验证单个表达式 | 不需要 |
解释执行swift xxx.swift | 快 | 验证单个脚本文件 | 不需要 |
| 游乐场 | 中等 | 看每行中间值、调试 UI 逻辑 | 轻量 |
| 编译成二进制 | 慢 | 发布、性能测试 | 建议要 |
新手最容易犯的错误是:跳过 REPL,直接打开 IDE 建工程,结果被工程配置、签名、模拟器那些事缠住,半天没写出一行 Swift 代码。正确的路径是先用 REPL 把手感找回来,再决定是不是要上工程。
2.2 首个可运行程序:用 swiftc 与解释模式跑通 Hello
工具链就绪后,写第一个程序。我一般会在任意目录建一个hello.swift,内容从变量声明到字符串插值,一行不浪费:
// 文件名:hello.swift // 用 let 声明常量,类型由编译器推断 let greeting = "Hello, Swift" // 字符串插值,注意 \( ) 的写法 print("\(greeting),这是我的第一个 Swift 程序") // 用 var 声明可变变量 var counter = 1 counter += 1 print("计数:\(counter)")运行方式有两种,对应两种需求。一种是直接解释执行,改动立即生效,适合还在调逻辑的阶段。另一种是编译成独立二进制,适合性能测试和发布。
# 编译成可执行文件,-o 指定输出名 swiftc hello.swift -o hello # 运行编译产物 ./hello # 跳过编译步骤,直接解释执行 swift hello.swiftswiftc是编译模式,产物是一个不依赖源文件的二进制;swift 文件.swift是解释模式,每次运行都会做一次编译,速度慢一点但省去产物管理。日常调代码用解释模式,确认无误后再用swiftc出正式产物。swiftc有几个参数我会频繁用到:-o指定输出文件名;-g生成调试信息,配合断点看变量;-O开启优化,跑性能测试用。
提示:解释模式下报错会直接定位到行号,但不会生成二进制。发布时务必走编译模式,并且把优化参数打开。
2.3 用官方包管理器建工程:多文件调试不再靠临时脚本
当验证内容超过一个文件、或者想给代码加测试,就该从单文件切到正式工程。Swift 的官方包管理器(Swift Package Manager)是官方工具链自带的,不需要额外安装。它的工程描述文件叫Package.swift,最小配置长这样:
// Package.swift // swift-tools-version:5.9 // 注意:这里写的是你本机工具链支持的版本,不是越新越好 import PackageDescription let package = Package( name: "SwiftManualDemo", targets: [ // 可执行 target,会生成可运行的二进制 .executableTarget( name: "SwiftManualDemo", path: "Sources/SwiftManualDemo" ) ] )目录结构按官方约定组织,Sources下放源码,入口文件必须是main.swift或者标注了@main的类型:
SwiftManualDemo/ ├── Package.swift └── Sources/ └── SwiftManualDemo/ └── main.swift构建和运行命令也是两条:
# 编译整个包,首次会生成 .build 目录 swift build # 运行可执行 target swift run SwiftManualDemoswift build是增量编译,第二次再跑会明显变快,因为它只编译改动过的文件。-c release参数可以切到发布模式,启用完整优化,性能测试时必须加。这里有个容易踩的点:main.swift和@main不能同时存在,如果源码里既放了main.swift又用@main标注了入口类,编译器会报重复入口错误。
单文件脚本适合验证语法,包工程适合验证模块、依赖、测试。前者是“秒开秒关”,后者是“正式养成”。新手不必一上来就建包,但一旦确认要持续写下去,切到包工程是迟早的事。
3. Swift 语法骨架:把中文手册里的概念词翻译成每天写的代码
3.1 let、var 与类型注解:把“可变性”先定死,后面少踩坑
Swift 的语法骨架里,最影响代码风格的不是类型系统,而是可变性声明。let和var的区分贯穿所有代码:let声明常量,赋值后不可改;var声明变量,可以重新赋值。手册里会花一整节讲存储属性、计算属性、可变性语义,落到工程里其实就一句话:能写let就不写var。
// let 是常量,赋值后不能再改 let maxRetry = 3 // var 是变量,后面可以重新赋值 var currentRetry = 0 currentRetry += 1 // 类型推断:编译器从右侧字面量推导类型 let price = 9.9 // 推断为 Double let name = "Swift" // 推断为 String // 需要显式标注类型时用冒号 let count: Int = 100为什么“优先 let”这么重要?首先,let告诉编译器这个值不会变,编译器可以做更多优化;其次,读代码的人一眼就知道这个变量不会在中途被改,逻辑推理成本直接下降。工程里最常见的翻车案例是:用var声明了一个数组,后续代码里有三处往里面追加元素,结果某次调试发现数据重复,溯源成本极高。如果是let,编译器直接不让你追加,错误停在编译期。
需要显式标注类型的场景也有章可循:空容器初始化时编译器无从推断元素类型,必须标注,例如let list: [String] = [];函数参数和返回值类型,属于 API 设计的一部分,必须标注。其余场景优先让编译器推断,代码更短也更不容易写错。
3.2 控制流与函数:guard 和 switch 的写法习惯
控制流是所有语言的共通部分,Swift 的差别在几个偏好性很强的写法上。第一个是guard,它的作用是“先检查必要条件,不满足就提前退出”,让后续代码保持平铺,不出现多层嵌套。第二个是switch,它在 Swift 里没有隐式贯穿,每个分支结束自动 break,少了一类经典 bug。
// 一个模拟连接操作的函数 func connect(server: String, port: Int) { // guard 先检查合法性,不满足就直接 return guard port > 0, port < 65536 else { print("端口不合法") return } print("连接 \(server):\(port)") } connect(server: "127.0.0.1", port: 8080) // switch 必须覆盖所有分支 let status = 404 switch status { case 200: print("成功") case 404: print("未找到") default: print("其他状态") }guard和if的选择标准我一般看“代码之后还要不要继续走”:如果条件不满足就彻底不想往下执行,用guard;如果只是二选一的两个分支,用if-else更直白。guard的额外好处是,通过检查的变量在后续作用域里可以直接用,不用再解一次包,这跟后面的可选型强相关。
switch在 Swift 里支持区间匹配多种值,配合where还能加附加条件,这个特性写数值分类、状态机时特别好用。新手容易犯的错是忘了写default,编译器会直接报“switch must be exhaustive”,这是 Swift 的设计意图:强迫你考虑所有分支,减少漏判。控制流这部分没有太多黑科技,按“能平铺就不嵌套、能全覆盖就不留 default 糊弄”的原则写,代码质量不会差到哪去。
3.3 可选型四种解包:手册里的“?”与“!”在不同场景的正确用法
可选型(Optional)是新手被劝退的头号概念。手册把它的原理包装得很严密:它本质上是一个枚举,有.none和.some(T)两个状态。理解这一点后,解包方式就好懂了:把值从“可能为空”的状态里取出来,有四种常见姿势,对应不同场景。
| 解包方式 | 写法 | 适用场景 | 风险 |
|---|---|---|---|
| 可选绑定 | if let v = value | 有值处理、无值跳过的常规场景 | 嵌套多时层级深 |
| guard 绑定 | guard let v = value else return | 函数入口检查,无值就走人 | 只能在函数内用 |
| nil 合并 | value ?? "默认值" | 无值时需要一个兜底值 | 无法区分无值原因 |
| 强制解包 | value! | 逻辑上保证非空 | 空值时直接崩溃 |
// 模拟一个可能返回空值的查询 func findKey(_ key: String) -> String? { if key == "name" { return "Swift" } return nil } let value = findKey("name") // 可选绑定:有值进分支,没值直接跳过 if let v = value { print("找到值:\(v)") } // nil 合并:无值时给一个兜底 print("结果是:\(value ?? "未知")") // 强制解包:明确知道非空才可以用 let definite: String? = "Manual" print(definite!)四种方式不是平级关系。90% 的场景用if let和guard let就够了,??用于需要兜底值的输出场景,!应该被当成“危险操作”而不是默认选项。工程里凡是出现!的地方,代码评审时我都会多问一句“这里凭什么保证非空”。很多线上崩溃都来自强制解包遇到空值,尤其是从字典取值、解析网络 JSON 这种“外界输入”的场景。
一个容易踩的坑是:if let解包出的变量只在分支作用域里有效,出了分支就访问不到。想在整个函数后续使用解包后的值,正确做法是guard let。这个区别手册里写得很隐晦,但实际写代码时几乎每天都会遇到。
4. 手册不会直说的深层设计:值类型、协议与并发
4.1 struct 与 class 的选择:值语义和引用语义谁在什么时候用
中文手册读到中段,会密集出现结构体(struct)和类(class)的对比。这个选择不是风格偏好,而是 Swift 整个内存模型的根基。结构体是值语义:赋值是完整拷贝,两个变量互不影响;类是引用语义:赋值是共享同一个实例,一个变量改了,另一个也变。
| 维度 | 结构体 | 类 |
|---|---|---|
| 语义 | 值语义,赋值即拷贝 | 引用语义,赋值即共享 |
| 继承 | 不支持,用协议组合 | 支持类继承 |
| 可变性 | let实例不能改var属性 | let实例也能改var属性 |
| 内存管理 | 自动管理,栈上为主 | 引用计数,有循环引用风险 |
| 默认选择 | 标准库多数类型是结构体 | 需要身份共享时才用 |
用代码看区别最直观。先看结构体:
struct Counter { var value = 0 // 值类型方法里修改属性必须加 mutating mutating func increment() { value += 1 } } var a = Counter() let b = a // b 是 a 的完整拷贝 a.increment() print(a.value) // 输出 1 print(b.value) // 输出 0,互不影响再看类:
final class CounterClass { var value = 0 func increment() { value += 1 } } let c = CounterClass() let d = c c.increment() print(d.value) // 输出 1,因为 d 和 c 指向同一个实例这个差异直接影响 debug 体验:值类型的问题通常好定位,因为一份数据就是一份,报错位置离真相近;引用类型的“灵异现象”往往是某个不起眼的角落把共享实例改了,排查时得先数清楚谁持有这个引用。
选型标准我按这个顺序判断:需要“两个变量指向同一个东西并同步变化”吗?需要——用类;只是拿一份数据做处理并传播结果?用结构体。递归数据结构(比如树节点持有子节点)也要用类或indirect enum,因为结构体直接包含自己会在编译期报“无限大小”错误。
4.2 协议不只是接口:把行为约束拆成可以组合的零件
协议(protocol)在 Swift 里远比其他语言的 interface 灵活,因为协议本身可以有默认实现。这让“面向协议编程”成为可能:你定义的不是一个继承树,而是一组可组合的能力清单。手册里最常见的一句话是“通过协议扩展提供默认实现”,翻译成工程语言就是:协议负责声明能力,扩展负责给能力兜底。
// 定义一个行为协议:任何类型只要满足这个协议就能被统一处理 protocol Describable { var summary: String { get } } // 协议扩展提供默认实现 extension Describable { var summary: String { "未命名" } } // 结构体遵守协议,不实现 summary 也编译通过 struct Article: Describable { var title: String // 这里给出具体实现,覆盖默认值 var summary: String { "文章:\(title)" } } // 函数依赖协议而不是具体类型 func output(_ item: Describable) { print(item.summary) } let post = Article(title: "Swift 协议实战") output(post)协议和类继承的本质区别是:继承回答“它是什么”,协议回答“它能做什么”。一个类只能继承一个父类,却能同时遵守十几个协议。写框架时这几乎是必杀技:定义协议约束行为,用扩展提供默认实现,调用方只需要声明“我遵守这个协议”,就能获得整套能力。
工程里最常见的误用是:拿协议强行模仿基类,把公共属性全塞进协议里,结果每个实现都要重复写一遍存储属性。正确做法是让协议只描述“行为”,把“状态”留给具体类型自己管理。协议里的{ get }和{ get set }也要注意:前者只要求可读,后者要求可读写,约束放太宽会失去控制力,放太严又会误伤实现方。
4.3 async/await 与 actor:让并发代码按顺序说人话
Swift 的并发模型在中文手册里篇幅不小,初读容易晕。落到代码上,先记住一个词:暂停点。标了async的函数可能暂停,await就是暂停的位置,等结果回来再继续。这套机制把以前回调嵌套的代码重新拉直了,读起来像同步代码。
// 模拟一个需要等待的耗时操作 func fetchUserInfo() async -> String { // 模拟网络耗时 1 秒 try? await Task.sleep(nanoseconds: 1_000_000_000) return "user-1001" } // 异步函数里用 await 等结果 func load() async { let user = await fetchUserInfo() print("拿到用户:\(user)") } // 从同步入口启动一个异步任务 Task { await load() }代码里两个点要说明:Task.sleep(nanoseconds:)是会抛错的可暂停函数,示例里用try?忽略错误,数值1_000_000_000是十亿纳秒,等于 1 秒;Task {}是手动创建一个异步任务,用来从同步函数进入异步世界。
并发真正难的不是语法,是共享状态。多个任务同时往同一个数组里写数据,顺序不可预期,这就是数据竞争。Swift 的答案是一个叫 actor 的类型:它内部状态一次只允许一个任务访问,其他任务必须排队等待。调用 actor 的方法要加await,这是它自己在保证串行。
// actor 让内部状态同一时间只被一个任务修改 actor CounterActor { var count = 0 func add(_ value: Int) { count += value } } // 使用时要 await,因为可能排队 let counter = CounterActor() Task { await counter.add(5) }刚接触并发的开发者不需要把 actor 的所有细节吃透,先建立这个意识就够了:只要看到多个异步任务要改同一个可变状态,就把状态装进 actor。手动加锁、用可变全局量的方案在 Swift 并发模型里都是下策,编译器不会帮你检查锁的覆盖范围,但 actor 会。
5. Swift 避坑:中文手册目录里没有的那几页
5.1 现象:编译报错指向底层库文件,以为是工具链坏了
有段时间我经常收到“编译器是不是坏了”的反馈。现象很统一:打开一个旧项目,代码一行没改,swift build报错,红字指向的却是.build目录里的依赖源码,或者更夸张,直接指向工具链自带的标准库头文件。新人第一反应是重装工具链,装完再跑,还是同样的错。原因多半是工具链版本跨了大版本:项目构建配置里记录的路径、依赖的编译产物,和当前默认工具链对不上。机器上如果还同时装过多个版本,命令行默认取到的可能是旧版。
解决办法分三步。第一步在项目根目录跑swift --version,确认当前命令行用的是哪个版本。第二步,删除.build目录,让所有依赖全部重新编译:
# 删除构建缓存,强制全量重编 rm -rf .build swift build第三步,如果还不行,检查项目配置里是否有针对旧工具链的硬编码路径。实际中九成情况是.build缓存把新旧工具链的产物混在一起了,清理后就能恢复。这条的经验是:编译报错先怀疑环境,再怀疑代码,尤其当你连着几天没动过一个文件时。
提示:不要一上来就改依赖里的源码去“修”报错,那是在给旧环境当补丁,换个机器又得重新补。
5.2 现象:字符串插值打出 Optional("xxx"),日志全被污染
写日志时经常看到这种输出:用户信息:Optional("user-1001")。App 运行正常,但日志没法看,传到日志系统里还带着噪音。很多人以为这是“数据有问题”,实际是字符串插值遇到可选型产生的显示效果。Swift 里\(value)插值会调用CustomStringConvertible描述,可选型的描述自带Optional(...)包装。
解决思路是“先解包再插值”,而不是“想办法把 Optional 去掉”。
let name: String? = "Swift" // 方案一:给默认值,适合面向用户展示 print("用户:\(name ?? "匿名")") // 方案二:只调试用,保留 Optional 痕迹 print("用户:\(String(describing: name))")工程上我的习惯是:给用户看的信息,插值前必须用??把可选型兜成具体值;给开发者看的调试日志,用String(describing:)反而能保留类型信息,方便排查。最忌讳的是为了省事在print里强制解包,一旦空值就崩在日志打印处——为一条日志崩溃,价值完全不匹配。
5.3 现象:结构体方法里给属性赋值报 “self is immutable”
自定义结构体里写了一个increase(by:)方法,内部给percent += step,编译直接报错,提示“self is immutable”。新手很容易被这个报错劝退,以为是 Swift 的代码风格限制,转头把所有结构体改成了类,彻底放弃值语义。实际原因很明确:结构体是值类型,方法默认不能修改自身的属性,想要修改必须在方法前面加mutating。
struct Progress { var percent = 0 // mutating 明确表态:这个方法会修改自身 mutating func increase(by step: Int) { percent += step } } var progress = Progress() progress.increase(by: 5) print(progress.percent)使用mutating方法时有一个隐含约束:必须通过var声明的变量调用,let声明的结构体实例没有修改权限。这个设计不是没事找事,而是在编译期拦住“不可变处修改”的意图。顺便说一个容易混淆的变体:如果结构体的属性是引用类型(比如一个类实例),那不加mutating也能修改那个引用指向的对象,因为改的不是结构体本身。看到这个报错时先别急着换类型,想清楚你要改的到底是谁的状态。
5.4 现象:闭包存储与循环引用剪不断理还乱
把闭包存进属性时,编译器会提示需要声明@escaping逃逸闭包。加上之后,代码开始出现一种“循环引用”问题:控制器持有闭包属性,闭包内部又持有控制器,两边都不释放,内存泄漏检查出来的全是这一类。理解它的关键是记住一个事实:闭包默认强捕获列表里的每个对象,只要闭包活着,被捕获的对象就活着。
解决办法是显式告诉闭包“不要强持有我”,用捕获列表写[weak self]:
final class Client { // 逃逸闭包,可能被存起来后异步调用 var onFinish: (() -> Void)? func start() { // [weak self] 打破循环引用,self 释放后闭包内的 self 为 nil onFinish = { [weak self] in self?.printDone() } } func printDone() { print("完成") } }代码里的self?.printDone()是安全调用:如果self已经释放,整句跳过,不崩溃。什么时候该写[weak self]、什么时候不需要写,我的判断标准是看闭包的生命周期:闭包只是局部变量,用一次就释放,不需要写;闭包会被保存、被延迟执行、被当成回调属性,一定要写。内存问题不会立刻报错,它像慢性病,等 App 切了十几回页面后突然内存暴涨,那时候排查最费时间。所以培养“存闭包就要想生命周期”的条件反射,比会写闭包更重要。
5.5 现象:异步任务打印顺序和预期完全对不上
并发代码里最常见的“翻车”案例:写了两段Task,期望先执行第一段再执行第二段,结果日志顺序反了。原因不复杂,任务调度不保证创建顺序等于执行顺序。多个并发任务同时等待资源时,谁先恢复由调度器决定,跟代码顺序无关。
Task { let user = await loadUser() print(user) // 期望先打印 } Task { let cache = await loadCache() // 期望后打印 print(cache) }这段代码里两个任务没有任何依赖关系,调度器可以任意顺序恢复。想保证顺序,必须把依赖关系写进代码里,用一条await链串起来:
Task { let user = await loadUser() // 第一个结果拿到之后,才发起下一次请求 let cache = await loadCache(for: user) print(cache) }这个坑的精髓在于:并发代码里“看起来的先后”不是先后,只有await建立起的依赖关系才是先后。写异步代码时,每次新建一个Task都要自问一遍:这个任务和已有的任务之间有顺序要求吗?如果有,把它并进同一条await链,而不是另起炉灶。多年以后回头看,一切并发问题都出在“顺序没表达成依赖”。
6. 进阶技巧:把“查手册”变成“写验证小代码片”的闭环
6.1 用单文件沙盒给每个语法点写最小验证
如果你已经能跑通上面每一段代码,接下来要做的不是读更多章节,而是建一条自己的验证闭环。我长期保持一个习惯:每读到一个新语法特性,就在一个临时目录里建一个scratch.swift,用最小代码把特性验证一遍,跑完删掉或留档。比如手册里讲到“闭包捕获列表”,我不会只看例子,而是自己写一段存闭包再调用的代码,亲眼确认生命周期。
# 语法验证沙盒:写一个临时文件,跑完即弃 touch scratch.swift # 编辑内容后,直接解释运行 swift scratch.swift单文件沙盒的核心价值是零成本启动:不需要建工程、不需要配依赖、不需要写目录结构,任何想法都能在 10 秒内得到验证。等验证通过,再把代码挪进正式工程里。这套流程把“读文档”变成了“做实验”,记忆效果比读十遍都牢。
6.2 顺手验证异常分支:崩溃信息是最好的教材
验证语法时,很多人只验证“正常路径”——调用一次、打印结果、闭环。我会额外做一步:把边界条件也跑了。比如验证数组索引,故意访问一个不存在的下标,看清楚崩溃信息长什么样;验证可选型强制解包,故意解一个空值,把编译器报错和运行时报错都见过一遍。原因很简单:正常路径的代码人人都能写,异常分支的边界条件才是项目里真正吃时间的地方。
我翻车最多的一次,是把手册里“用强制解包简化代码”当成了默认行为,结果在 JSON 解析里崩了一整天。从那以后,每个拿不准的特性都先在沙盒里跑一遍,把错误留在临时文件里,而不是留到业务代码里。你读手册的时候,很多行为文字上解释得模棱两可,但代码会给你确定的答案。希望这篇从工具链到避坑再到验证闭环的路径,能帮你在 Swift 中文手册里少绕一些路。
本文还有配套的精品资源,点击获取