面试题手册

梳理高频技术问题,帮助你按主题复习和查漏补缺。

服务端阅读 05月27日 11:30

Swift Codable 怎么用?JSON 键名不一致和默认值怎么处理?

Codable 是 Encodable + Decodable 的类型别名,让 Swift 类型可以自动编解码为 JSON、Plist 等格式。只要所有属性都是 Codable 类型,编译器自动合成编解码逻辑——不需要手写一行解析代码。struct User: Codable { let id: Int let name: String let email: String?}// 自动支持 JSONEncoder/JSONDecoderCodable 最大的价值是消灭了手动 JSON 解析的样板代码。之前用 Objective-C 的 NSJSONSerialization 返回 NSDictionary,再手动取值转型,又长又容易崩溃。Codable 让这个过程类型安全、编译器检查。追问JSON 的键名和 Swift 属性名不一样怎么办?用 CodingKeys 枚举映射:struct User: Codable { let name: String enum CodingKeys: String, CodingKey { case name = "full_name" }}CodingKeys 必须覆盖所有需要编解码的属性——没列出的属性会被跳过(如果是可选型且有默认值)。这个枚举也可以用来"只编码部分字段",不需要的字段不写进 CodingKeys 即可。什么时候需要自定义 init(from:) 和 encode(to:)?自动合成搞不定的场景:日期格式(JSON 里是时间戳或字符串,Swift 是 Date)、嵌套结构不一致、默认值逻辑、多个字段组合解码。比如 JSON 里日期是秒级时间戳:init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) let timestamp = try container.decode(Double.self, forKey: .createdAt) self.createdAt = Date(timeIntervalSince1970: timestamp)}能用 CodingKeys 解决的就不要自定义 init(from:)——自定义越多,维护成本越高。Codable 的默认值怎么处理?Swift 不支持在 Codable 属性上直接设默认值——如果 JSON 里缺了某个键,解码直接失败。解法一:用可选型 let email: String?,缺失时为 nil。解法二:自定义 init(from:),用 decodeIfPresent + 空合并运算符:self.name = try container.decodeIfPresent(String.self, forKey: .name) ?? "unknown"。Swift 5.9 有社区提案支持默认值,但目前还没有正式语法。Codable 和 NSCoding 有什么区别?NSCoding 是 Objective-C 时代的协议,需要手动实现 encode/decode,类型不安全(Any 类型),只支持 class。Codable 是 Swift 原生的,编译器自动合成,类型安全,struct 和 class 都支持,且不依赖 Objective-C 运行时。新项目用 Codable,没有理由再用 NSCoding。CustomStringConvertible 和 Codable 有什么关系?没有直接关系。CustomStringConvertible 控制 print() 和字符串插值的输出格式,Codable 控制序列化/反序列化格式。一个常见错误:在 CustomStringConvertible 的 description 里输出 JSON——应该用 JSONEncoder 编码,description 应该输出人类可读的调试信息,不是 JSON 字符串。写段代码struct User: Codable { let id: Int let name: String let email: String? // 键名映射 enum CodingKeys: String, CodingKey { case id, name case email = "email_address" }}// 编码let user = User(id: 1, name: "Alice", email: "a@b.com")let data = try JSONEncoder().encode(user)// 解码let decoded = try JSONDecoder().decode(User.self, from: data)// 自定义日期格式let decoder = JSONDecoder()decoder.dateDecodingStrategy = .secondsSince1970decoder.keyDecodingStrategy = .convertFromSnakeCase // snake_case → camelCase
服务端阅读 05月27日 11:30

Swift 高阶函数 map、filter、reduce 怎么用?有什么区别?

高阶函数是"接受函数作为参数"或"返回函数"的函数。Swift 里最常用的是 map、filter、reduce,加上 compactMap、flatMap、sorted。它们让集合操作从命令式循环变成声明式一行代码。map:变换每个元素,返回新数组filter:按条件筛选,返回满足条件的元素reduce:把所有元素聚合成一个值compactMap:map + 过滤 nilflatMap:map + 展平一层嵌套追问map 和 compactMap 有什么区别?map 对每个元素做变换,结果包含 nil(如果闭包返回 Optional)。compactMap 会自动过滤掉 nil,只保留非空值。典型场景:字符串数组转 Int——["1", "abc", "3"].compactMap { Int($0) } 得到 [1, 3],用 map 则得到 [Optional(1), nil, Optional(3)]。规则:闭包返回 Optional 时用 compactMap,否则用 map。flatMap 和 compactMap 有什么区别?Swift 4.1 之后,flatMap 的职责简化了:只用来展平嵌套数组([[1,2],[3,4]].flatMap { $0 } 得到 [1,2,3,4])。过滤 nil 的功能全部交给 compactMap。之前 flatMap 两个功能都有,容易混淆,所以拆开了。现在记住:去 nil 用 compactMap,展平用 flatMap。reduce 能做什么 map 和 filter 做不到的事?reduce 把集合聚合成任意类型的单个值——字符串拼接、字典构建、对象累积修改都能做。map 和 filter 只能返回数组。比如统计字符频率:str.reduce(into: [:]) { $0[$1, default: 0] += 1 },这个 map 做不了。另一个例子:把数组转成字典 items.reduce(into: [:]) { $0[$1.id] = $1 }。链式调用 map + filter + reduce 性能怎么样?每次链式调用都创建一个中间数组——map 创建一个,filter 再创建一个。数据量大时这有额外内存和 CPU 开销。如果性能敏感,用 reduce 合并操作,或者用 for 循环一次遍历完成。但大部分场景下,链式调用的可读性收益远大于性能损耗——除非 profile 发现有问题,否则别过早优化。forEach 和 for-in 有什么区别?forEach 不支持 break/continue/return——闭包里的 return 只退出当前闭包,不影响外层。for-in 循环可以用 break 提前退出。所以 forEach 只适合"对每个元素执行操作且必须全部执行"的场景。需要条件退出时用 for-in。另外 forEach 不返回值,不能链式调用。写段代码let numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]// map: 变换let doubled = numbers.map { $0 * 2 }// filter: 筛选let evens = numbers.filter { $0 % 2 == 0 }// reduce: 聚合let sum = numbers.reduce(0, +)// compactMap: 去 nillet strings = ["1", "abc", "3"]let ints = strings.compactMap { Int($0) } // [1, 3]// flatMap: 展平let nested = [[1, 2], [3, 4]]let flat = nested.flatMap { $0 } // [1, 2, 3, 4]// 链式:筛选 + 变换 + 聚合let result = numbers .filter { $0 % 2 == 0 } .map { $0 * $0 } .reduce(0, +) // 4+16+36+64+100 = 220// reduce(into:): 构建字典let freq = "hello".reduce(into: [Character: Int]()) { $0[$1, default: 0] += 1 }
服务端阅读 05月27日 11:29

Swift 结构体和类有什么区别?什么时候用 struct?

结构体(struct)和类(class)的核心区别:struct 是值类型,赋值拷贝;class 是引用类型,赋值传引用。Swift 推荐优先用 struct——更安全(不受其他代码影响)、更简单(没有循环引用)、更高效(栈分配 + 写时复制)。class 能做而 struct 做不了的事:继承、类型转换(is/as)、析构器(deinit)、引用计数(ARC)、共享同一实例。反过来,struct 能做而 class 做不了的事:编译器自动合成 init(class 没有成员初始化器)、不需要手动管理内存、天然线程安全(值隔离)。追问什么时候必须用 class?三种情况:需要继承(UIKit 的视图体系)、需要共享状态(单例、ViewModel 在多个视图间共享)、需要 deinit 清理资源(关闭文件、取消网络请求)。除了这三种,用 struct。struct 的方法为什么默认不能修改属性?struct 是值类型,方法调用时 self 是 let(不可变)。要修改属性必须标记 mutating,这会让编译器在调用时确保变量是 var 而不是 let。class 不需要 mutating——引用类型的方法调用不影响 self 的可变性。mutating 的另一个效果:在 mutating 方法里可以给 self 赋新值,比如 self = Self()。struct 可以实现协议吗?和 class 实现协议有什么区别?都可以实现协议。区别在于:如果协议要求 mutating 方法,struct 实现时必须标 mutating,class 不需要——因为 class 的方法天然可以修改属性。另外,class 实现的协议可以用作 existencial(let x: Protocol = MyClass()),struct 实现的协议在 Swift 5.7 之前有更多限制(有关联类型时不能直接当类型用)。struct 的 init 是怎么合成的?如果没有自定义 init,编译器自动合成成员初始化器:Point(x: 1, y: 2)。一旦你写了自定义 init,成员初始化器就没了。class 没有自动合成的成员初始化器——必须手写 init,或者所有属性都有默认值且不需要传参。Swift 5.10 之后 struct 在有自定义 init 的同时可以保留成员初始化器,用 init(x:y:) 即可。为什么 Apple 在 SwiftUI 里大量用 struct?两个原因:值语义让视图状态隔离——修改一个视图的状态不会意外影响其他视图;struct 没有引用计数开销,视图创建销毁频繁,用 struct 性能更好。SwiftUI 的 View 协议要求是 struct,body 属性每次调用都生成新的值,diff 算法比较新旧值决定是否更新。如果用 class,diff 成本和内存管理都会更复杂。写段代码struct Point { var x: Int, y: Int }class Node { var value: Int = 0; var next: Node? }// 值类型:拷贝隔离var p1 = Point(x: 1, y: 2)var p2 = p1p2.x = 10print(p1.x) // 1// 引用类型:共享实例var n1 = Node(); n1.value = 42var n2 = n1; n2.value = 99print(n1.value) // 99// mutating 修改属性struct Counter { var count = 0 mutating func increment() { count += 1 }}var c = Counter()c.increment()
服务端阅读 05月27日 11:29

Swift 值类型和引用类型有什么区别?什么时候用 struct?

值类型(struct、enum、tuple)赋值时拷贝,每个变量拥有独立的数据副本;引用类型(class、closure)赋值时传递引用,多个变量指向同一个实例。这是 Swift 最根本的类型区分,直接影响内存管理、线程安全和代码行为。值类型的"拷贝"不是每次赋值都真拷贝——Swift 有写时复制(Copy-on-Write)优化,只有修改时才真正创建副本,所以大数组的传参开销没有想象中大。追问值类型和引用类型在内存上有什么区别?值类型通常分配在栈上,分配和释放由编译器管理,速度快。引用类型分配在堆上,由 ARC 管理生命周期,有引用计数的开销。栈上的值类型在函数返回时自动释放,不需要额外的内存管理;堆上的引用类型需要 ARC 追踪引用计数,计数归零时才释放。写时复制(Copy-on-Write)是什么?值类型赋值时不立即拷贝数据,而是和原始值共享底层存储。只有当某一方尝试修改时,才真正创建独立副本。Array、Dictionary、String 等标准库类型都用了这个优化。自定义 struct 要支持 COW,需要手动检测引用计数并在修改时拷贝。什么时候必须用引用类型(class)?三种情况:需要继承和多态、需要共享状态(多个地方修改同一个实例)、需要控制生命周期(deinit 做清理)。UI 控件的 ViewModel、网络请求的 Session、文件句柄——这些场景需要用 class。其他情况优先 struct。值类型一定线程安全吗?值类型在单次赋值/传参时是安全的(因为拷贝隔离),但如果你把值类型放在 class 里作为属性,多线程同时修改就不安全了——值类型的拷贝只发生在赋值时,不是每次访问时。所以"值类型线程安全"的说法不严谨,准确说是"值类型的独立副本之间互不影响"。struct 里嵌套 class 会怎样?struct 仍然是值类型,赋值时 struct 本身被拷贝,但内部的 class 引用不会深拷贝——多个 struct 副本共享同一个 class 实例。这会导致意外的数据共享:修改一个 struct 副本里的 class 属性,其他副本也会受影响。如果需要真正的深拷贝,必须手动实现 Clone 方法或者在 struct 里只放值类型。写段代码struct Point { var x: Int, y: Int } // 值类型class Node { var value: Int = 0 } // 引用类型var p1 = Point(x: 1, y: 2)var p2 = p1 // 拷贝p2.x = 10print(p1.x) // 1 — p1 不受影响var n1 = Node()n1.value = 42var n2 = n1 // 传递引用n2.value = 99print(n1.value) // 99 — n1 受影响,指向同一实例// struct 嵌套 class 的陷阱struct Wrapper { var node = Node() }var w1 = Wrapper()var w2 = w1 // struct 拷贝,但 node 是引用w2.node.value = 100print(w1.node.value) // 100 — 共享同一个 Node 实例!
服务端阅读 05月27日 11:29

Swift Array、Set、Dictionary 有什么区别?怎么选?

Swift 有三种集合类型:Array(有序、可重复)、Set(无序、不可重复)、Dictionary(键值对)。选哪个看需求:需要顺序和索引用 Array,需要去重和集合运算用 Set,需要键值查找用 Dictionary。Array 是最常用的——按下标随机访问 O(1),尾部插入删除 O(1),中间插入删除 O(n)。Set 基于 HashMap,查找/插入/删除都是 O(1),但不能保序。Dictionary 也是 HashMap,键必须可哈希,下标访问返回 Optional(键可能不存在)。追问Set 的集合运算有哪些?实际用在哪?并集(union)、交集(intersection)、差集(subtracting)、对称差集(symmetricDifference)。实际场景:标签系统——"用户A的标签和用户B的标签有多少重叠"用交集,"A有B没有的"用差集,"两人不同时拥有的"用对称差集。Set 比 Array 做这些操作快得多——Array 的 contains 是 O(n),Set 是 O(1)。Dictionary 的下标访问为什么返回 Optional?因为键可能不存在。如果下标直接返回值类型,访问不存在的键就会崩溃。返回 Optional 让你安全处理两种情况:if let value = dict["key"] 安全解包,dict["key"]! 强制解包(确定存在时)。这比 NSDictionary 的返回 AnyObject? 更安全——Swift 的类型系统保证了值的类型。什么时候用 Array,什么时候用 Set?需要顺序或允许重复用 Array。去重或集合运算用 Set。一个常见误区:去重时先 Array 再手动过滤——直接用 Set 初始化就行了:Set(array).sorted()。另外 Set 不保序,如果去重后还要保持原始顺序,得用 reduce 手动构建有序数组,或者用 Swift 5.7+ 的 OrderedCollections 库。集合类型作为函数参数时应该传值还是传引用?Array、Set、Dictionary 都是值类型(struct),传参时默认拷贝。Swift 有写时复制(Copy-on-Write)优化——只有实际修改时才真正拷贝,所以大数组传参不会浪费内存。但如果函数内部需要修改集合并影响调用方,需要用 inout 参数:func append(_ item: Int, to array: inout [Int])。Dictionary 怎么安全地修改值?用下标修改时,键不存在会自动插入 nil 值。用 updateValue(_:forKey:) 返回旧值(Optional),可以知道是更新还是新增。最常见的模式是"有就更新,没有就插入默认值":dict[key, default: 0] += 1——比先判断再操作简洁得多。写段代码// Array 基本操作var nums = [3, 1, 4, 1, 5]nums.sort() // [1, 1, 3, 4, 5]nums.append(9) // 尾部插入 O(1)// Set 去重和集合运算let a: Set = [1, 2, 3, 4]let b: Set = [3, 4, 5, 6]a.intersection(b) // {3, 4}a.symmetricDifference(b) // {1, 2, 5, 6}// Dictionary 安全操作var freq: [String: Int] = [:]freq["apple", default: 0] += 1 // 安全递增,不存在时默认0freq.updateValue(5, forKey: "banana") // 返回旧值// 去重但保持顺序let items = [3, 1, 4, 1, 5, 3]var seen = Set<Int>()let unique = items.filter { seen.insert($0).inserted }
服务端阅读 05月27日 11:28

Swift 访问控制有哪些级别?open 和 public 有什么区别?

Swift 有五个访问级别,从宽松到严格:open > public > internal > fileprivate > private。internal 是默认值——不写访问修饰符就是 internal。核心区别在于两个维度:谁能访问(同模块/跨模块/同文件/同作用域),和能不能继承重写(只有 open 允许跨模块继承和重写)。| 级别 | 同模块 | 跨模块 | 可继承重写 ||------|--------|--------|------------|| open | ✅ | ✅ | ✅(跨模块) || public | ✅ | ✅ | ❌(跨模块不可重写) || internal | ✅ | ❌ | ✅(同模块内) || fileprivate | 同文件 | ❌ | 同文件内 || private | 同作用域 | ❌ | 同作用域内 |open 和 public 的唯一区别:open 允许跨模块继承和重写,public 不允许。框架对外暴露的基类用 open,工具类/辅助类用 public。追问open 和 public 有什么区别?什么时候用 open?open 允许其他模块继承类和重写方法,public 只允许访问不允许继承重写。用 open 的场景:你设计一个框架,希望使用者能继承你的基类来定制行为——UIKit 的 UIViewController 就是 open 的。用 public 的场景:功能完整的类,不希望被子类化——比如工具类、配置类。fileprivate 和 private 有什么区别?private 限制在定义的作用域内——类里的 private 属性,类的方法能访问,但扩展(同文件)不能访问(Swift 4 之前)。fileprivate 限制在定义的文件内——同文件的所有类型和扩展都能访问。Swift 4 之后 private 在同文件的扩展里也能访问了,所以 fileprivate 的用武之地变少了。如果多个类型需要共享某个属性或方法,放在同文件里用 fileprivate。默认访问级别是什么?为什么不写修饰符就是 internal?Swift 的哲学是"模块即边界"——大部分代码只在模块内部使用,不需要暴露给外部。internal 正好对应这个边界:模块内可见,模块外不可见。不写修饰符就是 internal,减少了大量样板代码。只有明确需要跨模块的 API 才需要写 public 或 open。子类的访问级别可以比父类更严格吗?可以。子类可以把父类的 open 方法重写为 public,但不能反过来——父类是 public 的方法,子类不能重写为 open。规则是"子类的访问级别不能比父类更宽松",但重写时可以收紧。属性也一样——父类 public var,子类可以重写为 internal var。访问控制和泛型有什么交互?泛型类型的访问级别取决于类型本身和泛型参数中最严格的那个。public class Container<T: PrivateProtocol> 编译不过——public 类型不能依赖 private 协议。函数同理:public func process(_ item: InternalType) 也编译不过。规则是"实体的访问级别不能比它依赖的类型更高"。写段代码// 框架对外 APIopen class BaseService { open func execute() { } // 允许跨模块重写 public func validate() { } // 允许跨模块调用,但不允许重写}// 模块内部实现class InternalHelper { // 默认 internal fileprivate func assist() { } // 同文件可访问 private var state: Int = 0 // 同作用域可访问}// 子类收紧访问级别class MyService: BaseService { public override func execute() { } // ✅ 收紧:open → public}
服务端阅读 05月27日 11:27

Swift 下标 subscript 怎么自定义?和多参数下标

下标(subscript)让你用 instance[index] 的方式访问值,不需要调方法。Array、Dictionary 的 [] 访问就是下标实现的。自定义类型也可以定义下标,用 subscript 关键字。下标本质是 getter + setter 的语法糖,和计算属性类似——可以只读也可以读写,可以有多个参数。区别是下标用 [] 调用,属性用 . 调用;下标可以接受参数,属性不能。追问下标可以接受多个参数吗?可以,参数数量和类型没有限制。典型用法是二维数组用 matrix[row, col] 访问。也可以用不同类型的参数重载下标——比如字典同时支持 dict["key"] 和 dict[0]。参数还可以有默认值和可变参数。下标和方法的区别?什么时候该用下标?下标是访问语义——"取一个值"或"设一个值"。方法是操作语义——"执行一个动作"。如果一个操作的核心含义是"按索引取值",用下标;如果是"做某件事",用方法。字典的 dict["key"] 是下标(取值),dict.removeValue(forKey:) 是方法(执行删除)。别滥用下标——复杂逻辑用方法更清晰。类型下标是什么?用 static subscript 定义,通过类型名直接调用:SomeType[index],不需要实例。枚举常用类型下标——比如 Planet[4] 返回第四颗行星。类型下标不能是实例下标的重载(参数签名相同时会冲突)。下标能抛出错误吗?能。Swift 5.2 开始下标可以标记 throws,调用时需要 try。不过实际项目中很少用——下标调用期望是简单的取值操作,抛出错误会让调用方写一堆 try-catch,违背了下标的简洁语义。索引越界通常用返回 Optional(dict[key] 返回 Value?)或直接 crash(array[index])来处理,而不是抛错误。写段代码struct Matrix { let rows: Int, cols: Int var grid: [Double] init(rows: Int, cols: Int) { self.rows = rows; self.cols = cols self.grid = Array(repeating: 0.0, count: rows * cols) } subscript(row: Int, col: Int) -> Double { get { grid[row * cols + col] } set { grid[row * cols + col] = newValue } }}var m = Matrix(rows: 3, cols: 3)m[0, 0] = 1.0m[1, 1] = 2.0print(m[0, 0]) // 1.0// 类型下标enum Planet: Int { case mercury = 1, venus, earth, mars static subscript(n: Int) -> Planet? { Planet(rawValue: n) }}print(Planet[3]!) // earth
服务端阅读 05月27日 11:27

Swift 协议是什么?协议扩展和面向协议编程怎么理解?

协议定义了一组属性和方法的蓝图,任何类型都可以遵循协议来实现这些要求。Swift 的协议比 Java 的接口更强——它支持默认实现(通过协议扩展)、协议组合、关联类型,是面向协议编程(POP)的核心。协议本身不实现功能,只定义"需要什么"。遵循协议的类型必须提供具体实现。协议可以作为类型使用——函数参数、变量、集合元素都可以是协议类型,实现多态。追问协议扩展的默认实现有什么用?协议扩展可以为协议方法提供默认实现,遵循协议的类型如果不自己实现,就自动用默认的。这解决了两个问题:一是不需要每个遵循类型都写一遍相同逻辑;二是可以给协议添加新方法而不破坏已有的遵循类型。标准库里大量用了这个特性——Collection 协议的 map、filter 都是协议扩展提供的默认实现。协议组合是什么?和继承有什么区别?用 ProtocolA & ProtocolB 把多个协议组合成一个临时类型,不需要定义新的协议。和继承的区别:组合是"有这些能力",继承是"是一种"。一个类型可以同时遵循任意多个协议(组合),但只能继承一个父类(单继承)。实际开发中,优先用协议组合代替继承——更灵活,耦合更低。关联类型是什么?什么时候用?协议里用 associatedtype 声明一个占位类型,由遵循协议的具体类型来确定。比如 Collection 协议的 Element 就是关联类型——Array 的 Element 是具体类型,Dictionary 的 Element 是键值对。有关联类型的协议不能直接当类型用(不能用 let x: SomeProtocol),需要用泛型约束 some SomeProtocol 或 any SomeProtocol。any 和 some 关键字有什么区别?any Protocol 是存在类型(existential),运行时可以是任何遵循协议的类型,有动态派发开销。some Protocol 是不透明类型,编译期确定具体类型,性能更好。Swift 5.7 开始,协议类型的变量必须显式写 any,否则编译器警告。函数返回值用 some 可以隐藏具体类型同时保证性能。面向协议编程(POP)比 OOP 好在哪?POP 用协议+扩展代替继承,解决了 OOP 的几个痛点:单继承限制(协议可以组合多个)、脆弱基类问题(协议扩展不依赖继承链)、强耦合(协议只定义接口,不绑定实现)。Swift 标准库本身就是面向协议设计的——Array 遵循 Collection 协议获得几十个方法,而不是继承自某个基类。写段代码protocol Drawable { func draw()}// 协议扩展提供默认实现extension Drawable { func draw() { print("默认绘制") }}protocol Scalable { var scale: CGFloat { get set } mutating func resize(to scale: CGFloat)}// 协议组合func render(_ item: some Drawable & Scalable) { item.draw()}// 关联类型protocol Container { associatedtype Item var count: Int { get } mutating func append(_ item: Item) subscript(i: Int) -> Item { get }}struct Stack<Element>: Container { private var items: [Element] = [] var count: Int { items.count } mutating func append(_ item: Element) { items.append(item) } subscript(i: Int) -> Element { items[i] }}
服务端阅读 05月27日 11:26

Swift 属性包装器 @propertyWrapper 怎么用?有什么局限?

属性包装器 @propertyWrapper 把属性的 get/set 逻辑抽出来封装成可复用的类型。比如你有一堆属性都需要做范围限制、线程安全、UserDefaults 存取——不写包装器的话,每个属性的 getter/setter 里都要写一遍相同的逻辑。属性包装器的核心是 wrappedValue:它替代了原始属性的存取逻辑。外部访问属性时,实际访问的是包装器的 wrappedValue。包装器还可以通过 projectedValue(用 $ 前缀访问)提供额外功能,比如标记值是否被裁剪过。@propertyWrapperstruct Clamped<Value: Comparable> { var value: Value let range: ClosedRange<Value> var wrappedValue: Value { get { value } set { value = min(max(newValue, range.lowerBound), range.upperBound) } } var projectedValue: Bool { value != value // 是否被裁剪 }}struct Player { @Clamped(range: 0...100) var health: Int = 100}追问属性包装器的 wrappedValue 和 projectedValue 有什么区别?wrappedValue 是属性的值本身,通过属性名直接访问。projectedValue 是包装器暴露的额外信息,通过 $ 前缀访问。比如 @UserDefault 的 wrappedValue 是存储的值,$someKey 可以返回一个 Publisher 用于响应式监听。projectedValue 不是必须的,大部分简单包装器只实现 wrappedValue。属性包装器能替代 willSet/didSet 吗?能,而且更灵活。willSet/didSet 是写在属性定义里的,每个属性重复一遍;属性包装器把同样的逻辑封装成类型,多个属性复用。但属性包装器比 willSet/didSet 复杂得多——如果只是简单的值变化通知,willSet/didSet 更直接。需要复用逻辑时才用包装器。SwiftUI 里常用的属性包装器有哪些?各自的作用?@State:值类型本地状态,视图独占@Binding:父子的双向数据绑定@ObservedObject:外部传入的引用类型观察对象@StateObject:视图自己创建和持有的观察对象@EnvironmentObject:从环境注入的共享对象@Published:在 ObservableObject 里标记属性变化时自动通知这些本质上都是属性包装器,但 SwiftUI 框架为它们注入了特殊的依赖追踪和视图刷新机制。属性包装器有什么局限?不能包装 lazy 属性和带观察器的属性——它们对存储方式有特殊要求,和包装器冲突。包装器的 init 不能访问 enclosing instance(所在类型的实例),所以包装器内部没法直接调用所在类型的方法。另外,包装器增加了间接调用层级,极端性能场景下可能有微小的额外开销。写段代码@propertyWrapperstruct UserDefault<T> { let key: String let defaultValue: T var wrappedValue: T { get { UserDefaults.standard.object(forKey: key) as? T ?? defaultValue } set { UserDefaults.standard.set(newValue, forKey: key) } }}struct Settings { @UserDefault(key: "darkMode", defaultValue: false) var darkMode: Bool @UserDefault(key: "fontSize", defaultValue: 14) var fontSize: Int}var settings = Settings()settings.darkMode = true // 自动写入 UserDefaultsprint(settings.darkMode) // 自动从 UserDefaults 读取
服务端阅读 05月27日 11:26

Swift 属性观察器怎么用?willSet 和 didSet 有什么区别?

属性观察器 willSet 和 didSet 在属性值变化时自动触发,让你在赋值前后插入自定义逻辑。willSet 在新值存储前调用,通过 newValue 访问即将写入的值;didSet 在新值存储后调用,通过 oldValue 访问替换前的值。核心规则:willSet 拿到的是 newValue(还没写进去),didSet 拿到的是 oldValue(已经被替换掉),didSet 里直接读属性拿到的是新值。两个观察器不需要同时写,按需选择。初始化器中设置属性值不会触发观察器——包括 init 里的赋值和设置默认值。这是因为对象还没构造完成,观察器依赖的状态可能不完整。延迟属性(lazy)和计算属性也不能有观察器——lazy 的初始化时机不确定,计算属性没有存储过程。追问willSet 和 didSet 的执行顺序是什么?willSet 先执行,新值存储,然后 didSet 执行。在 willSet 里不能修改即将写入的值——newValue 是只读的。在 didSet 里可以修改属性值,但会再次触发 didSet,Swift 用了保护机制避免无限循环——didSet 里对自身的赋值不会再触发观察器。didSet 里修改属性值有什么坑?didSet 里给属性赋新值是合法的,常见做法是值校验后回退:if age < 0 { age = 0 }。但要注意,这个赋值不会再次触发 didSet(Swift 防止无限递归),所以你的回退逻辑只执行一次。另外,didSet 里的赋值会覆盖刚写入的值——如果你在 didSet 里又赋了别的值,外部看到的是 didSet 里赋的那个。属性观察器和计算属性的 setter 有什么区别?计算属性没有存储空间,setter 里必须自己决定怎么存(通常写到另一个私有属性里)。属性观察器附加在存储属性上,值的存储是自动的,你只是在存储前后加逻辑。选择标准:需要自定义存储方式用计算属性,只想在值变化时做额外操作用观察器。父类和子类都有 didSet,执行顺序是什么?子类 willSet → 父类 willSet → 新值存储 → 父类 didSet → 子类 didSet。和 init 不同,属性观察器在继承链中会逐级触发。如果子类重写了父类的属性并添加观察器,父类的观察器也会执行——即使父类不知道子类的存在。什么场景下必须用 willSet 而不是 didSet?willSet 的典型场景是"在新值写入前做校验或准备"。比如你需要在新值写入前记录日志(记录即将变成什么),或者需要根据新旧值的差异提前通知观察者。实际上大部分场景用 didSet 就够了——willSet 用得少得多。写段代码class StepCounter { var totalSteps: Int = 0 { willSet { print("步骤将从 \(totalSteps) 变为 \(newValue)") } didSet { if totalSteps > oldValue { print("新增 \(totalSteps - oldValue) 步") } } }}let counter = StepCounter()counter.totalSteps = 100 // 触发 willSet + didSetcounter.totalSteps = 150 // 触发 willSet + didSet// didSet 校验回退class Person { var age: Int = 0 { didSet { if age < 0 { age = 0 } } }}