摘要:// 假設一個 Person 對象現在有一個下面的屬性: // NSArray *previousPay // 找到所有滿足過去工資的平均值大於 10 的人 NSString *predicateFormat = @"[email protected] > 10"。// 假設 mutablePersonAr 是一個 Person 數組,裏面有 "Karl" 和 "Jordan" NSPredicate *namesStartingWithK = [NSPredicate predicateWithFormat:@"SELF.name BEGINSWITH 'K'"]。

作者:Jordan Morgan, 原文鏈接 ,原文日期:2018-05-18

譯者: 石榴 ;校對: numbbbbbNemocdz ;定稿: Pancf

Swift 剛出現的時候,我們因它比 Objective-C 簡潔而着迷。接着它很快打開了面向協議編程的大門。並且,讓我們忘掉引用類型和類,還有很多。

確實,這些東西都是很棒的工具,都有優秀的用例。但我感覺它們經常被捧作銀彈,在決定架構時缺乏足夠的考慮。

因此在 2018 年,技術博客中充斥着各種 Swift 黑魔法(我的博客也不例外 ‍♂️),會議演講也都在討論 Swift 的函數式編程未來(沒錯,我也做了這種演講:raising_hand|type_1_2:‍♂️)。

所有人都對在 Swift 中使用集合感到激動, 但是 我們從 iOS 3 開始就可以用 Objective-C 來做 相似 的事了。所以今天我會討論 NSPredicate 的威力,以及如何用 篩選集合。

有必要提一下:我們最近看到了一些開發者一開始學了 Swift,後來又得回去維護 Objective-C 的代碼。如果說的就是你,那你很可能正在發愁如何優雅地在 Objective-C 中處理集合。

這裏講的東西可能對你有用。

用例

近幾年來,Objective-C 的集合有了長足的進步。還在幾年以前,我們還必須教這愚蠢的編譯器:

NSString *aString = (NSString *)[anArray indexOfObject:0];

感謝老天、庫比提諾[^1]和朋友們©終於用類型擦除的方式添加了泛型。這是一個很大的進步:

[^1]: 譯者注:Cupertino, CA,蘋果總部所在城市。

NSArray *anArray = @[@"Sup"];
NSString *aString = [anArray firstObject];

但無論是不是泛型,我們經常通過與下面類似的方法與 Objective-C 集合中的內容交互:

for (NSString *str in anArray)
{
    if ([str isEqualToString:@"The Key"])
    {
        // 做些什麼
    }
}

很多情況下,這樣寫是可以接受的。但是當需求越來越複雜,關係更加多種多樣,代碼就會變得不確定。如果你遵從代碼更少更穩定更容易維護的觀念,那麼這種簡單的查詢集合操作也可能成爲困擾。

Predicate 可以改善這個狀況。不是要在代碼中耍些小聰明,而是寫出簡潔和實用的代碼。

概覽

NSPredicate 的核心用途是限制或定義對內存中的數據過濾,或進行取回時的參數。當它和 Core Data 一起使用的時候纔會如虎添翼。它和 SQL 很像,只不過沒那麼糟糕*。

開個玩笑,只是我對基於集合的操作都無感 。

你給它提供邏輯條件,然後就會返回符合條件的東西。這意味着它可以提供基礎比較、複合 predicate、KeyPath 集合查詢、子查詢、合計以及更多的支持。

因爲它用來篩選集合,它可以獲得 Foundation 類的原生支持。使用可變版本時支持用結果直接修改,不可變版本會返回一個新實例:

// 修改原數組
[mutableArray filterUsingPredicate:/*NSPredicate*/];

// 返回新的數組
[mutableArray filteredArrayUsingPredicate:/*NSPredicate*/];

雖然 predicate 可以從 NSExpressionNSCompoundPredicateNSComparsionPredicate 中實例化,但它還可以用一個字符串的語法生成。這和可視化格式語言類似,我們可以用它定義排版約束。

在這裏我們主要關注能用字符串語法生成的能力。

配置

爲了更好的說明,文章的剩餘部分以下面的代碼爲前提。

// 僞代碼
Person:NSObject
Identifier:NSString
Name:NSString
PayGrade:NSNumber

// 某個含有 Person 實例的屬性
NSArray *employees

查詢:zap:️

本文剩下都在用直接的例子來介紹如何用字符串格式語法來配置查詢。

我們可以從一個簡單的搜索的情景開始。先假設我們有一個含有表示 Person 對象標識符的數組:

{
    @"erersdg32453tr",  
    @"dfs8rw093jrkls",  
    // etc
}

現在,我們想通過這些識別符從一個現存的 Person 數組中獲取 Person 對象。可以使用一個雙層嵌套的 for 循環來解決這個問題:

// 假設 "employees" 是一個存有 Person 對象的數組
NSArray  *morningEventAttendees = @[/*上面的人的識別符*/];
NSMutableArray  *peopleAttendingMorningEvent = [NSMutableArray new];

for (NSString *userID in morningEventAttendees)  
{  
    for (Person *person in employees)  
    {  
        if ([person.identifier isEqualToString:userID])  
        {  
            [peopleAttending addObject:person];  
        }  
    }  
}

// 現在 peopleAttendingMorningEvent 裏面就有我們想要的東西了

我們也可以使用 predicate 來達到完全一樣的效果:

NSPredicate *morningAttendees = [NSPredicate predicateWithFormat:@"SELF.identifier IN %@", peopleAttendingMorningEvent];

NSArray *peopleAttendingMorningEvent = [employees filteredArrayUsingPredicate:morningAttendees];

:dizzy:。

Predicate 的語法允許我們使用 SELF,它在這裏發揮了很大的作用。它表示數組裏正在被操作的對象,在這裏就是 Person 對象。

另一個額外的好處是我們不用把數組定義成可變的了。

正是因爲這個原因,我們可以訪問與 SELF 所表示對象關聯的 KeyPath。在上面的代碼中,引用了 identifier 屬性。

如果你喜歡的話,任何 KeyPath 可以用放在 “%K” 位置的變量來表示。這個版本和上面的效果一樣:

[NSPredicate predicateWithFormat:@"SELF.%K IN %@", @"identifier", peopleAttendingMorningEvent];

複合 Predicate

合併多個比較很簡單。假設我們還需要像上面一樣找到所有參加活動的人,但還要滿足他們的工資水平在 50000 到 60000 之間。

如果使用傳統的方法,我們的 if 語句只會越寫越長:

// 和上面的代碼一樣
if ([person.identifier isEqualToString:userID] && (person.paygrade.integerValue >= 5 && person.paygrade.integerValue <= 10))  
{  
    [peopleAttending addObject:person];  
}

但使用一個重構過的 predicate 可以讓我們用一種更符合語言習慣的方式來解決問題:

NSPredicate *morningAttendees = [NSPredicate predicateWithFormat:@"SELF.identifier IN %@ && SELF.paygrade.integerValue BETWEEN {50000, 60000}", peopleAttendingMorningEvent];

它允許用不同的操作符表示同樣的作用,可以根據你的偏好來提升可讀性。比如:

  • “&&” 或 “AND”
  • “||” 或 “OR”
  • “!” 或 “NOT”

如你所想,它們經常會在基本比較操作之間出現,聚合在一個 predicate 裏。

字符串比較

我們經常會處理一些基於字符串比較的匹配。大家都知道 Objective-C 對冗餘代碼的無止盡追求,在處理 NSString 的時候也絲毫不減:

NSString *name = @"Jordan";
name = [name stringByAppendingString:[NSString stringWithFormat:@"%@ %@", @"Wesley", @"Morgan"]];

……而 Swift 則一邊偷笑,一邊低調地把字符串們連接起來。幸虧我們在用 NSPredicate 來比較字符串時不會寫出上面那麼冗餘的代碼。

// 假設 mutablePersonAr 是一個 Person 數組,裏面有 "Karl" 和 "Jordan"
NSPredicate *namesStartingWithK = [NSPredicate predicateWithFormat:@"SELF.name BEGINSWITH 'K'"];
// 現在只有 Karl 了
[mutablePersonAr filterUsingPredicate:namesStartingWithK];

實際上任何比較都可以用 predicate 語法中的 CONTAINSBEGINSWITHENDSWITHLIKE 來實現:

// 假設 mutablePersonAr 是一個 Person 數組,裏面有 "Karl" 和 "Kathryn"
NSPredicate *namesStartingWithK = [NSPredicate predicateWithFormat:@"SELF.name LIKE 'Kar*'"];

// 現在只有 Karl 了
[mutablePersonAr filterUsingPredicate:namesStartingWithK];

你可能已經注意到上面的星號了;和很多的 DSL 一樣,這個星號代表一個通配符。

當你在一個查詢裏結合多個比較運算符時,這種簡潔用法的重要性就會體現出來了:

NSString *predicateFormat = @"(SELF.name LIKE 'Kar*') AND (SELF.paygrade.intValue >= 10)";

NSPredicate *namesStringWithK = [NSPredicate predicateWithFormat:predicateFormat];

// 現在只有 Karl 了
[mutablePersonAr filterUsingPredicate:namesStartingWithK];

更進一步,它還支持用 MATCHES 語法實現 NSPredicate 的類 SQL 語法與正則表達式混用:

[NSPredicate predicateWithFormat:@"SELF.phoneNumber MATCHES %@", phoneNumberRegex];

然而是時候該指出一點,predicate 語法十分嚴格。它就是一個字符串。除非你是 Mavis Beacon[^2], 否則你總會一遍又一遍地不小心打錯字。

[^2]: 譯者注: Mavis Beacon Teaches Typing ,一款在 1987 年發售的教盲打的軟件。

好消息是你會很快的發現問題 — 運行時的異常在等着你。我們獲得了能力和靈活性,但在某種程度上失去了靜態檢查的安全性。

爲了說明這一點,這段從上面代碼稍微修改而來的代碼會導致崩潰。你能看出來是爲什麼嗎?

NSString *predicateFormat = @"SELF.name LIKE 'Kar*') AND (SELF.paygrade.intValue >= 10)"

NSPredicate *namesStartingWithK = [NSPredicate predicateWithFormat:predicateFormat];

// 現在只有 Karl 了 
[mutablePersonAr filterUsingPredicate:namesStartingWithK];

爲了減輕這些問題,我經常把 predicate 和 NSStringFromSelector() 結合在一起用,以此應對錯別字和爲以後的重構提供多一層安全保障。

NSString *predicateFormat = @"(SELF.%@ LIKE 'Kar*') AND (SELF.paygrade.intValue >= 10)"

NSString *kpName = NSStringFromSelector(@selector(identifier));  
NSString *kpPaygrade = NSStringFromSelector(@selector(paygrade));

NSPredicate *namesStartingWithK = [NSPredicate predicateWithFormat:predicateFormat, kpName, kpPaygrade];

// 現在只有 Karl 了
[mutablePersonAr filterUsingPredicate:namesStartingWithK];

有點複雜了?確實。更安全了?毫無疑問。

KeyPath 集合查詢

由於基於 KeyPath 的用法, NSPredicate 擁有一全套工具去操作它們,以提供一個更好的搜索。考慮下面的代碼:

// 假設一個 Person 對象現在有一個下面的屬性:
// NSArray *previousPay

// 找到所有滿足過去工資的平均值大於 10 的人
NSString *predicateFormat = @"[email protected] > 10";
NSPredicate *previousPayOverTen = [NSPredicate predicateWithFormat:predicateFormat];

// 所有過去工資的平均值大於 10 的人
[mutablePersonAr filterUsingPredicate:previousPayOverTen];

你可以把 @avg 換成:

@sum
@max
@min
@count

想象下如果不使用 predicate 情況下完成同樣的工作,就不得不寫大量儘管很簡單的代碼。你可以開始將這些技巧用在你日常的工具鏈裏。

對數組的深究

和 KeyPath 查詢很像,predicate 也支持以更細的維度檢查數組:

array[FIRST]
array[LAST]
array[SIZE]
array[index]

應用在上面的代碼樣例上,我們就可以這樣查詢:

// 找到所有過去有三份不同工資的人
NSString *predicateFormat = @"previousPay[SIZE] == 3";

NSPredicate *threePreviousSalaries = [NSPredicate predicateWithFormat:predicateFormat];

// 這些 Person 對象過去有三份不同的工資
[mutablePersonAr filterUsingPredicate:threePreviousSalaries];

和在上面提到的一樣,我們也可以應用多個條件:

// 找到所有過去有三個不同的工資以及第一份工資大於 8 的人
NSString *predicateFormat = @"(previousPay[SIZE] == 3) AND (previousPay[FIRST].intValue > 8)";

NSPredicate *predicate = [NSPredicate predicateWithFormat:predicateFormat];
[mutablePersonAr filterUsingPredicate:predicate];

更加深入,你可以使用下面的操作符來實現更復雜的操作:

@distinctUnionOfArrays
@unionOfArrays
@unionOfObjects
@distinctUnionOfObjects

假設我們有一個含有 Person 對象的數組,我們需要的是找出在所有數組中識別符不同的 Person 實例:

// 假設 p1/2/3/4 都是 Person 對象
NSArray  *> *previousEmployees = @[@[p1],@[p2,p1,p2],@[p1],@[p4,p2],@[p4],@[p4],@[p1]];

// 獲取所有不同的 ID
NSArray *unqiuePreviousEmployeeIDs = [previousEmployees valueForKeyPath:@"@distinctUnionOfObjects.identifier"];

// 現在數組裏應該只含有不同的 ID

厲害吧!

還有更好玩的呢,還支持子查詢:

// 假設 Person 對象有了一個新的屬性表示他們的隊伍:
// NSArray  *team;
  
// 從僱員數組中找出這樣的人,他們的團隊中有人滿足這個條件:沒有歷史工資數據並且工資大於 1
NSString *predicateFormat = @"SUBQUERY(team, $teamMember, $teamMember.paygrade.intValue > 1 AND $teamMember.previousPay == nil).@count > 0";

NSPredicate *predicate = [NSPredicate predicateWithFormat:predicateFormat];
[employeeAr filterUsingPredicate:predicate];

當你發現你需要在一個含有對象的數組裏搜索,而這些對象含有的屬性本身就是一個集合的時候,子查詢十分有用。所以在上面的例子裏,我們有一個 Person 對象的數組,並且查詢它的 teamMember 數組。

便捷才是關鍵[^3]

[^3]: 譯者注:此處原作者用了雙關。原文是 “Convenience is Key(Path)”,既有便捷是關鍵的意思,又在暗指這裏的關鍵其實是 Key Path。

儘管 NSPredicate 是爲了搜索而設計出來的,但如果你不把它用在和原本設計 稍微 偏離的地方那它就不是 Objective-C 了。這裏也不例外。

當你想到 predicate,你想到的是從一個集合裏篩選 — 也就是說它的返回值(或更改過的原來數組)還含有相同的東西。

但是也可以讓他們含有 同的東西。其實我們在之前的代碼中已經這樣操作過了。上面的二維數組被用來返回一個識別符的數組 — NSString 實例。KeyPath 讓這些變得可能。

這有一個更直接的例子:

// 我們得到一個長度大於 10 的識別符字符串的數組
NSString *predicateFormat = @"SELF.identifier.length > 10";
NSPredicate *predicate = [NSPredicate predicateWithFormat:predicateFormat];
NSArray  *longEmployeeIDs = [[employeeArray filteredArrayUsingPredicate:predicate] valueForKey:@"identifier"];

// 現在 longEmployeeIDs 已經不含有 Person 對象了,只有字符串

總結

馬上在 Objective-C 的集合裏使用這些語法糖,這樣就可以不使用嵌套循環從一個特定的子集中提取數據。使用 NSPredicate 可以讓眼睛輕鬆很多。

雖然 Swift 從語言級別支持對集合進行切片操作,但使用創建的 NSPredicate 對象來解決相同的問題也不難。如果你發現你在維護一個成熟的代碼庫,或是需要用上古時代 Objective-C 的新項目,隨心所欲的使用 predicate 吧。

下次見吧:v:。

本文由 SwiftGG 翻譯組翻譯,已經獲得作者翻譯授權,最新文章請訪問http://swift.gg。

相關文章