面试题手册

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

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

Swift 字符串怎么拼接、截取和替换?常用方法详解

Swift 的 String 是值类型,基于 Unicode 标量构建。拼接用 + 和插值 \(),截取用 prefix/suffix/dropFirst/dropLast,替换用 replacingOccurrences,查找用 contains/hasPrefix/hasSuffix。拼接最常用字符串插值:"Hello, \(name)",比 + 拼接更灵活,支持表达式。截取要注意 Swift 的 String 不能用整数下标——因为 Unicode 字符长度不等,必须用 String.Index。str.index(str.startIndex, offsetBy: 5) 取偏移位置,str[first..<last] 取子串。替换支持正则:str.replacingOccurrences(of: "[0-9]", with: "*", options: .regularExpression)。查找除了 contains/hasPrefix/hasSuffix,还可以用 range(of:) 获取位置,ranges(of:) 获取所有匹配位置。追问为什么 Swift 的 String 不能用整数下标?Swift 的 String 是 Unicode 正确的——一个"字符"可能由多个 Unicode 标量组成(如 emoji 👨‍👩‍👧‍👦 由 4 个标量组成)。用整数下标意味着 O(n) 遍历找到位置,所以 Swift 干脆不允许,强制用 String.Index。如果确实需要按下标访问,先转成 Array 再用整数索引。Substring 和 String 有什么区别?String 切片得到的是 Substring,它和原 String 共享底层内存(写时复制),不会立即拷贝。Substring 适合临时使用,长期持有应该转成 String(String(substring)),否则原字符串的内存无法释放。函数返回值推荐用 String,内部临时操作用 Substring 省内存。如何高效拼接大量字符串?用 + 或插值在循环里拼接,每次都创建新字符串,O(n²) 复杂度。高效做法是用 joined():array.joined(separator: ",") 一次完成拼接,或者用 String 的 write(to:) 流式写入。少量拼接用插值就行,别过早优化。Swift 字符串比较是按什么规则?== 比较的是 Unicode 标量是否相同,不是字节相同。"é" 可以是一个标量(U+00E9)也可以是两个(e + 组合重音 U+0301),它们视觉一样但 == 返回 false。如果需要规范比较,用 str.precomposedStringWithCanonicalMapping 先标准化再比较。写段代码let str = "Hello, Swift!"// 拼接let greeting = "Hello, \(name)"// 截取str.prefix(5) // "Hello"str.suffix(6) // "Swift!"str.dropFirst(7) // "Swift!"// 替换(支持正则)str.replacingOccurrences(of: "Swift", with: "World")str.replacingOccurrences(of: "[aeiou]", with: "*", options: .regularExpression)// 查找str.contains("Swift") // truestr.hasPrefix("Hello") // truestr.range(of: "Swift") // Range<String.Index>?// 索引访问let idx = str.index(str.startIndex, offsetBy: 7)str[idx...] // "Swift!"
服务端阅读 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 } } }}
计算机基础阅读 05月27日 10:50

XML 中的 CDATA 是什么?什么时候需要用 CDATA?

CDATA(Character Data)是 XML 里的一个特殊标记,告诉解析器"这段内容别解析,原样保留"。当你需要在 XML 中放代码、HTML 片段或包含大量 <、>、& 的文本时,CDATA 省去逐个转义的麻烦。基本语法<code> <![CDATA[ if (x < 10 && y > 5) { return "ok"; } ]]></code><![CDATA[ 和 ]]> 之间的内容,XML 解析器不会尝试解析标签或实体引用,全部当作原始文本处理。什么时候需要 CDATA嵌入代码:JavaScript、SQL、CSS 里大量使用 <、>、&&,不用 CDATA 就得写成 <、>、&&,可读性极差。<script> <![CDATA[ function check() { if (count < 10 && status === "active") { return true; } } ]]></script>嵌入 HTML 片段:RSS feed 里经常包含 HTML 内容,CDATA 是标准做法。<description> <![CDATA[ <p>这是一段<strong>HTML</strong>内容</p> ]]></description>嵌入 SQL 查询:MyBatis、Hibernate 的 XML 映射文件里写 SQL,比较运算符必须转义或用 CDATA。<select id="findActive"> <![CDATA[ SELECT * FROM users WHERE age > 18 AND score >= 60 ]]></select>CDATA 的限制不能嵌套:CDATA 内部不能出现 ]]>,因为解析器会把第一个 ]]> 当作 CDATA 结束标记。如果内容里确实需要 ]]>,得拆成两个 CDATA 节:]]]><![CDATA[>。大小写敏感:必须是 CDATA,写成 cdata 或 Cdata 都不对。空白保留:CDATA 里的换行和缩进会原样保留,包括你不想保留的。格式化 XML 时注意别误改 CDATA 内的空白。不能做部分转义:CDATA 是全有或全无的——整个内容都不解析。如果只需要转义个别字符,用实体引用 < > 更精确。CDATA vs 实体引用| 特性 | CDATA | 实体引用 ||------|-------|----------|| 语法 | <![CDATA[...]]> | < > & || 适用范围 | 大段文本 | 单个字符 || 可读性 | 高,原文可读 | 低,需要还原 || 灵活性 | 低,整个块不解析 | 高,精确控制 |经验法则:超过 3 个特殊字符就用 CDATA,少于 3 个用实体引用。常见误区CDATA 不是数据类型:CDATA 只是告诉解析器别解析,它不改变数据的含义。解析后 <![CDATA[hello]]> 和 hello 是等价的——应用程序拿到的是同样的字符串。CDATA 不影响验证:XSD 验证时,CDATA 内的内容同样会被检查是否符合类型约束。CDATA 只跳过解析,不跳过验证。浏览器中的 CDATA:XHTML 里曾经用 //<![CDATA[ 包裹 JavaScript,但 HTML5 不需要——<script> 标签的内容本身就不被当作 XML 解析。这个用法已经过时了。
计算机基础阅读 05月27日 10:49

XML 和 JSON 有什么区别?什么时候该用 XML?

XML 和 JSON 是两种最常用的数据交换格式,但它们的定位不同:XML 是标记语言,擅长表达文档结构;JSON 是数据格式,擅长表达结构化数据。现代 Web 开发 90% 的场景用 JSON,但 XML 在特定领域仍然不可替代。核心区别| 特性 | XML | JSON ||------|-----|------|| 定位 | 标记语言,面向文档 | 数据格式,面向数据 || 语法 | 标签闭合 <name>值</name> | 键值对 "name": "值" || 数据类型 | 无内置类型,都是字符串 | string、number、boolean、null、array、object || 注释 | 支持 <!-- --> | 不支持 || 命名空间 | 支持,避免标签冲突 | 不支持 || 验证 | DTD / XSD 成熟方案 | JSON Schema(较新,工具链不完善) || 冗余度 | 高(开闭标签重复) | 低 || 解析速度 | 慢(DOM/SAX) | 快(原生支持) |一句话概括:XML 能做文档,JSON 只能做数据;JSON 传输快,XML 验证强。什么时候必须用 XML包含文档内容:XML 的标签能表达语义和层级(标题、段落、列表),JSON 的键值对做不到。Office 文档(docx、xlsx)底层是 XML,RSS/Atom feed 也是 XML——这些场景需要混合内容和结构。需要严格验证:XSD 可以定义精确的类型约束(值范围、正则模式、枚举),JSON Schema 功能弱得多。金融、医疗、政府数据交换标准(如 HL7、FHIR 的 XML 格式)依赖 XSD 验证。命名空间:多个词汇表组合时,命名空间避免标签冲突。SOAP、XHTML、SVG 都用命名空间。JSON 没有这个能力。遗留系统集成:企业里大量旧系统只认 XML。SAP、Oracle、银行接口——你不想用也得用。JSON 的优势场景Web API:RESTful API 用 JSON 是事实标准。体积小、解析快、前端原生支持,没有理由用 XML。配置文件:package.json、tsconfig.json、.eslintrc——开发工具链已经全面倒向 JSON(以及 JSON 超集如 JSON5、YAML)。移动端和低带宽场景:JSON 比 XML 小 30-50%,解析快 2-3 倍。移动网络下这个差距很实际。NoSQL 数据库:MongoDB、CouchDB 存储 JSON 文档,查询天然适配。同一数据的格式对比<!-- XML --><book id="1" category="web"> <title>XML Guide</title> <price>39.95</price> <tags> <tag>XML</tag> <tag>Programming</tag> </tags></book>{ "id": 1, "category": "web", "title": "XML Guide", "price": 39.95, "tags": ["XML", "Programming"]}XML 版本 156 字节,JSON 版本 98 字节。数据量大的时候,这个差距更明显。格式互转XML 和 JSON 互转不是无损的——XML 的属性、命名空间、混合内容在 JSON 里没有对应概念:XML 属性(<book id="1">)转 JSON 时变成普通字段,丢失"这是属性"的语义XML 混合内容(<p>文字<b>加粗</b>继续</p>)转 JSON 需要特殊处理JSON 的数组类型转 XML 时只能用重复标签模拟所以别指望"先写 XML 再转 JSON"或反过来——两种格式的数据模型不同,互转会丢信息。选型决策简单判断:如果数据是给人读的文档,用 XML;如果是给程序消费的结构化数据,用 JSON。如果两者都涉及(比如带格式的富文本数据),考虑用 JSON 做传输、XML 做存储,或者直接用 Markdown + JSON 元数据的组合方案。
计算机基础阅读 05月27日 10:49

XML Schema 和 DTD 有什么区别?XSD 为什么取代了 DTD?

XML Schema(XSD)和 DTD 都用来定义 XML 文档的结构和约束,但能力差距很大。XSD 是 DTD 的现代替代方案——基于 XML 语法、支持数据类型、支持命名空间、可扩展可继承。DTD 语法简单但功能有限,新项目几乎不再使用。核心区别| 特性 | XML Schema (XSD) | DTD ||------|------------------|-----|| 语法 | XML 格式,可用 XML 解析器处理 | 自有语法,不是 XML || 数据类型 | 丰富内置类型(string、int、date、boolean 等) | 只有字符串,没有类型区分 || 命名空间 | 原生支持 | 不支持 || 类型继承 | 支持 extension 和 restriction | 不支持 || 可重用性 | 支持类型导入和引用 | 难以复用 || 约束精度 | 可定义值范围、正则模式、枚举 | 只能定义元素出现次数 |简单说:XSD 能做的 DTD 做不了(类型约束、命名空间),DTD 能做的 XSD 都能做且做得更好。XSD 基础结构XSD 本身是 XML 文档,根元素是 <xs:schema>:<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"> <xs:element name="book" type="BookType"/> <xs:complexType name="BookType"> <xs:sequence> <xs:element name="title" type="xs:string"/> <xs:element name="price" type="xs:decimal"/> <xs:element name="publishDate" type="xs:date"/> </xs:sequence> <xs:attribute name="id" type="xs:string" use="required"/> </xs:complexType></xs:schema>complexType 定义包含子元素或属性的复杂类型,simpleType 定义带约束的简单类型。XSD 的约束能力XSD 比 DTD 强的地方在于精确约束:<!-- 值范围约束 --><xs:simpleType name="AgeType"> <xs:restriction base="xs:integer"> <xs:minInclusive value="0"/> <xs:maxInclusive value="120"/> </xs:restriction></xs:simpleType><!-- 正则约束 --><xs:simpleType name="EmailType"> <xs:restriction base="xs:string"> <xs:pattern value="[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}"/> </xs:restriction></xs:simpleType><!-- 枚举约束 --><xs:simpleType name="StatusType"> <xs:restriction base="xs:string"> <xs:enumeration value="active"/> <xs:enumeration value="inactive"/> </xs:restriction></xs:simpleType>DTD 只能声明元素存在和出现次数,无法约束值的格式和范围。类型继承和扩展XSD 支持两种继承方式:extension:在基础类型上添加新元素或属性restriction:在基础类型上收紧约束<!-- 扩展:在 PersonType 上加 department --><xs:complexType name="EmployeeType"> <xs:complexContent> <xs:extension base="PersonType"> <xs:sequence> <xs:element name="department" type="xs:string"/> </xs:sequence> </xs:extension> </xs:complexContent></xs:complexType>这是 DTD 完全做不到的——DTD 没有类型体系,每个元素定义都是独立的。在 XML 中引用 XSD<book xmlns="http://www.example.com/books" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.example.com/books books.xsd"> <title>XML Guide</title> <price>49.99</price> <publishDate>2024-01-15</publishDate></book>schemaLocation 属性成对出现:命名空间 URI + XSD 文件路径。解析器会根据 XSD 验证文档内容。什么时候还在用 DTD?DTD 在 2025 年基本只剩这些场景:维护遗留系统(改 DTD 风险比替换小)HTML5 的 DOCTYPE 声明(严格说是简化版 DTD)简单的配置文件验证(不值得写 XSD 的场景)新项目用 XSD,没有理由选 DTD。如果嫌 XSD 太啰嗦,可以考虑 RelaxNG——更简洁的替代方案。
服务端阅读 05月27日 10:48

MariaDB 窗口函数怎么用?排名、累计和同比计算详解

窗口函数是 SQL 里做数据分析最好用的工具——不用窗口函数,计算排名、累计、同比这些需求得写各种子查询和自连接,代码又长又慢。MariaDB 从 10.2 开始支持窗口函数,基本覆盖了 SQL 标准的核心功能。语法结构每个窗口函数都遵循同一套语法:函数名(表达式) OVER ( PARTITION BY 分组字段 ORDER BY 排序字段 ROWS/RANGE 窗口范围)OVER 子句定义了"窗口"——函数在这个范围内计算。PARTITION BY 把数据分组,每组独立计算;ORDER BY 决定组内排序;ROWS/RANGE 进一步约束参与计算的行范围。排名函数:ROWNUMBER、RANK、DENSERANK三个排名函数的区别在处理并列值时的行为:ROW_NUMBER:严格递增,1-2-3-4,不管值是否相同RANK:并列同名,跳号,1-1-3-4DENSE_RANK:并列同名,不跳号,1-1-2-3典型场景——每个部门薪资前三名:SELECT * FROM ( SELECT name, department, salary, DENSE_RANK() OVER (PARTITION BY department ORDER BY salary DESC) AS rnk FROM employees) t WHERE rnk <= 3;用 DENSE_RANK 而不是 RANK,因为如果前三名有并列,RANK 会跳号,导致实际返回的记录少于 3 条。聚合函数做累计和移动平均窗口函数让 SUM/AVG/COUNT 不再只是"一组一个数",而是逐行累计:-- 累计销售额SELECT order_date, amount, SUM(amount) OVER (ORDER BY order_date) AS running_totalFROM orders;-- 7 天移动平均SELECT order_date, amount, AVG(amount) OVER (ORDER BY order_date ROWS BETWEEN 6 PRECEDING AND CURRENT ROW) AS avg_7dFROM orders;ROWS BETWEEN … AND … 定义了参与计算的行范围。6 PRECEDING AND CURRENT ROW 表示当前行和前 6 行,总共 7 行做平均。LAG 和 LEAD:访问前后行LAG 取前 N 行的值,LEAD 取后 N 行的值。算环比增长率靠它们:SELECT month, revenue, LAG(revenue, 1) OVER (ORDER BY month) AS prev_month, ROUND((revenue - LAG(revenue, 1) OVER (ORDER BY month)) / LAG(revenue, 1) OVER (ORDER BY month) * 100, 2) AS growth_pctFROM monthly_sales;LAG 的第二个参数是偏移量,第三个参数是默认值(缺省返回 NULL)。算同比就改成 LAG(revenue, 12),往前取 12 个月。FIRSTVALUE 和 LASTVALUE 的坑FIRSTVALUE 取分组内第一个值,LASTVALUE 取最后一个值。但 LAST_VALUE 有个常见陷阱——默认窗口范围是 ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW,不是整个分区。所以如果你想取部门最低薪资,必须显式指定窗口范围:-- 正确写法:指定完整窗口范围SELECT name, department, salary, LAST_VALUE(salary) OVER ( PARTITION BY department ORDER BY salary DESC ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING ) AS lowest_in_deptFROM employees;不加 ROWS BETWEEN … UNBOUNDED FOLLOWING,LAST_VALUE 每行返回的值都不一样——因为窗口只到当前行为止。ROWS 和 RANGE 的区别ROWS:按物理行号计算,窗口大小固定RANGE:按逻辑值范围计算,同一排序值的行作为一个整体-- ROWS:固定 3 行窗口SUM(amount) OVER (ORDER BY date ROWS BETWEEN 1 PRECEDING AND CURRENT ROW)-- RANGE:同一天的行一起算SUM(amount) OVER (ORDER BY date RANGE BETWEEN INTERVAL 7 DAY PRECEDING AND CURRENT ROW)RANGE 适合按时间窗口聚合,同一时间点的所有行会被包含在同一个窗口内。ROWS 更精确,适合固定行数的滑动窗口。
服务端阅读 05月27日 10:46

Serverless 监控和调试怎么做?实战方案详解

Serverless 的监控比传统应用难得多——函数生命周期短,日志转瞬即逝,一次请求可能跨越十几个函数,调用链路像黑盒。传统 APM 工具虽然支持 Serverless,但配置复杂,且按函数收费的定价模式在大量函数场景下很贵。先搞清楚要监控什么:冷启动频率和耗时、函数执行时间、错误率和错误类型、并发数和限流情况、调用链路和依赖关系。这五项覆盖了 Serverless 最核心的可观测性需求。日志:结构化是前提Serverless 函数的日志不是写在本地文件上的,而是打到 CloudWatch / 日志服务。如果还是 print("something happened") 这种格式,在几千条日志里找到你要的那条无异于大海捞针。必须做两件事:JSON 格式输出:每条日志包含 traceid、functionname、timestamp、level、message,方便用 CloudWatch Insights 或 Loki 过滤查询请求级 trace ID:在 API Gateway 层生成,一路透传到下游所有函数,这样可以用一条查询把整个请求链路的日志串起来分布式追踪:看清调用链路一个 API 请求从 API Gateway → Lambda A → SQS → Lambda B → DynamoDB,光看日志拼不出完整链路。分布式追踪就是解决这个问题的。AWS X-Ray 是最直接的选择——和 Lambda 原生集成,开启即可用,但功能有限。OpenTelemetry 更灵活,支持多后端(Jaeger、Zipkin、Datadog),适合多云或混合架构。关键配置点:确保所有函数都开启 tracing在函数间传递 trace context(W3C Trace Context 标准的 traceparent header)设置合理的采样率——全量追踪成本高,1% 采样又可能漏掉关键错误本地调试:模拟环境还是直接上云?本地跑 Serverless 函数有两种思路:模拟派:用 SAM CLI 的 sam local invoke 或 Serverless Framework 的 offline 插件,在本地模拟 API Gateway、DynamoDB 等服务。好处是快,坏处是模拟环境总跟线上有差异——IAM 策略、VPC 配置、环境变量都可能不一样。直接上云派:写完代码直接部署到 dev 环境,用真实云服务测试。好处是环境一致,坏处是部署慢、费钱。实际项目中两者结合:单元测试本地跑,集成测试部署到 dev 环境。别花太多时间折腾本地模拟——线上环境的差异问题,本地模拟永远解决不了。错误处理和告警Serverless 函数报错后实例就被回收了,现场信息不会保留。所以错误处理必须做到两点:捕获所有异常:在函数入口包一层 try-catch,把错误信息连同上下文(请求参数、环境变量、调用栈)写入日志和错误追踪服务(Sentry、Rollbar)。裸奔的 Lambda 一旦崩溃,你连它为什么崩溃都不知道。设置合理的告警:CloudWatch Alarm 按 Lambda 的 ErrorRate 和 Duration 设阈值,错误率超过 5% 或 P99 延迟超过 2 秒就触发告警。别等到用户投诉才发现问题。
服务端阅读 05月27日 10:45

Serverless 架构有什么缺点?冷启动、调试和成本问题怎么解?

用了两年 Serverless,踩了不少坑。冷启动延迟、调试困难、成本失控……这些问题在 PPT 里不会被提及,但上线后每一个都会咬你。下面按实际影响程度,逐一拆解 Serverless 架构的主要限制和应对策略。冷启动:最让人头疼的延迟函数一段时间没被调用后,运行环境会被回收。下次请求进来,平台得重新分配资源、加载运行时、初始化代码——这个过程就是冷启动。AWS Lambda 冷启动通常 200ms-5s,Java 等重运行时更慢,可能 10 秒以上。应对方法:预留实例:花钱保活,适合对延迟敏感的核心接口轻量运行时:Go、Rust 比 Java/Node 冷启动快一个数量级定时心跳:每 5 分钟调一次函数防止回收(治标不治本,还浪费钱)SnapStart:AWS 提供的快照恢复功能,Java 冷启动从 10 秒降到 200ms执行时间和资源的天花板AWS Lambda 最长执行 15 分钟,内存上限 10GB,/tmp 最多 10GB。视频转码、大数据批处理这类长任务直接超出限制。解法不是硬塞进 Lambda,而是换架构:长任务用 AWS Fargate / Azure Container Apps,本质是 Serverless 容器,没有执行时间限制批处理用 AWS Step Functions 编排多个 Lambda,每个处理一部分大内存任务用 AWS Lambda 的 10GB 配置,但成本很高状态管理的天然缺陷Serverless 函数是无状态的——每次调用可能落在不同的实例上,实例间不能共享内存。这意味着:不能用全局变量缓存数据(下次请求可能不是同一个实例)WebSocket 长连接需要借助 API Gateway + DynamoDB 维护用户会话必须存外部存储(Redis、DynamoDB)这不是"限制"而是"设计约束"——接受无状态,把状态外置到专用服务,反而让架构更清晰。调试和可观测性是硬伤本地跑得好好的,部署到云端就出问题——环境差异、IAM 权限、网络配置都可能不一样。传统应用的断点调试在 Serverless 里基本不可行。实际可用的调试手段:本地模拟:用 Serverless Framework 的 local invoke 或 AWS SAM 的 sam local,模拟云环境结构化日志:每个请求带 trace ID,用 CloudWatch/Loki 按请求链路追踪X-Ray/Jaeger:分布式追踪,看清函数间的调用链和耗时预发布环境:和线上配置一致,上线前跑一遍集成测试厂商锁定:被平台绑定的隐性成本AWS Lambda 用了 API Gateway + DynamoDB + Step Functions,整套架构深度绑定 AWS。要迁到阿里云函数计算,代码、配置、基础设施全得重写。降低锁定风险的策略:业务逻辑和基础设施解耦:核心代码不依赖云 SDK,通过适配层调用云服务用 Terraform/CDK 管理基础设施:换平台只改配置,不改业务代码优先选开放标准:容器镜像部署比原生函数更容易迁移现实一点:锁定是不可避免的,关键是评估迁移成本是否在可接受范围内成本:看起来便宜,算起来不一定Serverless 按调用量计费,低流量场景确实便宜。但高并发或被攻击时,费用可能远超预期——DDoS 攻击不只是安全风险,还是财务风险。成本控制手段:设置并发上限和账单告警:AWS Lambda 支持账户级并发限制,防止失控对比预留实例和按需付费:稳定流量下预留实例更划算关注冷启动的间接成本:预留实例花钱,冷启动浪费请求时间,找到平衡点监控每次调用的成本:CloudWatch 可以按函数统计费用,及时发现异常
服务端阅读 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?}