
概覽
在 CoreData 支持的 App 中,一種常見操作就是計算數(shù)據(jù)庫表中指定字段的最大值(或最小值)。就是這樣一種看起來“不足掛齒”的任務,可能稍不留神就會“馬失前蹄”。

在實際的代碼中,我們怎樣才能既迅速又簡潔的找出字段的最大值呢?
在本篇博文中,您將學到如下內(nèi)容:
- 使用 NSExpression 表達式
- 孰是孰非?
相信學完本課后,大家 CoreData 的算法武器庫中又會多幾種“削鐵如泥”的利刃啦。
本文中所有代碼的測試環(huán)境為:
- MBA 2022,M2,內(nèi)存 16 GB
- macOS 15.3.2(Sequoia)
那還等什么呢?Let‘s find out?。?!;)
4. 使用 NSExpression 表達式
除了按時間排序 VictoryStage 數(shù)組以外,我們還可以直接利用 NSExpression 表達式自帶的 max 函數(shù)來計算 VictoryStage 托管對象最新的 end 值。

大多數(shù)小伙伴們可能都對 NSExpression 的使用比較陌生,其實利用 NSExpression 我們可以寫出極具靈活性的比較邏輯表達式:
func testPerformanceWithFetchExpression() throws {
// 利用 NSExpression 的 max 函數(shù)來計算最新的 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)")
}
}
在上面的實現(xiàn)中,我們完成的主要工作是:
- 創(chuàng)建一個計算 VictoryStage.end 字段最大值(max:)的 NSExpression 表達式;
- 將該表達式應用在 Fetch Request 上;
- 調用 CoreData 上下文的 fetch 方法來求得表達式的結果;
- 從結果字典中解析最近的日期;
在運行結果中,我們可以看到實際耗時為 0.000389 秒,在 4 種方法里排名第二:

不過,這種使用 NSExpression 表達式來計算最大值方法的一個美中不足是:我們只能得到最新的日期,但卻無法同時得到其所對應的 VictoryStage 對象。
一種解決辦法是,利用上面求得的最新 end 值來再次查詢出對應的 VictoryStage 對象:
func testPerformanceWithFetchExpression() throws {
// 利用 NSExpression 的 max 函數(shù)來計算最新的 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 創(chuàng)建的一個“虛擬”屬性,它實際不存在于數(shù)據(jù)庫 VictoryStage 的表結構中,而我們無法用 NSExpression 去操作這種“虛擬”屬性,除非我們的托管對象中有一個持久的 ID 存在于數(shù)據(jù)庫的表中。
5. 孰是孰非?
現(xiàn)在,我們已經(jīng)分別實現(xiàn)了計算 CoreData 字段最大值的四種方法,最后我們再簡單聊聊它們有哪些明顯的優(yōu)點和缺點:
- CoreData 關系屬性(Relation Property)排序;
- NSArray 排序;
- CoreData Fetch 請求;
- NSExpression 表達式;
在上面這些方法中,前三種基本都是排序,只是執(zhí)行排序的層面不一樣(Array、NSArray、Sqlite),而最后一種則是實打實的用 max: 函數(shù)計算最大值。
第一種方法最慢,但實現(xiàn)起來最簡單。第二種方法也比較簡單,但效率有了數(shù)量級的提升。第三種方法速度最快,實現(xiàn)也不算麻煩,是一個不錯的選擇。最后一種方法對于這種簡單的求最大值問題略顯繁瑣,但它能擴展到更復雜的計算場景中,其擴展性和靈活性最高。
當然,大家還可以各顯神通實現(xiàn)自己獨特的算法。若如此,本篇拋磚引玉的目的就美美的達到了。
那么,看完上面這 4 種算法后,大家又作何感想呢?是不是都有種躍躍欲試的“趕腳”呢?棒棒噠!??
總結
在本篇博文中,我們討論了如何用 NSExpression 表達式來計算 CoreData 托管類字段的最大值,我們最后還對所有 4 種方法的孰是孰非做了總結。
感謝觀賞,再會啦!8-)