《Swift进阶》第六章(结构体和类)知识点梳理、重点与难点总结

6 阅读10分钟

一、核心知识点罗列

(一)值类型与引用类型的本质区别

  1. 核心定义与语义
    • 值类型(结构体/枚举):变量直接持有值本身,赋值或传递时会按值拷贝,多个变量各自持有独立副本,修改一个不会影响另一个(具有值语义)。
    • 引用类型(类/参与者):变量持有指向实例的引用(指针),赋值或传递时仅拷贝引用,多个变量可指向同一个实例,修改实例属性会影响所有引用者(具有引用语义)。
  2. 底层存储差异
    • 值类型:实例数据直接存储在变量指向的内存位置,无额外间接层(语义上)。
    • 引用类型:变量存储引用(内存地址),实例数据存储在引用指向的独立内存区域,存在一层间接引用。
  3. 典型示例对比
    • 值类型(结构体):IntCGPoint、自定义ScoreStruct,赋值后修改副本不影响原变量。
    • 引用类型(类):UIViewNSObject、自定义ScoreClass,赋值后修改实例属性会同步影响所有引用变量。

(二)可变性控制(let/var的行为差异)

  1. 变量声明(let/var)的影响
    • 值类型(结构体)
      • let声明:变量本身不可变,即使属性是var也无法修改(修改属性等同于赋值新实例)。
      • var声明:变量可修改,支持属性变更或整体赋值新实例。
    • 引用类型(类)
      • let声明:变量持有的引用不可变(不能重新赋值新实例),但实例的var属性可自由修改。
      • var声明:变量既可以重新赋值新实例,也支持修改实例的var属性。
  2. 属性声明(let/var)的影响
    • 无论值类型还是引用类型,let属性初始化后不可修改,var属性支持动态变更。
    • 结构体推荐用var声明属性:通过变量的let/var控制实例可变性,灵活性更高;类需谨慎使用var属性,避免全局可变状态。

(三)可变方法与inout参数

  1. 可变方法(mutating func)
    • 适用场景:仅结构体/枚举(值类型),用于修改自身属性(默认selflet,需mutating显式标记)。
    • 核心行为:调用可变方法等同于给变量赋值新实例,仅能在var声明的变量上调用。
    • 类的特殊性:无需mutating关键字,方法中selflet(不能重新赋值self),但可修改实例的var属性。
  2. inout参数
    • 作用:允许函数修改传入的变量(值类型),本质是“传值+返回时覆盖原变量”(非引用传递)。
    • 使用规则:
      • 传入变量必须是var声明,传递时加&符号(明确告知变量会被修改)。
      • 函数内部可直接修改inout参数的属性,修改结果会同步到外部变量。
    • 与可变方法的关系:可变方法本质是对inout self的封装,全局函数修改值类型需显式使用inout

(四)生命周期与内存管理

  1. 值类型(结构体)的生命周期
    • 与变量作用域绑定:变量离开作用域时,实例内存自动释放,无额外清理逻辑(无需deinit)。
    • 无多所有者问题:每个变量持有独立副本,生命周期互不影响。
  2. 引用类型(类)的生命周期
    • 自动引用计数(ARC):通过追踪引用计数管理内存,实例被最后一个引用释放时,调用deinit清理资源后释放内存。
    • 引用计数变更场景:
      • 赋值给变量:引用计数+1。
      • 变量赋值nil或离开作用域:引用计数-1。
      • 弱引用/无主引用不影响引用计数。

(五)循环引用与解决方案

  1. 循环引用的产生
    • 场景:两个或多个类实例互相强引用(如WindowrootView强引用ViewViewwindow强引用Window),导致引用计数无法降至0,实例永久无法释放(内存泄漏)。
    • 核心原因:强引用形成闭环,打破了“引用链可终止”的前提。
  2. 解决方案
    • 弱引用(weak)
      • 特性:不增加引用计数,实例销毁后自动置为nil,必须声明为可选值。
      • 适用场景:引用双方生命周期可能不同步(如代理模式中,tableView弱引用delegate)。
    • 无主引用(unowned)
      • 特性:不增加引用计数,实例销毁后不置为nil(非可选值),访问已销毁实例会崩溃。
      • 适用场景:引用双方生命周期强绑定(如“部分-整体”关系,PersonidCard无主引用PersonidCard不可能脱离Person存在)。
    • 闭包捕获列表
      • 场景:闭包捕获类实例,且实例持有闭包(如self.buttonTapped = { self.doSomething() })。
      • 解决方案:通过[weak self][unowned self]弱化捕获,避免闭环强引用。

(六)结构体与类的选择依据

  1. 优先使用结构体的场景
    • 存储独立数据(无共享状态),如模型对象、配置参数。
    • 希望具有值语义(赋值后独立,无意外副作用)。
    • 无需继承、多态,仅需属性和简单方法。
  2. 必须使用类的场景
    • 需要共享状态(多个组件操作同一实例),如UIView、网络管理器。
    • 需支持继承、动态派发(多态)。
    • 需自定义生命周期管理(deinit中执行清理逻辑,如关闭文件句柄、注销通知)。

(七)值语义与引用语义的灵活实现

  1. 具有值语义的类
    • 核心要求:实例不可变(所有属性为let),或修改时返回新实例(不修改原实例)。
    • 示例:NSDate(不可变)、自定义ImmutablePerson类(无var属性)。
    • 限制:需确保子类不添加可变属性(建议标记final)。
  2. 具有引用语义的结构体
    • 场景:结构体内部持有引用类型属性(如Array<UIView>),虽结构体是值类型,但内部引用会导致“浅拷贝”。
    • 注意:修改内部引用类型的属性会影响所有副本(破坏纯值语义),需手动实现深拷贝或写时复制。

(八)写时复制(Copy-On-Write,COW)优化

  1. 核心原理
    • 值类型的优化机制:赋值时不立即拷贝底层存储,仅当其中一个副本被修改时,才复制存储内容(保证值语义的同时提升性能)。
    • 标准库支持:ArrayDictionarySet等集合类型默认实现COW。
  2. 自定义实现步骤
    • 封装引用类型存储(如class Storage持有实际数据)。
    • 利用isKnownUniquelyReferenced(&storage)判断引用唯一性。
    • 仅当非唯一引用时,修改前复制存储内容。
  3. 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%。