面试题手册

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

服务端阅读 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 } } }}
服务端阅读 05月27日 10:43

Swift 类型转换怎么做?is、as、as?、as! 有什么区别?

Swift 用 is、as、as?、as! 四个操作符做类型检查和转换。is 检查"是不是这个类型",返回 Bool;as 向上转型(子类→父类),永远成功,编译器自动推断时甚至不用写;as? 向下转型(父类→子类),成功返回可选值,失败返回 nil,安全;as! 强制向下转型,失败直接 crash,危险。核心规则:向上转型用 as(安全,编译器保证),向下转型优先 as?(安全,失败返回 nil),只在逻辑上100%确定时才用 as!。实际开发中 as? 用得最多——处理 JSON 解析、UITableViewCell 复用、协议类型的向下转型,几乎都是 as?。Any 和 AnyObject 是 Swift 的两种"万能类型":Any 可以表示任何类型(包括值类型和引用类型),AnyObject 只能表示类类型。类型转换在处理 [Any] 数组时最常见——网络请求返回的 JSON 经常被解析为 [String: Any],需要逐个 as? 转换。追问as、as?、as! 分别在什么场景用?as:向上转型(子类→父类)和桥接 Objective-C 类型(String as NSString),编译器保证安全。as?:向下转型不确定是否成功时,比如 JSON 解析 value as? String、UITableViewCell 复用 cell as? CustomCell,日常开发最常用。as!:逻辑上100%确定类型时,比如 storyboard 里已知 cell 类型,或者单元测试中断言类型。和 try! 一样,as! 基本只在"失败就是 bug"的场景用。Any 和 AnyObject 有什么区别?Any 可以表示任何类型——class、struct、enum、闭包、元组都行。AnyObject 只能表示类类型,本质是 Objective-C 的 id 类型。从 Objective-C 桥接过来的 API 返回 AnyObject,纯 Swift 代码用 Any。实际项目中尽量少用这两个——频繁用 Any 说明设计有问题,应该用协议或泛型代替。protocol 类型的 as? 转型有什么坑?把协议类型向下转型为具体类型时,只有协议标记了 @objc 或者是 class-only 协议才能 as?。纯 Swift 协议(没有 class 约束)的 existential 不能直接 as? 转为具体类型——编译器会报错。解法:把协议改为 @objc protocol,或者用泛型/关联类型替代类型转换。为什么 Swift 不推荐频繁用类型转换?频繁用 is/as? 说明你在绕过类型系统,通常意味着设计有问题。正确做法是用多态——定义协议方法让子类各自实现,调用方不需要知道具体类型。如果代码里一堆 if let x = y as? A { ... } else if let x = y as? B { ... },应该重构为协议 + 多态调用。写段代码class MediaItem { var name: String; init(name: String) { self.name = name } }class Movie: MediaItem { var director: String; init(name: String, director: String) { self.director = director; super.init(name: name) } }class Song: MediaItem { var artist: String; init(name: String, artist: String) { self.artist = artist; super.init(name: name) } }let library: [MediaItem] = [ Movie(name: "Casablanca", director: "Curtiz"), Song(name: "Blue Suede Shoes", artist: "Elvis"), Movie(name: "Citizen Kane", director: "Welles")]// as? 安全转型for item in library { if let movie = item as? Movie { print("Movie: \(movie.name) by \(movie.director)") } else if let song = item as? Song { print("Song: \(song.name) by \(song.artist)") }}// switch + is/asfor item in library { switch item { case is Movie: print("\(item.name) is a movie") case let song as Song: print("\(song.name) by \(song.artist)") default: break }}
服务端阅读 05月27日 10:42

Swift Result 类型怎么用?和 throws 有什么区别?

Result 是 Swift 5 引入的枚举,用两个 case 表示操作结果:.success 携带成功值,.failure 携带错误。它把错误从控制流(do-catch)变成了值——你可以像处理普通枚举一样 switch 它、map 它、存起来以后再处理。Result 最大的价值在异步回调。completion handler 里没法用 throws 传播错误(因为回调函数本身不是 throws 的),而 Result 作为返回值天然适合这种场景:func fetch(id: String, completion: @escaping (Result<User, Error>) -> Void)。async/await 出现后,新代码直接 async throws 更清晰,Result 主要用于兼容旧接口。Result 的常用方法:map 转换成功值(失败原样传递),flatMap 链式转换(返回新的 Result),get 把 Result 变回 throws(try result.get() 可以在 do-catch 中使用)。Result { try someThrowingFunc() } 初始化器可以方便地把 throws 函数包装成 Result。追问Result 和 throws 怎么选?同步代码用 throws + do-catch 更直观,也是 Swift 惯用风格。异步回调用 Result,因为 completion handler 里的 throws 外层 catch 不到。async/await 之后,新代码统一用 async throws,Result 退居兼容层。简单说:能 throws 就 throws,必须回调就用 Result。Result 的 map 和 flatMap 有什么区别?map 接收 (Success) -> NewValue 闭包,只转换成功值,失败原样传递,返回 Result。flatMap 接收 (Success) -> Result,整个 Result 替换,适合链式调用另一个可能失败的操作。类比:map 是"成功的话转换一下",flatMap 是"成功的话再做一次可能失败的操作"。Result 的 Failure 类型有什么坑?泛型约束 Failure: Error,但不同 Result 的 Failure 类型不同时不能直接组合。比如 Result 和 Result 做 flatMap 链会报类型不匹配。解法一:统一用 Error 作为 Failure 类型(牺牲类型精确性);解法二:用 mapError 统一错误类型;解法三:自定义 AppError 枚举把所有错误 case 收拢。Result 怎么和 async/await 配合?用 try await someAsyncFunc() 直接替代 Result 回调。如果必须兼容旧的 Result 接口,可以用 let result = await Result { try await someFunc() } 包装。反过来,把 Result 回调转 async/await 可以用 withCheckedContinuation 桥接。Result 和 Optional 有什么区别?什么时候用哪个?Optional 表示"有值或没有",没有失败原因;Result 表示"成功或失败",失败时携带错误信息。读配置文件、查缓存——只关心有没有值,用 Optional;网络请求、文件读写——需要知道失败原因,用 Result。try? 可以把 throws 函数降级为 Optional,但丢失了错误详情。写段代码enum APIError: Error { case invalidURL case badStatus(Int) case decodingFailed}func fetchUser(id: String) -> Result<User, APIError> { guard let url = URL(string: "https://api.example.com/users/\(id)") else { return .failure(.invalidURL) } return .success(User(name: "Test"))}// 链式调用let result = fetchUser(id: "42") .map { $0.name } .flatMap { name in fetchUser(id: name) }// Result 转 throwsdo { let user = try fetchUser(id: "42").get()} catch { print(error)}// 包装 throws 为 Resultlet result2 = Result { try JSONDecoder().decode(User.self, from: data) }
服务端阅读 05月27日 10:40

Swift 内存管理怎么做?ARC 和循环引用详解

Swift 用 ARC(自动引用计数)管理内存。每次创建类实例,ARC 分配内存并将引用计数置为 1;每多一个强引用指向它,计数 +1;引用离开作用域或被赋新值,计数 -1;计数归零,ARC 立刻释放内存。ARC 只管引用类型(class),值类型(struct/enum)不存在引用计数。循环引用是 ARC 最大的坑:两个实例互相强引用,计数永远不归零,内存泄漏。两种经典场景——类属性互相引用,和闭包捕获 self。类属性互引用的解法:把一边改成 weak 或 unowned。weak 必须是可选型 var,引用的对象释放后自动变 nil,安全;unowned 不是可选型,对象释放后访问会 crash,但性能更好。选哪个看生命周期——如果被引用对象可能先死,用 weak;如果确定被引用对象活得比自己久,用 unowned。闭包捕获 self 导致循环引用更隐蔽:闭包强引用了 self,self 又持有闭包。解法是捕获列表 [weak self] 或 [unowned self],在闭包内用 guard let self = self 解包。实际项目中 90% 用 weak self,因为闭包执行时 self 可能已释放。追问weak 和 unowned 有什么区别?怎么选?weak 修饰的属性在对象释放后自动置 nil,必须是可选型 var,访问安全;unowned 不会自动置 nil,对象释放后访问触发野指针 crash。选择标准:被引用对象可能先于自己释放用 weak(比如 delegate 模式),确定对方活得比自己久用 unowned(比如 Customer 持有 CreditCard,CreditCard 无主引用 Customer)。拿不准就用 weak,安全第一。weak 引用的底层实现是什么?Runtime 维护一张 weak 表(哈希表),key 是对象地址,value 是指向该对象的所有 weak 指针数组。对象 dealloc 时,Runtime 遍历 weak 表找到对应的指针数组,逐个置 nil,然后从表中删除。这也是为什么 weak 访问需要加锁——多线程可能同时在读 weak 指针和修改 weak 表。weak-strong dance 是什么?为什么需要?闭包中 [weak self] 后,self 变成可选型,每次用都要解包。如果闭包执行期间 self 被释放,中途解包失败会导致逻辑断裂。weak-strong dance 的做法:在闭包开头 guard let self = self else { return },把 weak 引用提升为局部强引用。这样闭包执行期间 self 不会被释放,逻辑连贯,执行结束后局部强引用消失,不影响释放。怎么检测循环引用?Xcode 的 Memory Graph(Debug Memory Graph 按钮)可以直接看对象引用关系,找到循环引用链。Instruments 的 Leaks 工具可以自动检测泄漏,切换到 Cycles & Roots 视图能看到循环引用的图形化展示。日常开发中,在 deinit 里打个 log,如果控制器退出后没触发,大概率有循环引用。ARC 和 MRC 有什么区别?ARC 做了什么优化?MRC 手动写 retain/release/autorelease,ARC 由编译器自动插入这些调用。ARC 的优化:编译器会省略不必要的 retain/release——比如函数返回值直接传递给调用方,中间不需要 retain 再 release(快速路径优化)。ARC 不等于垃圾回收,它在编译期确定,没有运行时停顿。写段代码class ViewController: UIViewController { var onComplete: (() -> Void)? func setup() { // 循环引用:闭包捕获 self,self 持有闭包 onComplete = { self.dismiss(animated: true) // ⚠️ 循环引用 } // 解法:weak self + guard onComplete = { [weak self] in guard let self = self else { return } self.dismiss(animated: true) // ✅ 安全 } }}// delegate 用 weak 防止循环引用class ListView: UIView { weak var delegate: ListViewDelegate?}
服务端阅读 05月27日 10:38

Swift 错误处理怎么做?throws、try、catch 详解

Swift 的错误处理核心就四个关键字:Error 定义错误、throws 声明可能失败、throw 抛出错误、try+do-catch 捕获处理。整个流程编译器强制检查——throws 函数必须被 try 调用,try 必须在 do-catch 或另一个 throws 函数中,不可能"忘了处理错误"。这点和 Java 的 checked exception 思路类似,但 Swift 更轻量。try 三种形式各有用途:try(标准,需 do-catch)、try?(失败返回 nil,丢弃错误详情)、try!(断言不会失败,否则 crash)。日常 80% 场景用 try? 就够了——只关心成功还是失败,不需要区分原因。需要根据错误类型走不同分支时才用完整 do-catch。try! 只在逻辑上不可能失败的路径或单元测试中用。错误沿调用链自动向上传播:A throws → B 调用 A 也必须 throws → C 调用 B 也必须处理,直到某个调用方用 do-catch 消化。传播链编译器强制检查,不存在运行时才发现错误没处理的情况。defer 无论是否抛错都执行,用于资源清理,多个 defer 按栈序(后进先出)执行。rethrows 专用于高阶函数——函数本身不抛错,但传入闭包如果是 throws 的,调用方也必须 try。标准库 map、filter、forEach 都用了 rethrows:传普通闭包不强制 try,传 throws 闭包才需要。Swift 5 的 Result 把错误从控制流变成值,可以像处理枚举一样处理成功和失败,特别适合异步回调。async/await 普及后新代码用 async throws 更直观,Result 主要兼容旧接口。Swift 错误处理不是传统"异常"——不会 unwinding 调用栈,happy path 几乎零开销。追问try? 和 try! 有什么区别?什么场景用哪个?try? 失败返回 nil;try! 失败直接 crash。绝大多数场景用 try?,比如读配置文件读不到就用默认值。try! 只在逻辑上不可能失败时用,比如解析 Bundle 内的 JSON——这都能失败说明资源缺失,是开发阶段就该暴露的 bug,不应静默处理。rethrows 和 throws 有什么区别?throws 表示函数自身会抛错;rethrows 表示函数本身不抛错,但透传闭包参数的错误。关键区别:rethrows 函数传入不抛错的闭包时,调用方不需要 try。Array.map 用 rethrows,所以 arr.map { $0 + 1 } 不需要 try,arr.map { try throwingFunc($0) } 才需要。Result 和 do-catch 怎么选?同步用 do-catch;异步回调用 Result,因为 completion handler 里抛出的错误外层 catch 不到。async/await 之后新代码直接 async throws 更清晰。Result(catching: { try func() }) 可以把 throws 函数包装成 Result,方便在回调中传递。defer 有什么常见坑?多个 defer 后进先出。坑一:在循环里写 defer 以为每次迭代都执行,实际等函数作用域结束。坑二:defer 不等异步操作完成,需要异步清理得用其他机制。正确做法:打开资源后立刻写 defer 关闭。Swift 错误处理和 Java 异常有什么本质区别?Swift 的错误不会 unwinding 调用栈,happy path 零开销。Java checked 异常强制 catch 或声明,代码侵入性强;Swift 更轻量,try? 提供了快速降级路径。Swift 没有 finally,用 defer 替代,且 defer 不限于 do-catch 块,任何作用域都能用。写段代码enum APIError: Error { case invalidURL case badStatus(Int)}func fetchUser(id: String) throws -> User { guard let url = URL(string: "https://api.example.com/users/\(id)") else { throw APIError.invalidURL } let (data, resp) = try URLSession.shared.data(from: url) guard let http = resp as? HTTPURLResponse, http.statusCode == 200 else { throw APIError.badStatus((resp as? HTTPURLResponse)?.statusCode ?? -1) } return try JSONDecoder().decode(User.self, from: data)}do { let user = try fetchUser(id: "42")} catch APIError.badStatus(let code) { print("HTTP \(code)")} catch { print(error)}let maybe = try? fetchUser(id: "42") // User?