NSPredicate一直是Apple提供的强大工具,允许开发者通过定义复杂的逻辑条件,以自然高效的方式过滤和评估数据集合。随着时间的推移,随着 Swift 语言的不断成熟和发展,2023 年,Swift 社区承担了使用纯 Swift 语言重构 Foundation 框架的任务。在这次重大更新中,引入了基于 Swift 编码的新 Predicate 功能,标志着数据处理和评估进入了新阶段。本文旨在探讨 Swift Predicate 在实际开发中的使用、结构以及重点注意事项。

什么是谓词?

在现代软件开发中,高效、准确地过滤和评估数据至关重要。谓词是一个强大的工具,允许开发人员通过定义返回布尔值(true 或 false)的逻辑条件来实现此目标。这不仅在过滤集合或查找集合中的特定元素方面发挥着关键作用,而且还是数据处理和业务逻辑实现的基础。

尽管 Apple 的 NSPredicate 提供了此功能,但它依赖于 Objective-C 语法,存在运行时错误的风险,并且面临平台限制,这使得限制了其在各种环境中的适用性和灵活性。

迅速

 

类 MyObject: NSObject {
 @objc 变量名称:字符串
 初始化(名称:字符串){
 self.name = 姓名
 }
}
让对象= MyObject(名称:“胖子”)

// 创建 NSPredicate
让谓词 = NSPredicate(格式: "name = %@", "fat")
XCTAssertTrue(predicate.evaluate(with: object)) // true

让 objs = [对象]
// 通过谓词过滤对象
让filteredObjs = (objs as NSArray).filtered(using: predicate) as! [我的对象]
XCTAssertEqual(filteredObjs.count, 1)

Swift谓词的介绍和改进

为了克服这些限制,扩大谓词的应用范围,Swift 社区重构了 Foundation 框架,引入了基于 Swift 语言的 Predicate 功能。这一新功能不仅消除了对 Objective-C 的依赖,还通过 Swift 的宏功能简化了谓词构造过程,如下所示:

迅速

 

{ 中的对象
object.name == “fat” && object.name.count < 3 } 尝试 XCTAssertTrue(predicate.evaluate(object)) // false' data-lang="text/x-swift">

let predicate = #Predicate{ 对象中
 object.name == "fat" && object.name.count < 3
}
尝试 XCTAssertTrue(predicate.evaluate(object)) // false

而且,当前的MyObject不需要继承自NSObject< /span> 或在其属性上使用 @objc 注释来支持 KVC。当然,Swift Predicate 也适用于仍然继承自 NSObject 的类型。

NSPredicate 和 Swift Predicate 的比较

与 NSPredicate 相比,Swift Predicate 提供了许多改进:

  • 开源和平台兼容性:支持跨平台使用,例如在 Linux 和 Windows 上。
  • 类型安全:利用 Swift 的类型检查来减少运行时错误。
  • 开发效率:受益于 Xcode 支持,提高代码编写速度和准确性。
  • 语法灵活性:提供更大的表达自由度,不受 Objective-C 语法规则的限制。
  • 多功能性:适用于所有 Swift 类型,而不仅仅是继承自 NSObject 的类型。
  • 支持现代 Swift 功能:支持 Sendable 和 Codable 等现代 Swift 功能,使其更适合当前的 Swift 编程范例。

通过这些改进,Swift Predicate 不仅优化了开发人员的工作流程,还为 Swift 生态系统的扩展和发展开辟了新途径。

Swift 谓词的主要组成部分

在深入了解 Swift Predicate 的使用和注意事项之前,有必要了解其结构。具体来说,我们应该理解Predicate由哪些元素构成以及Predicate宏如何发挥作用。

谓词表达式协议

PredicateExpression协议(或遵守该协议的具体类型)定义了表达式的条件逻辑。例如,它可以表示“小于”条件,包含特定的逻辑判断来确定输入值是否小于给定值。该协议是构建 Swift Predicate 架构的关键部分。 PredicateExpression协议的声明如下:

迅速

 

公共协议 PredicateExpression {
 关联类型输出
 
 func评估(_绑定:PredicateBindings)抛出->输出
}

基金会提供了一系列遵守预定义表达式类型PredicateExpression 协议,允许开发者直接使用 PredicateExpressions 下的类型或类型方法来构造谓词表达式。这为构建灵活且强大的条件评估逻辑铺平了道路。例如,如果我们要构造一个表示数字的表达式4,则对应的代码如下:

迅速

 

让express = PredicateExpressions.Value(4)

代码的实现="false">PredicateExpressions.Value 如下所示:

迅速

 

扩展谓词表达式 {
 公共结构值 <输出> : PredicateExpression {
 公共让值:输出
 
 公共初始化(_值:输出){
 自我价值=价值
 }
 
 公共函数评估(_绑定:PredicateBindings)->输出{
 回归自我价值
 }
 }
}

Value结构直接封装了一个值,并在评估<时简单地返回封装的值< /code> 方法被调用。这使得 Value 成为在谓词表达式中表示常量值的有效方法。

<块引用>

值得注意的是,PredicateExpressionevaluate方法> 可以返回任何类型的值,不限于布尔类型。

此外,如果我们需要定义一个表示条件的表达式3<4,则对应的代码示例如下:

迅速

 

让express = PredicateExpressions.build_Comparison(
 lhs: PredicateExpressions.Value(3),
 rhs: PredicateExpressions.Value(4),
 操作:.小于
)

此代码片段将生成一个符合 PredicateExpression 协议的实例:

迅速

 

PredicateExpressions.Comparison, PredicateExpressions.Value>

当调用该实例的evaluate方法时,会返回一个布尔值,表示判断的结果。

通过嵌套表达式的方法,开发者可以构造极其复杂的逻辑判断。同时,生成的类型表达式也相应变得复杂。

谓词结构

即使通过宏定义,Swift Predicate 的核心仍然是 Predicate 结构。该结构负责将逻辑条件(由 PredicateExpression 实现)与特定值绑定。此机制允许 Predicate 实例化特定的逻辑条件并接受输入值进行评估。

其定义如下:

迅速

 

公共结构谓词<每个输入>:可发送{
    public let 表达式:任何 StandardPredicateExpression
    public let 变量:(重复 PredicateExpressions.Variable<每个输入>)
    
    public init(_ builder: (repeat PredicateExpressions.Variable<每个输入>) -> 任何 StandardPredicateExpression) {
        self.variable = (重复 PredicateExpressions.Variable<每个输入>())
        self.expression = builder(重复每个变量)
    }
    
    公共函数评估(_输入:重复每个输入)抛出 - > Bool {
        尝试表达式.evaluate(
            .init(repeat (每个变量,每个输入))
        )
    }
}

主要功能包括:

  • 布尔值返回限制谓词专门处理返回布尔值的表达式。这意味着复杂表达式树的最终结果必须是布尔值,以方便逻辑判断。
  • 构造过程:构造谓词时,必须提供闭包。此闭包接收 PredicateExpressions.Variable 类型参数并返回遵循 StandardPredicateExpression< 的表达式/code>协议。
  • StandardPredicateExpression 协议:这是 的扩展PredicateExpression 协议,要求表达式也遵循 Codable可发送的协议。目前,只有基金会预设的表达方式才允许遵守该协议。
迅速

 

公共协议 StandardPredicateExpression :PredicateExpression、Codable、Sendable {}

  • 构造闭包和变量属性的高级功能:利用 Swift 的 参数包功能,谓词支持创建可以同时处理多个通用参数的谓词,该功能不支持可在 NSPredicate 中使用。

例如,利用 Predicate 结构和 PredicateExpression 协议,我们可以构造一个谓词示例来比较两个整数 nm (检查是否 n < m):

迅速

 

//定义一个闭包:比较两个整数值是否满足“小于”关系
// 这个闭包采用两个 PredicateExpressions.Variable 类型参数,
// 并构造一个表示“小于”比较逻辑的 PredicateExpression
让express = { (value1: PredicateExpressions.Variable, value2: PredicateExpressions.Variable) in
    PredicateExpressions.build_Comparison(
        左:值1,
        右旋:值2,
        操作:.小于
    )
}

// 使用 Express 闭包构造一个 Predicate 实例,
// 其中express定义了求值逻辑,即第一个参数是否小于第二个参数
让谓词 = 谓词 {
    快递($0, $1)
}

让 n = 3
设 m = 4

// 评估谓词:检查 n 是否小于 m,期望返回 true
尝试 XCTAssertTrue(predicate.evaluate(n, m))

谓词宏

与用字符串构造NSPredicate相比,直接使用PredicateExpressionPredicate 结构构建谓词提供了诸如类型安全检查和代码自动完成等优点。但这种方式效率较低,代码编写和阅读的复杂度也较高,这无疑增加了开发人员创建谓词时的心理负担。

为了降低这种复杂性,Foundation 引入了 Predicate 宏(#Predicate),旨在帮助开发者以更简洁、更简洁的方式构建 Swift Predicates。高效的方式。

再次以构造谓词判断是否n为例,使用宏可以显着简化流程:

let predicate = #Predicate{ $0 < $1}
让 n = 3
设 m = 4
尝试 XCTAssertTrue(predicate.evaluate(n,m)) // true

在Xcode中,通过检查宏展开后生成的代码,我们可以清楚地看到宏如何简化了之前需要大量代码的逻辑。

Predicate 宏的实现代码,内容如下:长 1200 行,仅支持 Foundation 中预定义的谓词表达式以及可在谓词中使用的特定方法。转换过程中,如果出现不支持的表达式类型、方法或找不到对应的表达式,将会抛出错误。

通过引入 Predicate 宏,Swift 提供了一种简洁而强大的方式来构建复杂的谓词逻辑,允许开发者在几乎原生的 Swift 代码中直接构建复杂的逻辑判断,显着增强了代码的可读性和可维护性。更重要的是,Predicate宏的使用大大减轻了开发者构造复杂查询时的心智负担,使开发工作流程更加顺畅、高效。

构建 Swift 谓词的提示和注意事项

了解了 Swift Predicate 的结构后,我们可以更准确地掌握构建 Predicate 所涉及的限制和技术。

全局函数的限制

使用 Predicate 宏构建谓词时,需要注意的是,宏的转换逻辑会将闭包代码转换为 Foundation 预定义的 PredicateExpress 表达式。 PredicateExpress 的当前实现不支持直接访问全局函数、方法或类型属性返回的数据。因此,在使用此类数据构造谓词时,应首先使用 let 关键字捕获所需的数据。例如:

迅速

 

func now() -> 日期 { 。现在 } let predicate = #Predicate{ $0 < now() } // 此谓词不支持全局函数

正确的做法是首先捕获函数或属性值,然后构造谓词:

迅速

 

让 now = now()
让 predicate = #Predicate{ $0 < now }

同样,直接访问类型属性也有限制:

迅速

 

let predicate = #Predicate{ $0 < Date.now }
// 键路径不能引用静态成员“now”

让现在 = Date.now
让 predicate = #Predicate{ $0 < now }

这是因为当前的谓词表达式只支持KeyPath,例如属性,不支持类型属性。

实例方法的限制

与上一点类似,也不支持在谓词内直接调用实例方法(例如 .lowercased())。

迅速

 

结构 A {
  变量名称:字符串
}

let predicate = #Predicate{ $0.name.lowercased() == "fat" } // 此谓词不支持 lowercased() 函数

在这种情况下,应该使用 Swift Predicate 支持的内置方法,例如:

目前可用的内置方法集合比较有限,包括但不限于:containsallSatisfyflatMap过滤器下标开始最小值最大值localizedStandardContainslocalizedComparecaseInsensitiveCompare官方文档或者直接参考Predicate宏的源代码全面了解最新支持的方法。

鉴于当前内置方法集并不全面,NSPredicate 中的一些常见谓词构造方法可能在 Swift Predicate 中尚不支持。这意味着,尽管 Swift Predicate 提供了用于构建类型安全和富有表现力的谓词的强大工具,但开发人员可能仍然需要寻找替代解决方案或等待未来的扩展以涵盖更广泛的用例。

支持具有多个通用参数的谓词

得益于参数包功能,Swift Predicate 允许定义可接受多个泛型参数的谓词,从而为开发人员提供了更大的灵活性。此功能显着扩展了谓词的适用性,使开发人员能够轻松处理各种复杂的条件评估。

正如前面的 n 示例所示,这种方法不仅限于比较单一类型的参数,还可以扩展与传统 Swift 高阶函数相比,进一步增强了 Swift Predicate 的表达能力和灵活性。这一特性使 Swift Predicate 成为构建复杂逻辑判断的强大工具,同时保持代码清晰性和类型安全性。

迅速

 

结构 A {
  变量名称:字符串
}

结构体 B {
  变量年龄:Int
}

让谓词 = #Predicate{ a,b in
  !a.name.isEmpty && b.age > 10
}

通过嵌套机制创建复杂的判断逻辑

Swift Predicate 的设计允许开发者通过嵌套谓词表达式构建复杂的谓词逻辑。此功能使得实现通常依赖于 NSPredicate 中的子查询的条件判断变得更加直观和简洁。如今,这些复杂的逻辑表达式可以更加符合Swift编程约定,增强代码的可读性和可维护性。

迅速

 

结构地址 {
  var 城市:字符串
}
结构人{
  变量地址:[地址]
}

让谓词 = #Predicate{ 人们在
  people.address.contains { 地址在
    地址.city == "大连"
  }
}

支持构建具有可选值的谓词

Swift Predicate 支持使用可选值类型,这在处理数据模型中常见的可选属性时是一个显着的优势。这种支持允许开发人员直接在谓词逻辑中处理可选值,使谓词表达式的编写更加简单和清晰。

例如,以下示例演示了如何在 Swift Predicate 中处理可选字符串属性,并根据其是否以特定前缀开头进行过滤:

迅速

 

let predicate = #Predicate {
  如果让 name = $0.name {
    返回 name.starts(with: "fat")
  } 别的 {
    返回错误
  }
}

对于有兴趣更深入地了解如何有效处理 Swift Predicate 中的可选值的开发人员,建议阅读 如何处理 SwiftData 谓词中的可选值

Swift 谓词是线程安全的

Swift Predicate的设计考虑了并发编程的需求,保证了其线程安全。通过遵守 Sendable 协议,Swift Predicate 支持不同执行上下文之间的安全通道。这一特性显着增强了 Swift Predicate 的实用性,使其能够适应现代 Swift 应用程序对并发和异步编程的广泛需求。

Swift 谓词支持序列化和反序列化

通过实现Codable协议,Swift Predicate可以转换为JSON或其他格式,从而实现数据的序列化和反序列化。对于需要将谓词条件保存到数据库或配置文件或需要在客户端和服务器之间共享谓词逻辑的场景,此功能尤其重要。

以下示例演示如何将 Predicate 实例序列化为 JSON 数据,然后可以存储或传输该数据:

构造复杂谓词时要注意对编译时间的影响

与在 SwiftUI 中构建接口时遇到的情况类似,在构造复杂的 Swift Predicate 表达式时,Swift 编译器需要将其处理并转换为庞大而复杂的类型。在此过程中,一旦表达式的复杂度超过一定阈值,编译器花在类型推断上的时间就会显着增加。

如果编译时间受到影响,开发人员可能会考虑将复杂的谓词声明放在单独的 Swift 文件中。这种方法不仅有助于组织和管理代码,而且还可以在一定程度上减少因频繁修改代码其他部分而触发的重新编译的需要。

尚不支持用于构建谓词的自定义谓词表达式

目前,虽然开发者可以创建符合 PredicateExpress 协议的自定义表达式类型,但官方指南不允许这些自定义表达式符合StandardPredicateExpression 协议。因此,尽管可以创建自定义表达式类型,但在构造谓词时不能直接使用这些自定义表达式。

即使开发人员将其自定义表达式标记为符合 StandardPredicateExpression 协议,Predicate 宏目前也仅支持使用 StandardPredicateExpression Foundation 中预定义的实现。此限制阻止开发人员在 Predicate 宏中使用自定义表达式,从而阻碍了使用自定义表达式构造谓词的能力。

尚不支持将多个谓词组合成更复杂的谓词

在构造NSPredicate时,开发者可以灵活组合多个简单逻辑NSPredicates 使用 NSCompoundPredicate 转换为更复杂的谓词。不过,Swift Predicate目前并没有提供类似的能力,这在一定程度上限制了开发者构建复杂谓词的灵活性。

在后续的文章中,我将介绍现阶段如何使用PredicateExpress动态构造复杂谓词,以满足特定需求。这种方法可能在某些情况下提供替代解决方案,以解决当前不支持多个谓词组合的限制。

在 SwiftData 中应用 Swift 谓词

在 SwiftData 和 Core Data 中使用 Predicates 作为数据检索条件是许多开发者的常见场景。了解 SwiftData 如何处理 Swift Predicates 对于最大化其效用至关重要。

SwiftData 和 Swift Predicate 交互机制

在 SwiftData 中为 FetchDescriptor 设置 Predicate 时,SwiftData 并不直接使用 Swift Predicate 的求值机制。相反,它解析由 Predicate 的 express 属性定义的表达式树,并将这些表达式转换为 SQL 语句以从 SQLite 数据库检索数据。这意味着,在 SwiftData 环境中,求值操作实际上是通过 SQLite 数据库中的 SQL 命令执行的,发生在数据库端。

SwiftData 中谓词参数的限制

SwiftData 要求每个 FetchDescriptor 对应一个特定的数据实体。因此,在构造谓词时,相应的实体类型成为谓词的唯一参数,这对于有效利用 SwiftData 构建谓词至关重要。

SwiftData 中谓词的表达式能力限制

尽管 Swift Predicate 提供了强大的数据过滤框架,但与使用 NSPredicate 和 Core Data 相比,它在 SwiftData 环境中的表达能力有些有限。面对特定的过滤需求,开发人员可能需要诉诸间接的方法,例如执行多个过滤器或为实体预先添加特定属性以适应当前的谓词能力。例如,由于内置的​​ starts 方法区分大小写,为了实现不区分大小写的匹配,建议创建一个预处理版本过滤属性(例如将其全部转换为小写)以支持更灵活的数据检索。

谓词运行时错误

即使 Swift Predicate 编译没有错误,但在使用 SwiftData 检索数据时,您可能会遇到无法成功转换为 SQL 语句的情况,从而导致运行时错误。考虑以下示例:

迅速

 

let predicate = #Predicate { $0.id == noteID }
// 运行时错误:Couldn't find \Note.id on Note with fields

虽然Note类型符合PersistentModel协议,其id 属性类型也是 PersistentIdentifier,当将谓词转换为 SQL 命令时,SwiftData 无法识别 id 属性。在这种情况下,开发人员应使用 persistentModelID 属性进行比较(在谓词转换期间,persistentModelID 是除了底层数据模型的相应属性之外的少数特别受支持的属性之一):

迅速

 

let predicate = #Predicate { $0.persistentModelID == noteID }

此外,尝试对 PersistentModel 的属性应用一组内置方法也可能会带来问题:

迅速

 

}
// 运行时错误:无法在带有字段的 Note 上找到 \Note.name.localizedLowercase" data-lang="text/x-swift">

let predicate = #Predicate {
  $0.name.localizedLowercase.starts(with: "abc".localizedLowercase)
}
// 运行时错误:无法在带有字段的 Note 上找到 \Note.name.localizedLowercase

当 SwiftData 转换这些表达式时,许多内置方法同样不适合 PercientModel 的属性,并且 SwiftData 错误地将它们视为 KeyPath 。因此,在此阶段,开发人员可能需要创建其他属性(例如属性的小写版本)来适应此类场景。

未达到预期结果的情况

在某些情况下,Swift Predicate 可以在 SwiftData 环境中顺利编译和运行,不会产生任何错误,但由于 SwiftData 错误地转换 SQL 命令,它可能无法检索到预期结果。下面的例子说明了这一点:

迅速

 

让谓词 = #Predicate {
  $0.note?.parent?.persistentModelID == rootNoteID
}

该谓词在编译和运行时不会出现任何问题,但最终无法正确获取数据。为了解决这个问题,我们需要以不同的方式构造具有相同逻辑的谓词,确保它能够正确处理可选值。有关更多详细信息,请参阅文章如何处理 SwiftData 中的可选值谓词

迅速

 

让谓词 = #Predicate {
  如果让 note = $0.note {
    返回 note.parent?.persistentModelID == rootNoteID
  } 别的 {
    返回错误
  }
}

<块引用>

因此,在构建 SwiftData 谓词时进行全面、及时的单元测试变得尤为重要。通过测试,开发人员可以验证谓词的行为是否符合预期,保证数据检索的准确性和应用程序的稳定性。

摘要

Swift Predicate为Swift开发者提供了强大而灵活的工具,使数据过滤和逻辑判断更加直观和高效。通过本文的讨论,我希望开发者不仅能够充分掌握 Swift Predicate 的强大功能和使用方法,而且能够在面临挑战和限制时找到创造性的解决方案。

Comments are closed.