1. Swift 数组扩展实战指南
作为iOS开发中最基础的数据结构,Array在Swift中扮演着重要角色。但标准库提供的功能往往不能满足实际开发需求,这时候扩展(Extension)就派上用场了。我在多个商业项目中积累了一套实用的Array扩展方案,今天先分享其中最基础的几个增强功能。
提示:本文所有扩展都基于Swift 5.7+语法,建议在Xcode 14+环境下测试
1.1 为什么需要扩展Array
标准库的Array虽然提供了map、filter等基础功能,但在实际业务场景中经常遇到这些痛点:
- 安全访问元素时需要频繁写guard语句
- 数组判空逻辑散落在各处
- 需要手动处理数组越界问题
- 缺少常用的数据转换方法
比如从网络请求返回的JSON数组,我们经常需要这样的操作:
guard let firstItem = jsonArray.first else { return } guard jsonArray.count > 5 else { return } let validItems = jsonArray.filter { $0.isValid }这些样板代码会重复出现在各个模块,通过合理的扩展可以大幅提升代码整洁度。
2. 基础安全扩展实现
2.1 安全下标访问
最经典的扩展莫过于安全下标访问,避免数组越界崩溃:
extension Array { subscript(safe index: Index) -> Element? { return indices.contains(index) ? self[index] : nil } }使用示例:
let fruits = ["Apple", "Orange", "Banana"] print(fruits[safe: 5] ?? "Unknown") // 输出"Unknown"实现要点:
- 使用indices.contains先检查索引有效性
- 返回Optional类型强制调用方处理nil情况
- 保持与原生下标相似的调用语法
2.2 非空判断扩展
项目中最常见的判断就是数组是否非空:
extension Array { var isNotEmpty: Bool { !isEmpty } }虽然简单,但能显著提升代码可读性:
// 之前 if !users.isEmpty { ... } // 之后 if users.isNotEmpty { ... }注意:这种简单扩展适合团队代码规范统一的情况,如果项目中原生isEmpty使用较多,突然引入isNotEmpty可能造成风格混乱
3. 数据转换扩展
3.1 去重操作
处理网络数据时经常需要去除重复元素:
extension Array where Element: Hashable { func unique() -> [Element] { var seen = Set<Element>() return filter { seen.insert($0).inserted } } }技术细节:
- 通过where子句限制Element必须遵守Hashable协议
- 使用Set的insert返回值判断元素是否已存在
- 时间复杂度O(n),空间复杂度O(n)
使用示例:
let numbers = [1, 2, 2, 3, 4, 4].unique() // [1, 2, 3, 4]3.2 分组转换
后台返回的扁平数据经常需要按某个属性分组:
extension Array { func groupBy<Key: Hashable>(_ keyForValue: (Element) -> Key) -> [Key: [Element]] { return Dictionary(grouping: self, by: keyForValue) } }实际应用:
struct Person { let name: String let age: Int } let people = [ Person(name: "Alice", age: 25), Person(name: "Bob", age: 30), Person(name: "Charlie", age: 25) ] let grouped = people.groupBy { $0.age } /* [ 25: [Person(name: "Alice", age: 25), Person(name: "Charlie", age: 25)], 30: [Person(name: "Bob", age: 30)] ] */4. 性能优化扩展
4.1 批量更新操作
当需要修改多个元素时,传统方式会多次触发didSet:
extension Array { mutating func updateBatch(_ indices: [Int], transform: (inout Element) -> Void) { indices.forEach { index in if indices.contains(index) { transform(&self[index]) } } } }使用示例:
var numbers = [1, 2, 3, 4, 5] numbers.updateBatch([0, 2, 4]) { $0 *= 10 } // numbers变为 [10, 2, 30, 4, 50]优化点:
- 减少属性观察器的触发次数
- 单次完成所有元素修改
- 自动处理索引越界情况
4.2 预取优化
处理大型数组时,prefetch可以提升性能:
extension Array { func prefetch(batchSize: Int, at index: Int, execute: ([Element]) -> Void) { let start = max(0, index - batchSize/2) let end = min(count, index + batchSize/2) execute(Array(self[start..<end])) } }典型使用场景:
let largeArray = (0..<10000).map { $0 } largeArray.prefetch(batchSize: 50, at: 500) { batch in // 预加载处理 print("Prefetching \(batch.count) items") }5. 实战问题解决方案
5.1 多条件排序
产品列表经常需要多级排序:
extension Array { func sorted<T: Comparable>(by keyPaths: KeyPath<Element, T>...) -> [Element] { return sorted { a, b in for keyPath in keyPaths { if a[keyPath: keyPath] != b[keyPath: keyPath] { return a[keyPath: keyPath] < b[keyPath: keyPath] } } return false } } }使用示例:
struct Product { let name: String let price: Double let sales: Int } let products = [ Product(name: "A", price: 100, sales: 50), Product(name: "B", price: 80, sales: 100), Product(name: "C", price: 80, sales: 80) ] // 先按价格升序,再按销量降序 let sorted = products.sorted(by: \.price, \.sales)5.2 分页加载处理
列表分页是移动端常见需求:
extension Array { func paginated(page: Int, pageSize: Int) -> (data: [Element], hasMore: Bool) { let start = (page - 1) * pageSize guard start < count else { return ([], false) } let end = min(start + pageSize, count) return (Array(self[start..<end]), end < count) } }实际调用:
let allItems = (1...153).map { "Item \($0)" } let (page1, hasMore1) = allItems.paginated(page: 1, pageSize: 50) let (page2, hasMore2) = allItems.paginated(page: 4, pageSize: 50)6. 扩展的注意事项
命名冲突风险: 在大型项目中,不同模块可能定义相同名称的扩展。建议:
- 添加模块名前缀:
module_safeSubscript - 使用更具体的名称:
safeIndexSubscript
- 添加模块名前缀:
协议约束选择:
// 过于宽松 extension Array where Element: Any { ... } // 过于严格 extension Array where Element: CustomStringConvertible & Equatable { ... } // 适度约束 extension Array where Element: Equatable { ... }性能考量:
- 避免在扩展中频繁创建临时数组
- 链式调用时注意中间结果的产生
- 大数据量操作考虑使用lazy
测试覆盖要点:
func testSafeSubscript() { let array = [1, 2, 3] XCTAssertNil(array[safe: -1]) XCTAssertNil(array[safe: 3]) XCTAssertEqual(array[safe: 0], 1) }
我在实际项目中总结的经验是:Array扩展应该保持高内聚性,每个扩展只解决一个具体问题。下期我们会探讨更高级的Array扩展技巧,包括:
- 与SwiftUI的深度结合
- 属性包装器的应用
- 并发安全访问方案