一、核心知识点罗列
(一)值类型与引用类型的本质区别
- 核心定义与语义
- 值类型(结构体/枚举):变量直接持有值本身,赋值或传递时会按值拷贝,多个变量各自持有独立副本,修改一个不会影响另一个(具有值语义)。
- 引用类型(类/参与者):变量持有指向实例的引用(指针),赋值或传递时仅拷贝引用,多个变量可指向同一个实例,修改实例属性会影响所有引用者(具有引用语义)。
- 底层存储差异
- 值类型:实例数据直接存储在变量指向的内存位置,无额外间接层(语义上)。
- 引用类型:变量存储引用(内存地址),实例数据存储在引用指向的独立内存区域,存在一层间接引用。
- 典型示例对比
- 值类型(结构体):
Int、CGPoint、自定义ScoreStruct,赋值后修改副本不影响原变量。 - 引用类型(类):
UIView、NSObject、自定义ScoreClass,赋值后修改实例属性会同步影响所有引用变量。
- 值类型(结构体):
(二)可变性控制(let/var的行为差异)
- 变量声明(let/var)的影响
- 值类型(结构体):
let声明:变量本身不可变,即使属性是var也无法修改(修改属性等同于赋值新实例)。var声明:变量可修改,支持属性变更或整体赋值新实例。
- 引用类型(类):
let声明:变量持有的引用不可变(不能重新赋值新实例),但实例的var属性可自由修改。var声明:变量既可以重新赋值新实例,也支持修改实例的var属性。
- 值类型(结构体):
- 属性声明(let/var)的影响
- 无论值类型还是引用类型,
let属性初始化后不可修改,var属性支持动态变更。 - 结构体推荐用
var声明属性:通过变量的let/var控制实例可变性,灵活性更高;类需谨慎使用var属性,避免全局可变状态。
- 无论值类型还是引用类型,
(三)可变方法与inout参数
- 可变方法(mutating func)
- 适用场景:仅结构体/枚举(值类型),用于修改自身属性(默认
self为let,需mutating显式标记)。 - 核心行为:调用可变方法等同于给变量赋值新实例,仅能在
var声明的变量上调用。 - 类的特殊性:无需
mutating关键字,方法中self为let(不能重新赋值self),但可修改实例的var属性。
- 适用场景:仅结构体/枚举(值类型),用于修改自身属性(默认
- inout参数
- 作用:允许函数修改传入的变量(值类型),本质是“传值+返回时覆盖原变量”(非引用传递)。
- 使用规则:
- 传入变量必须是
var声明,传递时加&符号(明确告知变量会被修改)。 - 函数内部可直接修改
inout参数的属性,修改结果会同步到外部变量。
- 传入变量必须是
- 与可变方法的关系:可变方法本质是对
inout self的封装,全局函数修改值类型需显式使用inout。
(四)生命周期与内存管理
- 值类型(结构体)的生命周期
- 与变量作用域绑定:变量离开作用域时,实例内存自动释放,无额外清理逻辑(无需
deinit)。 - 无多所有者问题:每个变量持有独立副本,生命周期互不影响。
- 与变量作用域绑定:变量离开作用域时,实例内存自动释放,无额外清理逻辑(无需
- 引用类型(类)的生命周期
- 自动引用计数(ARC):通过追踪引用计数管理内存,实例被最后一个引用释放时,调用
deinit清理资源后释放内存。 - 引用计数变更场景:
- 赋值给变量:引用计数+1。
- 变量赋值
nil或离开作用域:引用计数-1。 - 弱引用/无主引用不影响引用计数。
- 自动引用计数(ARC):通过追踪引用计数管理内存,实例被最后一个引用释放时,调用
(五)循环引用与解决方案
- 循环引用的产生
- 场景:两个或多个类实例互相强引用(如
Window的rootView强引用View,View的window强引用Window),导致引用计数无法降至0,实例永久无法释放(内存泄漏)。 - 核心原因:强引用形成闭环,打破了“引用链可终止”的前提。
- 场景:两个或多个类实例互相强引用(如
- 解决方案
- 弱引用(weak):
- 特性:不增加引用计数,实例销毁后自动置为
nil,必须声明为可选值。 - 适用场景:引用双方生命周期可能不同步(如代理模式中,
tableView弱引用delegate)。
- 特性:不增加引用计数,实例销毁后自动置为
- 无主引用(unowned):
- 特性:不增加引用计数,实例销毁后不置为
nil(非可选值),访问已销毁实例会崩溃。 - 适用场景:引用双方生命周期强绑定(如“部分-整体”关系,
Person的idCard无主引用Person,idCard不可能脱离Person存在)。
- 特性:不增加引用计数,实例销毁后不置为
- 闭包捕获列表:
- 场景:闭包捕获类实例,且实例持有闭包(如
self.buttonTapped = { self.doSomething() })。 - 解决方案:通过
[weak self]或[unowned self]弱化捕获,避免闭环强引用。
- 场景:闭包捕获类实例,且实例持有闭包(如
- 弱引用(weak):
(六)结构体与类的选择依据
- 优先使用结构体的场景
- 存储独立数据(无共享状态),如模型对象、配置参数。
- 希望具有值语义(赋值后独立,无意外副作用)。
- 无需继承、多态,仅需属性和简单方法。
- 必须使用类的场景
- 需要共享状态(多个组件操作同一实例),如
UIView、网络管理器。 - 需支持继承、动态派发(多态)。
- 需自定义生命周期管理(
deinit中执行清理逻辑,如关闭文件句柄、注销通知)。
- 需要共享状态(多个组件操作同一实例),如
(七)值语义与引用语义的灵活实现
- 具有值语义的类
- 核心要求:实例不可变(所有属性为
let),或修改时返回新实例(不修改原实例)。 - 示例:
NSDate(不可变)、自定义ImmutablePerson类(无var属性)。 - 限制:需确保子类不添加可变属性(建议标记
final)。
- 核心要求:实例不可变(所有属性为
- 具有引用语义的结构体
- 场景:结构体内部持有引用类型属性(如
Array<UIView>),虽结构体是值类型,但内部引用会导致“浅拷贝”。 - 注意:修改内部引用类型的属性会影响所有副本(破坏纯值语义),需手动实现深拷贝或写时复制。
- 场景:结构体内部持有引用类型属性(如
(八)写时复制(Copy-On-Write,COW)优化
- 核心原理
- 值类型的优化机制:赋值时不立即拷贝底层存储,仅当其中一个副本被修改时,才复制存储内容(保证值语义的同时提升性能)。
- 标准库支持:
Array、Dictionary、Set等集合类型默认实现COW。
- 自定义实现步骤
- 封装引用类型存储(如
class Storage持有实际数据)。 - 利用
isKnownUniquelyReferenced(&storage)判断引用唯一性。 - 仅当非唯一引用时,修改前复制存储内容。
- 封装引用类型存储(如
- COW的破坏场景
willSet/didSet观察者:会强制触发复制(即使未修改属性)。- 结构体中持有非COW的引用类型属性:需手动处理深拷贝。
二、重点知识点总结
(一)值类型与引用类型的核心差异(语法+行为)
- 语法层面:结构体用
struct,类用class;结构体默认生成成员初始化器,类需手动实现或依赖默认初始化器。 - 行为层面:赋值/传递时的拷贝机制(值拷贝vs引用拷贝)、可变性控制(
let对实例属性的影响)、生命周期管理(作用域绑定vs ARC)。 - 核心原则:“值类型保独立,引用类型共实例”,选择时优先考虑是否需要共享状态。
(二)可变性的底层逻辑
- 结构体的可变性是“变量级”:
let变量锁定整个实例,var变量允许修改;类的可变性是“实例级”:let变量仅锁定引用,实例属性可独立修改。 - 实践建议:结构体属性默认用
var,通过变量的let/var控制可变性;类属性谨慎用var,避免无意识的全局状态变更。
(三)循环引用的识别与解决
- 常见场景:代理模式(类互相引用)、闭包捕获(实例持有闭包+闭包捕获实例)。
- 解决口诀:“生命周期不同步用weak,强绑定用unowned,闭包捕获加列表”。
- 关键提醒:
weak必须是可选值,unowned需确保引用实例始终存在,否则会崩溃。
(四)写时复制(COW)的核心价值与实现
- 价值:平衡值语义的安全性和性能(避免不必要的拷贝)。
- 核心步骤:封装引用存储→判断唯一性→修改时复制,标准库集合已实现,自定义大体积值类型时建议手动实现。
三、难点知识点总结
(一)可变性的深层逻辑与常见误区
- 误区1:认为
let修饰的类实例“不可变”→ 实际仅引用不可变,实例属性可修改。 - 误区2:结构体属性用
let就能保证实例不可变→ 若结构体持有引用类型属性,引用指向的实例仍可能被修改。 - 深层逻辑:结构体的“不可变”是语义上的(变量无法生成新实例),而非底层内存不可变;类的“不可变”仅针对引用本身,与实例状态无关。
(二)循环引用的隐蔽场景与解决选择
- 隐蔽场景:多实例闭环引用(如A→B→C→A)、闭包间接捕获(闭包捕获B,B持有A,A持有闭包)。
- weak vs unowned的选择难点:
- 无法确定实例生命周期是否同步时,优先用
weak(安全,避免崩溃)。 - 仅当“被引用者一定晚于引用者销毁”时,用
unowned(简洁,无可选值解包成本)。
- 无法确定实例生命周期是否同步时,优先用
(三)写时复制的实现细节与破坏场景
- 实现难点:
isKnownUniquelyReferenced的正确使用(仅适用于纯Swift类,不支持Objective-C类)、深拷贝与浅拷贝的边界处理。 - 破坏场景:
willSet/didSet会触发强制复制,导致COW失效(如结构体属性添加观察者后,赋值即拷贝);结构体中嵌套非COW引用类型,会破坏值语义。
(四)值语义与引用语义的混合使用陷阱
- 结构体持有引用类型属性(如
struct Model { var views: [UIView] }),赋值后修改views中的实例属性,会影响所有副本(因数组是COW,但UIView是引用类型)。 - 解决方案:要么避免结构体持有可变引用类型,要么手动实现深拷贝(修改时复制引用类型实例)。
(五)inout参数与引用传递的混淆
- 误区:认为
inout是传递引用→ 实际是“传值+回写”,编译器可能优化为引用传递,但逻辑上需视为“修改后覆盖原变量”。 - 限制:
inout参数不能逃逸(如返回捕获inout参数的闭包),否则编译报错。
四、总结
本章核心围绕“值类型与引用类型的本质差异”展开,核心逻辑是“通过类型选择控制语义,通过可变性控制安全性,通过内存管理避免泄漏”。重点在于掌握结构体和类的行为差异、可变性的底层逻辑、循环引用的解决方法及写时复制的优化;难点集中在可变性的深层逻辑、循环引用的隐蔽场景、COW的实现与破坏,以及混合语义的陷阱。
实际开发中,应遵循“优先结构体,必要时用类”的原则:简单数据模型用结构体保证值语义安全,共享状态或需继承时用类,并通过弱引用/无主引用避免循环引用。对于大体积值类型,可手动实现写时复制优化性能,同时规避willSet等破坏COW的场景,平衡安全性与效率。
如果需要,我可以帮你整理结构体与类的行为对比表,或针对某个难点(如自定义写时复制、循环引用排查)提供详细代码示例。当前文件内容过长,豆包只阅读了前 15%。