概览
在 CoreData 支持的 App 中,一种常见操作就是计算数据库表中指定字段的最大值(或最小值)。就是这样一种看起来“不足挂齿”的任务,可能稍不留神就会“马失前蹄”。
在实际的代码中,我们怎样才能既迅速又简洁的找出字段的最大值呢?
在本篇博文中,您将学到如下内容:
相信学完本课后,大家 CoreData 的算法武器库中又会多几种“削铁如泥”的利刃啦。
本文中所有代码的测试环境为:
- MBA 2022,M2,内存 16 GB
- macOS 15.3.2(Sequoia)
那还等什么呢?Let‘s find out!!!😉
4. 使用 NSExpression 表达式
除了按时间排序 VictoryStage 数组以外,我们还可以直接利用 NSExpression 表达式自带的 max 函数来计算 VictoryStage 托管对象最新的 end 值。
大多数小伙伴们可能都对 NSExpression 的使用比较陌生,其实利用 NSExpression 我们可以写出极具灵活性的比较逻辑表达式:
func testPerformanceWithFetchExpression() throws {
// 利用 NSExpression 的 max 函数来计算最新的 VictoryStage
measure {
let req = NSFetchRequest<NSDictionary>(entityName: "VictoryStage")
req.predicate = .init(format: "project = %@", project)
let maxExpr = NSExpression(forKeyPath: \VictoryStage.end)
let maxExprDesc = NSExpressionDescription()
maxExprDesc.name = "endDay"
maxExprDesc.expression = NSExpression(forFunction: "max:", arguments: [maxExpr])
maxExprDesc.expressionResultType = .dateAttributeType
req.propertiesToFetch = [maxExprDesc]
req.resultType = .dictionaryResultType
let result = try! context.fetch(req).first as! [String: Any]
let endDay = result["endDay"] as! Date
print("最新 VStage 的日期: \(endDay)")
}
}
在上面的实现中,我们完成的主要工作是:
- 创建一个计算 VictoryStage.end 字段最大值(max:)的 NSExpression 表达式;
- 将该表达式应用在 Fetch Request 上;
- 调用 CoreData 上下文的 fetch 方法来求得表达式的结果;
- 从结果字典中解析最近的日期;
在运行结果中,我们可以看到实际耗时为 0.000389 秒,在 4 种方法里排名第二:
不过,这种使用 NSExpression 表达式来计算最大值方法的一个美中不足是:我们只能得到最新的日期,但却无法同时得到其所对应的 VictoryStage 对象。
一种解决办法是,利用上面求得的最新 end 值来再次查询出对应的 VictoryStage 对象:
func testPerformanceWithFetchExpression() throws {
// 利用 NSExpression 的 max 函数来计算最新的 VictoryStage
measure {
let req = NSFetchRequest<NSDictionary>(entityName: "VictoryStage")
req.predicate = .init(format: "project = %@", project)
let maxExpr = NSExpression(forKeyPath: \VictoryStage.end)
let maxExprDesc = NSExpressionDescription()
maxExprDesc.name = "endDay"
maxExprDesc.expression = NSExpression(forFunction: "max:", arguments: [maxExpr])
maxExprDesc.expressionResultType = .dateAttributeType
req.propertiesToFetch = [maxExprDesc]
req.resultType = .dictionaryResultType
let result = try! context.fetch(req).first as! [String: Any]
let endDay = result["endDay"] as! Date
let req2 = VictoryStage.fetchRequest()
req2.predicate = .init(format: "project = %@ AND end = %@", argumentArray: [project!, endDay])
req2.fetchLimit = 1
let mostRecent = try! context.fetch(req2).first
print("最新 VStage 的日期:\(mostRecent?.end ?? .distantPast)")
}
}
不过这种方法需要 2 次查表,所以会带来一定性能上的损失。它的耗时为 0.000688 秒,比原先慢了将近一倍:
有些小伙伴们可能会认为,我们可以通过同时获取最新日期和对应对象的 ID 来找到最新日期对应的 VictoryStage 对象:
let maxEndExpr = NSExpression(forFunction: "max:", arguments: [NSExpression(forKeyPath: "end")])
let maxEndExprDesc = NSExpressionDescription()
maxEndExprDesc.name = "maxEnd"
maxEndExprDesc.expression = maxEndExpr
maxEndExprDesc.expressionResultType = .dateAttributeType
// 2. 定义对象的唯一标识符(如 objectID)
let objectIDExpr = NSExpression(forKeyPath: \VictoryStage.objectID)
let objectIDExprDesc = NSExpressionDescription()
objectIDExprDesc.name = "objectID"
objectIDExprDesc.expression = objectIDExpr
objectIDExprDesc.expressionResultType = .objectIDAttributeType
req.propertiesToFetch = [maxEndExprDesc, objectIDExprDesc]
let results = try context.fetch(req)
但遗憾的是,这种方法行不通。原因是:VictoryStage.objectID 属性是 CoreData 创建的一个“虚拟”属性,它实际不存在于数据库 VictoryStage 的表结构中,而我们无法用 NSExpression 去操作这种“虚拟”属性,除非我们的托管对象中有一个持久的 ID 存在于数据库的表中。
5. 孰是孰非?
现在,我们已经分别实现了计算 CoreData 字段最大值的四种方法,最后我们再简单聊聊它们有哪些明显的优点和缺点:
- CoreData 关系属性(Relation Property)排序;
- NSArray 排序;
- CoreData Fetch 请求;
- NSExpression 表达式;
在上面这些方法中,前三种基本都是排序,只是执行排序的层面不一样(Array、NSArray、Sqlite),而最后一种则是实打实的用 max: 函数计算最大值。
第一种方法最慢,但实现起来最简单。第二种方法也比较简单,但效率有了数量级的提升。第三种方法速度最快,实现也不算麻烦,是一个不错的选择。最后一种方法对于这种简单的求最大值问题略显繁琐,但它能扩展到更复杂的计算场景中,其扩展性和灵活性最高。
当然,大家还可以各显神通实现自己独特的算法。若如此,本篇抛砖引玉的目的就美美的达到了。
那么,看完上面这 4 种算法后,大家又作何感想呢?是不是都有种跃跃欲试的“赶脚”呢?棒棒哒!💯
想要进一步系统地学习 Swift 开发的小伙伴们,可以来我的《Swift 语言开发精讲》专栏逛一逛哦:
总结
在本篇博文中,我们讨论了如何用 NSExpression 表达式来计算 CoreData 托管类字段的最大值,我们最后还对所有 4 种方法的孰是孰非做了总结。
感谢观赏,再会啦!😎