MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Objective-C中的谓词(NSPredicate)高级用法

2022-05-271.6k 阅读

谓词在数据过滤中的应用原理

在 Objective-C 编程中,谓词(NSPredicate)是一种强大的工具,用于对集合中的对象进行过滤和筛选。它的核心原理基于一种声明式的逻辑表达式,通过这种表达式来定义满足特定条件的对象集合。

例如,假设我们有一个包含多个 Person 对象的数组,每个 Person 对象有 nameage 等属性。如果我们想从这个数组中找出所有年龄大于 30 岁的人,就可以使用谓词来实现。谓词会遍历数组中的每个 Person 对象,根据我们定义的逻辑表达式(年龄大于 30 岁)来判断该对象是否符合条件,符合条件的对象就会被筛选出来。

从本质上讲,谓词是一种抽象的条件描述方式,它将条件的定义与具体的对象操作分离开来。这样,我们可以在不同的场景下复用相同的谓词逻辑,而不需要为每个具体的数据操作都编写特定的代码。例如,同样是年龄大于 30 岁这个条件,我们既可以用于过滤数组中的 Person 对象,也可以用于数据库查询中筛选符合条件的记录,只需要在不同的环境中应用相同的谓词逻辑即可。

谓词与集合操作的紧密结合

  1. 与数组的结合:在 Objective-C 中,NSArray 类提供了方法来应用谓词进行过滤。例如,我们有如下代码:
NSArray *people = @[
    [[Person alloc] initWithName:@"Alice" age:25],
    [[Person alloc] initWithName:@"Bob" age:35],
    [[Person alloc] initWithName:@"Charlie" age:40]
];
NSPredicate *predicate = [NSPredicate predicateWithFormat:@"age > 30"];
NSArray *filteredPeople = [people filteredArrayUsingPredicate:predicate];

在上述代码中,我们首先创建了一个包含多个 Person 对象的数组 people。然后定义了一个谓词 predicate,其条件是 age > 30。最后,通过 filteredArrayUsingPredicate: 方法将谓词应用到数组 people 上,得到了一个新的数组 filteredPeople,其中只包含年龄大于 30 岁的 Person 对象。

  1. 与集合类的结合:除了数组,NSSet 等集合类也支持使用谓词进行过滤。例如:
NSSet *numberSet = [NSSet setWithArray:@[@10, @20, @30, @40]];
NSPredicate *numberPredicate = [NSPredicate predicateWithFormat:@"SELF > 20"];
NSSet *filteredNumberSet = [numberSet filteredSetUsingPredicate:numberPredicate];

这里我们创建了一个包含整数的集合 numberSet,定义了谓词 numberPredicate 用于筛选大于 20 的数,然后通过 filteredSetUsingPredicate: 方法得到了过滤后的集合 filteredNumberSet

复杂谓词表达式的构建

  1. 逻辑连接词的运用:谓词表达式可以使用逻辑连接词(如 ANDORNOT)来构建复杂的条件。例如,我们想筛选出年龄大于 30 岁且名字以 “B” 开头的人,可以这样构建谓词:
NSPredicate *complexPredicate = [NSPredicate predicateWithFormat:@"age > 30 AND name BEGINSWITH 'B'"];

在这个表达式中,AND 连接了两个条件,只有同时满足这两个条件的 Person 对象才会被筛选出来。

  1. 通配符的使用:谓词支持通配符来进行模糊匹配。例如,要找出名字包含 “ar” 的人,可以使用如下谓词:
NSPredicate *wildcardPredicate = [NSPredicate predicateWithFormat:@"name CONTAINS[cd] 'ar'"];

这里的 CONTAINS[cd] 表示不区分大小写的包含匹配,[cd] 是可选的修饰符,用于指定匹配时忽略大小写和重音符号。

  1. 比较运算符的组合:我们还可以组合多种比较运算符来构建复杂条件。比如,筛选出年龄在 30 到 40 岁之间的人:
NSPredicate *rangePredicate = [NSPredicate predicateWithFormat:@"age >= 30 AND age <= 40"];

通过这种方式,可以灵活地根据业务需求定义出各种复杂的筛选条件。

谓词在数据排序中的作用

  1. 基于谓词的排序原理:谓词不仅可以用于数据过滤,还能在数据排序中发挥作用。在对集合进行排序时,我们可以结合谓词来定义排序的规则。例如,我们有一个包含 Book 对象的数组,每个 Book 对象有 titleprice 属性。如果我们想按照价格从高到低对书籍进行排序,并且只考虑价格大于 50 的书籍,可以这样实现:
NSArray *books = @[
    [[Book alloc] initWithTitle:@"Book1" price:40],
    [[Book alloc] initWithTitle:@"Book2" price:60],
    [[Book alloc] initWithTitle:@"Book3" price:55]
];
NSSortDescriptor *sortDescriptor = [NSSortDescriptor sortDescriptorWithKey:@"price" ascending:NO];
NSPredicate *pricePredicate = [NSPredicate predicateWithFormat:@"price > 50"];
NSArray *filteredAndSortedBooks = [[books filteredArrayUsingPredicate:pricePredicate] sortedArrayUsingDescriptors:@[sortDescriptor]];

在上述代码中,首先定义了一个按照 price 属性从高到低排序的 NSSortDescriptor。然后定义了一个谓词 pricePredicate 用于筛选价格大于 50 的书籍。最后,先通过谓词过滤数组,再对过滤后的数组进行排序,得到了符合条件且按价格排序的书籍数组。

  1. 多条件排序与谓词的结合:在实际应用中,可能需要根据多个条件进行排序。例如,先按照价格从高到低排序,如果价格相同,则按照标题字母顺序排序。同时,我们还是只考虑价格大于 50 的书籍,代码如下:
NSSortDescriptor *priceSortDescriptor = [NSSortDescriptor sortDescriptorWithKey:@"price" ascending:NO];
NSSortDescriptor *titleSortDescriptor = [NSSortDescriptor sortDescriptorWithKey:@"title" ascending:YES];
NSArray *sortDescriptors = @[priceSortDescriptor, titleSortDescriptor];
NSPredicate *pricePredicate = [NSPredicate predicateWithFormat:@"price > 50"];
NSArray *filteredAndMultiSortedBooks = [[books filteredArrayUsingPredicate:pricePredicate] sortedArrayUsingDescriptors:sortDescriptors];

这里定义了两个 NSSortDescriptor,分别用于价格和标题的排序。然后将这两个描述符组成数组,应用到过滤后的数组上,实现了多条件排序。

谓词在数据库查询模拟中的应用

  1. 模拟数据库查询场景:虽然 Objective-C 本身不是专门的数据库编程语言,但通过谓词可以在一定程度上模拟数据库查询的功能。假设我们有一个简单的 “数据库”,以数组形式存储数据,每个数据项是一个 Record 对象,包含 idnamevalue 等属性。我们可以使用谓词来模拟各种数据库查询操作。

例如,要查询 id 为 10 的记录:

NSArray *records = @[
    [[Record alloc] initWithId:10 name:@"Record1" value:100],
    [[Record alloc] initWithId:20 name:@"Record2" value:200],
    [[Record alloc] initWithId:10 name:@"Record3" value:300]
];
NSPredicate *idPredicate = [NSPredicate predicateWithFormat:@"id == 10"];
NSArray *queriedRecords = [records filteredArrayUsingPredicate:idPredicate];
  1. 复杂查询模拟:我们还可以模拟更复杂的数据库查询,比如查询 name 包含 “Record” 且 value 大于 150 的记录:
NSPredicate *complexQueryPredicate = [NSPredicate predicateWithFormat:@"name CONTAINS[cd] 'Record' AND value > 150"];
NSArray *complexQueriedRecords = [records filteredArrayUsingPredicate:complexQueryPredicate];

通过这种方式,在不使用真正数据库的情况下,我们可以利用谓词对内存中的数据进行灵活的查询操作,这在一些小型应用或者数据量较小的场景中非常实用。

谓词在数据验证中的巧妙运用

  1. 数据验证的概念:数据验证是确保程序中输入数据合法性的重要步骤。谓词可以被用于定义数据验证的规则,通过将输入数据与谓词定义的条件进行匹配,来判断数据是否合法。

例如,我们有一个用户注册功能,要求用户输入的密码长度必须在 6 到 12 位之间。可以使用谓词来验证密码:

NSString *password = @"abc1234";
NSPredicate *passwordPredicate = [NSPredicate predicateWithFormat:@"SELF.length >= 6 AND SELF.length <= 12"];
BOOL isValidPassword = [passwordPredicate evaluateWithObject:password];

在上述代码中,定义了一个谓词 passwordPredicate,其条件是字符串长度在 6 到 12 之间。然后通过 evaluateWithObject: 方法将输入的密码与谓词条件进行匹配,返回 BOOL 值表示密码是否合法。

  1. 复杂数据结构的验证:对于更复杂的数据结构,比如一个包含多个字段的用户信息对象,我们可以定义更复杂的谓词来验证整个对象的合法性。假设 UserInfo 对象有 name(不能为空)、age(必须在 18 到 100 岁之间)和 email(必须符合邮箱格式)等属性:
UserInfo *user = [[UserInfo alloc] initWithName:@"John" age:25 email:@"john@example.com"];
NSPredicate *namePredicate = [NSPredicate predicateWithFormat:@"name != nil AND name.length > 0"];
NSPredicate *agePredicate = [NSPredicate predicateWithFormat:@"age >= 18 AND age <= 100"];
NSPredicate *emailPredicate = [NSPredicate predicateWithFormat:@"SELF MATCHES %@", @"[A-Z0-9a-z._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}"];
BOOL isValidUser = [namePredicate evaluateWithObject:user] && [agePredicate evaluateWithObject:user] && [emailPredicate evaluateWithObject:user.email];

这里分别定义了针对 nameageemail 的谓词,然后通过 evaluateWithObject: 方法对 UserInfo 对象及其相关属性进行验证,最终通过逻辑与操作判断整个用户信息是否合法。

谓词在动态条件构建中的灵活性

  1. 动态构建谓词的需求:在实际编程中,经常会遇到需要根据不同的用户输入或者运行时条件来动态构建谓词的情况。例如,在一个搜索功能中,用户可以选择按照不同的字段(如姓名、年龄、地址等)进行搜索,并且可以输入不同的搜索关键词。这时就需要根据用户的选择和输入动态构建谓词。

  2. 基于用户输入动态构建谓词示例:假设我们有一个 Employee 对象数组,用户可以选择按照 name 或者 department 字段进行搜索,并输入搜索关键词。代码如下:

NSArray *employees = @[
    [[Employee alloc] initWithName:@"Alice" department:@"HR"],
    [[Employee alloc] initWithName:@"Bob" department:@"Engineering"],
    [[Employee alloc] initWithName:@"Charlie" department:@"HR"]
];
NSString *searchField = @"name"; // 用户选择的搜索字段
NSString *searchKeyword = @"Al"; // 用户输入的搜索关键词
NSString *predicateFormat;
if ([searchField isEqualToString:@"name"]) {
    predicateFormat = @"name CONTAINS[cd] %@";
} else if ([searchField isEqualToString:@"department"]) {
    predicateFormat = @"department CONTAINS[cd] %@";
}
NSPredicate *dynamicPredicate = [NSPredicate predicateWithFormat:predicateFormat, searchKeyword];
NSArray *searchedEmployees = [employees filteredArrayUsingPredicate:dynamicPredicate];

在上述代码中,首先根据用户选择的搜索字段确定谓词的格式,然后根据用户输入的搜索关键词构建动态谓词,最后应用该谓词对 Employee 数组进行过滤,实现了根据用户输入动态搜索的功能。

  1. 多条件动态谓词构建:如果用户可以同时输入多个搜索条件,例如同时搜索某个部门且名字包含特定关键词,我们可以进一步扩展动态谓词的构建。假设用户可以选择是否搜索特定部门,并且输入名字关键词:
BOOL searchByDepartment = YES;
NSString *department = @"HR";
NSString *nameKeyword = @"Al";
NSMutableArray *predicateSubstrings = [NSMutableArray array];
if (searchByDepartment) {
    [predicateSubstrings addObject:@"department == %@"];
}
[predicateSubstrings addObject:@"name CONTAINS[cd] %@"];
NSString *finalPredicateFormat = [predicateSubstrings componentsJoinedByString:@" AND "];
NSMutableArray *arguments = [NSMutableArray array];
if (searchByDepartment) {
    [arguments addObject:department];
}
[arguments addObject:nameKeyword];
NSPredicate *multiConditionDynamicPredicate = [NSPredicate predicateWithFormat:finalPredicateFormat, arguments];
NSArray *multiSearchedEmployees = [employees filteredArrayUsingPredicate:multiConditionDynamicPredicate];

这里通过 NSMutableArray 动态构建谓词的格式字符串和参数数组,根据用户的选择和输入动态生成包含多个条件的谓词,实现了更复杂的动态条件搜索功能。

谓词在国际化和本地化中的考量

  1. 国际化和本地化的基本概念:在开发全球化应用时,国际化(Internationalization,简称 i18n)和本地化(Localization,简称 l10n)是重要的环节。国际化是指设计和开发应用程序,使其能够适应不同语言、地区和文化的过程。本地化则是将国际化的应用程序针对特定语言、地区和文化进行定制的过程。

  2. 谓词在国际化和本地化中的应用场景:当应用涉及到不同语言和地区的数据处理时,谓词也需要考虑国际化和本地化的因素。例如,在日期和数字的比较中,不同地区的格式可能不同。假设我们有一个包含 Event 对象的数组,每个 Event 对象有 eventDate 属性(NSDate 类型)。如果我们要筛选出某个日期之后的事件,在不同地区日期格式不同的情况下,就需要注意谓词的使用。

在本地化日期比较时,我们可以使用 NSDateFormatter 来处理日期格式的差异。例如:

NSDate *referenceDate = [NSDate date];
NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init];
[dateFormatter setDateFormat:@"yyyy - MM - dd"];
NSString *formattedDate = [dateFormatter stringFromDate:referenceDate];
NSPredicate *datePredicate = [NSPredicate predicateWithFormat:@"eventDate > %@", referenceDate];
NSArray *eventsAfterDate = [events filteredArrayUsingPredicate:datePredicate];

这里通过 NSDateFormatterNSDate 对象转换为特定格式的字符串,然后在谓词中使用 NSDate 对象进行日期比较,这样可以确保在不同地区和语言环境下日期比较的正确性。

  1. 字符串比较的本地化:在字符串比较方面,不同语言的排序和比较规则也可能不同。例如,在某些语言中,字母的大小写和重音符号在比较时需要特殊处理。当使用谓词进行字符串比较时,我们可以使用本地化的比较选项。例如:
NSString *localizedString1 = @"äpfel";
NSString *localizedString2 = @"Apfel";
NSComparisonResult result = [localizedString1 compare:localizedString2 options:NSLocalizedSearch];
NSPredicate *stringPredicate = [NSPredicate predicateWithFormat:@"SELF == %@", localizedString2];
BOOL isEqual = [stringPredicate evaluateWithObject:localizedString1];

在上述代码中,通过 NSLocalizedSearch 选项进行本地化的字符串比较,同时在谓词中也考虑了本地化字符串的比较,确保在不同语言环境下字符串比较的准确性。

谓词与其他框架和技术的集成

  1. 与 Core Data 的集成:Core Data 是 iOS 和 macOS 开发中用于数据持久化和管理的框架。谓词在 Core Data 中扮演着至关重要的角色,用于查询和筛选数据。例如,我们有一个 Core Data 实体 Person,包含 nameage 属性。要从 Core Data 存储中查询年龄大于 30 岁的人,可以这样实现:
NSFetchRequest *fetchRequest = [NSFetchRequest fetchRequestWithEntityName:@"Person"];
NSPredicate *agePredicate = [NSPredicate predicateWithFormat:@"age > 30"];
[fetchRequest setPredicate:agePredicate];
NSError *error;
NSArray *result = [managedObjectContext executeFetchRequest:fetchRequest error:&error];

在上述代码中,首先创建了一个 NSFetchRequest 用于从 Person 实体中获取数据。然后定义了谓词 agePredicate 用于筛选年龄大于 30 岁的人,并将谓词设置到 fetchRequest 中。最后通过 executeFetchRequest:error: 方法执行查询,得到符合条件的 Person 对象数组。

  1. 与 MapKit 的集成:在地图应用开发中,使用 MapKit 框架时,谓词可以用于筛选地图上特定区域内的标注点。假设我们有一个包含 MapAnnotation 对象的数组,每个 MapAnnotation 对象有 coordinate 属性(CLLocationCoordinate2D 类型)。要筛选出在某个矩形区域内的标注点,可以这样做:
MKMapRect targetRect = MKMapRectMake(10, 10, 100, 100);
NSPredicate *rectPredicate = [NSPredicate predicateWithBlock:^BOOL(MapAnnotation *annotation, NSDictionary *bindings) {
    MKMapPoint point = MKMapPointForCoordinate(annotation.coordinate);
    return MKMapRectContainsPoint(targetRect, point);
}];
NSArray *filteredAnnotations = [annotations filteredArrayUsingPredicate:rectPredicate];

这里通过 NSPredicate 的 block 形式定义了一个谓词,在 block 中判断每个 MapAnnotation 的坐标是否在指定的矩形区域内,然后应用该谓词对标注点数组进行过滤,得到在指定区域内的标注点数组。

  1. 与第三方库的集成:在使用第三方库进行数据处理时,谓词也可以与之集成。例如,在使用 AFNetworking 进行网络数据请求后,得到的数据可能需要进行筛选。假设我们从网络获取了一个包含 JSON 数据的数组,每个 JSON 对象包含 idtitle 等字段。我们可以将 JSON 数据转换为自定义对象,然后使用谓词进行筛选。
NSArray *jsonArray = @[
    @{@"id": @1, @"title": @"Item1"},
    @{@"id": @2, @"title": @"Item2"},
    @{@"id": @3, @"title": @"Item3"}
];
NSMutableArray *customObjects = [NSMutableArray array];
for (NSDictionary *jsonDict in jsonArray) {
    CustomObject *object = [[CustomObject alloc] initWithDictionary:jsonDict];
    [customObjects addObject:object];
}
NSPredicate *idPredicate = [NSPredicate predicateWithFormat:@"id == 2"];
NSArray *filteredCustomObjects = [customObjects filteredArrayUsingPredicate:idPredicate];

在上述代码中,首先将 JSON 数组转换为 CustomObject 对象数组,然后定义谓词 idPredicate 用于筛选 id 为 2 的对象,最后通过谓词对 CustomObject 数组进行过滤,得到符合条件的对象数组。通过这种方式,谓词可以与各种第三方库配合使用,灵活处理不同来源的数据。

谓词的性能优化与注意事项

  1. 性能优化
  • 避免复杂计算:在谓词表达式中,应尽量避免进行复杂的计算。例如,如果谓词中包含大量的数学运算或者字符串处理操作,会增加计算开销,影响性能。如果确实需要进行复杂计算,建议在构建谓词之前预先计算好结果,并将结果作为谓词的参数。
  • 合理使用索引:在 Core Data 等数据库相关的应用中,合理使用索引可以显著提高谓词查询的性能。例如,如果经常根据某个属性进行谓词筛选,在 Core Data 实体设计时,可以为该属性添加索引。这样在执行谓词查询时,数据库可以更快地定位符合条件的数据。
  • 批量处理:当对大量数据进行谓词操作时,批量处理可以提高效率。例如,在对数组进行过滤时,如果数组非常大,可以考虑将数组分成多个较小的子数组,分别应用谓词进行过滤,最后合并结果。这样可以减少内存占用,提高处理速度。
  1. 注意事项
  • 谓词格式正确性:编写谓词表达式时,要确保格式的正确性。不正确的格式可能导致运行时错误。例如,在谓词格式字符串中,占位符的使用必须与提供的参数数量和类型相匹配。如果使用 %@ 占位符,提供的参数必须是对象类型;如果使用 %d 等占位符,提供的参数必须是相应的基本数据类型。
  • 对象类型匹配:在应用谓词时,要确保谓词中引用的属性在对象中实际存在,并且类型匹配。例如,如果谓词中对某个对象的 age 属性进行比较,该对象必须确实有 age 属性,并且 age 的类型必须与谓词比较操作所期望的类型一致(如 NSNumber 类型用于数值比较)。
  • 内存管理:在使用谓词进行数据过滤和处理时,要注意内存管理。例如,当对大量数据进行过滤时,可能会生成新的数组或集合来存储过滤后的结果,要确保这些新生成的对象在不再使用时能够被正确释放,避免内存泄漏。

通过对谓词的性能优化和注意相关事项,可以在实际应用中更高效、稳定地使用谓词进行数据处理和操作。