适用于 macOS、iOS、tvOS 和 watchOS 的 Swift 友好的有限状态机语法
Swift FSM 的灵感来自 Uncle Bob 的 SMC 语法,它是一个用于指定和操作有限状态机 (FSM) 的 Swift DSL。
本指南假定您熟悉 FSM,特别是上面链接的 SMC 语法。 Swift FSM 大量使用 @resultBuilder
块、运算符重载、callAsFunction()
和 尾随闭包,所有这些都相互结合使用 - 熟悉这些概念很有帮助。
Swift FSM 是一个适用于所有 Apple 平台的 Swift Package,可通过 Swift Package Manager 获得,并且需要 Swift 6 或更高版本。 它仅限于 macOS 15、iOS 18、tvOS 18 和 watchOS 11 或更高版本。
建议使用 Swift 6 语言模式 - 它应该可以与仍使用 Swift 5 语言模式的项目一起使用,但是可能会出现警告,并且某些环境中可能会出现编译错误(请参阅Swift 并发和Swift 6 语言模式)。
它有一个依赖项 - Apple 的 Algorithms。
我们将使用地铁旋转栅门系统来镜像 SMC 的示例。 此旋转栅门具有两种状态:Locked
和 Unlocked
,以及两个事件:Coin
和 Pass
。
逻辑如下(来自 Uncle Bob,重点强调)
- 给定 我们处于 Locked 状态,当 我们收到 Coin 事件时,那么 我们转换到 Unlocked 状态并 调用 unlock 动作。
- 给定 我们处于 Locked 状态,当 我们收到 Pass 事件时,那么 我们保持在 Locked 状态并 调用 alarm 动作。
- 给定 我们处于 Unlocked 状态,当 我们收到 Coin 事件时,那么 我们保持在 Unlocked 状态并 调用 thankyou 动作。
- 给定 我们处于 Unlocked 状态,当 我们收到 Pass 事件时,那么 我们转换到 Locked 状态并 调用 lock 动作。
SMC
Initial: Locked
FSM: Turnstile
{
Locked {
Coin Unlocked unlock
Pass Locked alarm
}
Unlocked {
Coin Unlocked thankyou
Pass Locked lock
}
}
Swift FSM
let turnstile = FSM<State, Event>(initialState: .locked)
try turnstile.buildTable {
define(.locked) {
when(.coin) | then(.unlocked) | unlock
when(.pass) | then(.locked) | alarm
}
define(.unlocked) {
when(.coin) | then(.unlocked) | thankyou
when(.pass) | then(.locked) | lock
}
}
Swift FSM(带有其他上下文代码)
import SwiftFSM
class MyClass: SyntaxBuilder {
enum State { case locked, unlocked }
enum Event { case coin, pass }
let turnstile = FSM<State, Event>(initialState: .locked)
func myMethod() async throws {
try turnstile.buildTable {
define(.locked) {
when(.coin) | then(.unlocked) | unlock
when(.pass) | then(.locked) | alarm
}
define(.unlocked) {
when(.coin) | then(.unlocked) | thankyou
when(.pass) | then(.locked) | lock
}
}
await turnstile.handleEvent(.coin)
}
}
class MyClass: SyntaxBuilder {
SyntaxBuilder
协议提供指定转换表所需的 define
、when
和 then
方法。 它有两个关联类型 State
和 Event
,它们必须是 Hashable & Sendable
。
let turnstile = FSM<State, Event>(initialState: .locked)
FSM
是泛型类型,适用于 State
和 Event
。 在这里,我们使用 enum
将 FSM 的初始状态指定为 .locked
。
try turnstile.buildTable {
turnstile.buildTable
是一个抛出错误的函数 - 尽管类型系统会阻止各种不合逻辑的语句,但仍然存在一些语义问题只能在运行时检测到。
define(.locked) {
define
语句大致对应于 FSM 的自然语言描述中的“Given”关键字。 但是,预计每个状态只会编写一个 define
。
define
接受两个参数 - 一个 State
和一个 @resultBuilder
块。
when(.coin) | then(.unlocked) | unlock
|
(管道)运算符将 when
、then
和 actions 绑定到离散的转换中。 它将左侧的输出馈送到右侧的输入,就像您在终端中预期的那样。
由于我们位于 define
块内,因此我们将 .locked
状态作为给定状态。 我们现在逐行列出我们的转换。 when
我们收到 .coin
事件时,我们将 then
转换到 .unlocked
状态并调用函数 unlock
。
由于 unlock
是对函数的引用,因此也可以声明如下
when(.coin) | then(.unlocked) | { unlock() //; otherFunction(); etc. }
两种类型的函数作为 Swift FSM 动作有效
@isolated(any) () async -> Void
@isolated(any) (Event) async -> Void
如果希望将关联值与事件 enum
一起传递到回调函数,则接受 Event
的 Actions 很有用(有关如何实现此操作的更多详细信息,请参阅使用事件传递值,有关组合不同类型操作列表的方法,请参阅操作数组)。
await turnstile.handleEvent(.coin)
由于 handleEvent
可能会调用 async
动作,因此 handleEvent
本身也必须是 async
。
FSM
将为其当前状态找到适当的转换,调用关联的函数,并转换到关联的下一个状态。 在这种情况下,我们调用 unlock
函数并转换到 unlocked
状态。 如果未找到任何转换,则不会发生任何事情,并且如果为调试编译,则会在控制台中打印警告消息。
如果传递一个操作数组,您可能希望使用 Swift FSM 提供的便利的 &
运算符重载,以便能够混合和匹配不同的操作签名
when(.coin) | then(.unlocked) | first & secondAsync & thirdWithEvent ...
这等效于(尽管在技术上并不完全相同)更详细但同样有效的
when(.coin) | then(.unlocked) | { event in await first(); await secondAsync(); thirdWithEvent(event) ... }
现在让我们添加一个报警状态,必须由维修人员重置
SMC
Initial: Locked
FSM: Turnstile
{
Locked {
Coin Unlocked unlock
Pass Alarming alarmOn
Reset - {alarmOff lock}
}
Unlocked {
Reset Locked {alarmOff lock}
Coin Unlocked thankyou
Pass Locked lock
}
Alarming {
Coin - -
Pass - -
Reset Locked {alarmOff lock}
}
}
Swift FSM
try turnstile.buildTable {
define(.locked) {
when(.coin) | then(.unlocked) | unlock
when(.pass) | then(.alarming) | alarmOn
when(.reset) | then() | alarmOff & lock
}
define(.unlocked) {
when(.reset) | then(.locked) | alarmOff & lock
when(.coin) | then(.unlocked) | thankyou
when(.pass) | then(.locked) | lock
}
define(.alarming) {
when(.coin) | then()
when(.pass) | then()
when(.reset) | then(.locked) | alarmOff & lock
}
}
没有参数的 then()
表示“没有状态更改” - FSM 保持在其当前状态。 操作管道也是可选的 - 如果转换不执行任何操作,则可以省略它。
请注意 Reset 转换的重复。 在所有三种状态下,Reset 事件都执行相同的操作。 它转换到 Locked 状态,并调用 lock 和 alarmOff 操作。 可以通过使用超级状态来消除这种重复,如下所示
SMC
Initial: Locked
FSM: Turnstile
{
// This is an abstract super state.
(Resetable) {
Reset Locked {alarmOff lock}
}
Locked : Resetable {
Coin Unlocked unlock
Pass Alarming alarmOn
}
Unlocked : Resetable {
Coin Unlocked thankyou
Pass Locked lock
}
Alarming : Resetable { // inherits all it's transitions from Resetable.
}
}
Swift FSM
try turnstile.buildTable {
let resetable = SuperState {
when(.reset) | then(.locked) | alarmOff & lock
}
define(.locked, adopts: resetable) {
when(.coin) | then(.unlocked) | unlock
when(.pass) | then(.alarming) | alarmOn
}
define(.unlocked, adopts: resetable) {
when(.coin) | then(.unlocked) | thankyou
when(.pass) | then(.locked) | lock
}
define(.alarming, adopts: resetable)
}
SuperState
采用与 define
相同的 @resultBuilder
,但没有起始状态。 起始状态取自传递给它的 define
语句。 然后 define
将添加在每个 SuperState
实例中声明的转换,然后再添加在 define
中声明的其他转换。
如果将 SuperState
实例传递给 define
,则 @resultBuilder
参数是可选的。
SuperState
实例可以采用其他 SuperState
实例,并将它们与 define
组合在一起
let s1 = SuperState { when(.coin) | then(.unlocked) | unlock }
let s2 = SuperState { when(.pass) | then(.alarming) | alarmOn }
let s3 = SuperState(adopts: s1, s2)
// s3 is equivalent to:
let s4 = SuperState {
when(.coin) | then(.unlocked) | unlock
when(.pass) | then(.alarming) | alarmOn
}
在 SuperState
中声明的转换不能被其采用者覆盖。 因此,假定以下代码存在错误并抛出
let s1 = SuperState { when(.coin) | then(.unlocked) | unlock }
let s2 = SuperState(adopts: s1) {
when(.coin) | then(.locked) | beGrumpy // 💥 error: clashing transitions
}
define(.locked, adopts: s1) {
when(.coin) | then(.locked) | beGrumpy // 💥 error: clashing transitions
}
要覆盖 SuperState
转换,您必须使用 overriding { }
块
let s1 = SuperState { when(.coin) | then(.unlocked) | unlock }
let s2 = SuperState(adopts: s1) {
overriding {
when(.coin) | then(.locked) | beGrumpy // ✅ overrides inherited transition
}
}
define(.locked, adopts: s1) {
overriding {
when(.coin) | then(.locked) | beGrumpy // ✅ overrides inherited transition
}
}
由于允许多重继承,因此覆盖会替换所有匹配的转换
let s1 = SuperState { when(.coin) | then(.unlocked) | doSomething }
let s2 = SuperState { when(.coin) | then(.unlocked) | doSomethingElse }
define(.locked, adopts: s1, s2) {
overriding {
when(.coin) | then(.locked) | doYetAnotherThing // ✅ overrides both inherited transitions
}
}
如果在没有要覆盖的内容的情况下使用 overriding
,则 FSM 将抛出错误
define(.locked) {
overriding {
when(.coin) | then(.locked) | beGrumpy // 💥 error: nothing to override
}
}
在父级而不是子级中编写 overriding
将抛出错误
let s1 = SuperState {
overriding {
when(.coin) | then(.locked) | beGrumpy
}
}
let s2 = SuperState(adopts: s1) { when(.coin) | then(.unlocked) | unlock }
// 💥 error: overrides are out of order
尝试在同一个 SuperState { }
或 define { }
中覆盖会抛出错误
define(.locked) {
when(.coin) | then(.locked) | doSomething
overriding {
when(.coin) | then(.locked) | doSomethingElse
}
}
// 💥 error: duplicate transitions
在此作用域中,单词 override 没有意义,因此会被错误处理程序忽略。 剩下的就是重复的转换,从而导致错误。
覆盖遵循通常的继承规则。 在覆盖链中,最后一个转换优先
let s1 = SuperState { when(.coin) | then(.unlocked) | a1 }
let s2 = SuperState(adopts: s1) { overriding { when(.coin) | then(.unlocked) | a2 } }
let s3 = SuperState(adopts: s2) { overriding { when(.coin) | then(.unlocked) | a3 } }
let s4 = SuperState(adopts: s3) { overriding { when(.coin) | then(.unlocked) | a4 } }
define(.locked, adopts: s4) {
overriding { when(.coin) | then(.unlocked) | a5 } // ✅ overrides all others
}
turnstile.handleEvent(.coin) // 'a5' is called
在前面的示例中,每次进入 Alarming 状态时都打开警报,并在每次退出 Alarming 状态时关闭警报的事实都隐藏在几个不同转换的逻辑中。 我们可以使用进入动作和退出动作来明确这一点。
SMC
Initial: Locked
FSM: Turnstile
{
(Resetable) {
Reset Locked -
}
Locked : Resetable <lock {
Coin Unlocked -
Pass Alarming -
}
Unlocked : Resetable <unlock {
Coin Unlocked thankyou
Pass Locked -
}
Alarming : Resetable <alarmOn >alarmOff - - -
}
Swift FSM
try turnstile.buildTable {
let resetable = SuperState {
when(.reset) | then(.locked)
}
define(.locked, adopts: resetable, onEntry: lock*) {
when(.coin) | then(.unlocked)
when(.pass) | then(.alarming)
}
define(.unlocked, adopts: resetable, onEntry: unlock*) {
when(.coin) | then(.unlocked) | thankyou
when(.pass) | then(.locked)
}
define(.alarming, adopts: resetable, onEntry: alarmOn*, onExit: alarmOff*)
}
onEntry
和 onExit
指定在进入或离开定义的状态时要执行的操作数组。 由于 Swift 的函数匹配算法对采用多个闭包参数的函数存在限制,因此这些需要数组语法,而不是更方便的 varargs。
由于该数组是异构的(它可以包括两种操作类型中的任何一种),因此提供了一个特殊的后缀运算符 *
,用于将其中一个转换为 AnyAction
数组。
_ = unlock* // preferred syntax, same as...
_ = Array(unlock) // same as...
_ = [AnyAction(unlock)]
_ = unlock & thankyou // preferred syntax, same as...
_ = AnyAction(unlock) & thankyou // same as...
_ = AnyAction(unlock) & AnyAction(thankyou) // same as...
_ = [AnyAction(unlock), AnyAction(thankyou)]
SuperState
实例也接受进入和退出动作
let resetable = SuperState(onEntry: lock*) {
when(.reset) | then(.locked)
}
define(.locked, adopts: resetable) {
when(.coin) | then(.unlocked)
when(.pass) | then(.alarming)
}
// equivalent to:
define(.locked, onEntry: lock*) {
when(.reset) | then(.locked)
when(.coin) | then(.unlocked)
when(.pass) | then(.alarming)
}
SuperState
实例也会从其超状态继承进入和退出动作
let s1 = SuperState(onEntry: unlock*) { when(.coin) | then(.unlocked) }
let s2 = SuperState(onEntry: alarmOn*) { when(.pass) | then(.alarming) }
let s3 = SuperState(adopts: s1, s2)
// s3 is equivalent to:
let s4 = SuperState(onEntry: [unlock, alarmOn]) {
when(.coin) | then(.unlocked)
when(.pass) | then(.alarming)
}
在 SMC 中,即使状态没有更改,也会始终调用进入和退出动作。 因此,在所有进入 Unlocked
状态的转换中,始终会调用 unlock 进入动作。
Swift FSM 的默认行为是 仅在存在状态更改时 调用进入和退出动作。 在上面的示例中,这意味着在 .unlocked
状态下,在 .coin
事件之后,不 调用 unlock
。
如果将 .executeAlways
传递给 FSM.init
,Swift FSM 将与 SMC 匹配。 默认值为 .executeOnChangeOnly
,并且不是必需的。
FSM<State, Event>(initialState: .locked, actionsPolicy: .executeAlways)
所有语句必须以 define { when | then | actions }
的形式进行。 有关此规则的例外情况,请参阅扩展语法。
为了方便起见,when
语句接受 vararg Event
实例。
define(.locked) {
when(.coin, or: .pass, ...) | then(.unlocked) | unlock
}
// equivalent to:
define(.locked) {
when(.coin) | then(.unlocked) | unlock
when(.pass) | then(.unlocked) | unlock
...
}
Actions 可以接收导致调用它们的事件。 SwiftFSM 需要一个特殊的结构 FSMValue<T>
和协议 EventWithValues
,它们一起工作以使您能够执行此操作。
enum Event: EventWithValues {
case .coin(FSMValue<Int>), ...
var coinValue: Int? {
guard case .coin(let amount) = event else { return nil }
return amount.wrappedValue
}
}
func main() throws {
try turnstile.buildTable(initialState: .locked) {
define(.locked) {
when(.coin(.any)) | then(.verifyingPayment) | verifyPayment
// here we use .any to match any value
}
}
try turnstile.handleEvent(.coin(50))
// here we pass a specific value that will be matched by .any
}
func verifyPayment(_ event: Event) {
// here we receive the actual value passed to handleEvent: .coin(50)
if let amount = event.coinValue {
if amount >= requiredAmount {
letThemThrough()
} else {
insufficientPayment(shortfall: requiredAmount - amount)
}
}
}
when(.coin(.any))
以多态方式工作,匹配 .coin(someValue)
内的任何值,并将 someValue
传递给 verifyPayment
函数。
如果没有 EventWithValues
和 FSMValue<T>
的组合,则必须按如下方式编写表
try turnstile.buildTable(initialState: .locked) {
define(.locked) {
when(.coin(1)) | then(.verifyingPayment) | verifyPayment
when(.coin(2)) | then(.verifyingPayment) | verifyPayment
when(.coin(3)) | then(.verifyingPayment) | verifyPayment
when(.coin(4)) | then(.verifyingPayment) | verifyPayment
... // and so on for all relevant values
}
}
通过使用 EventWithValues.any
,当收到 .coin
事件时,无论包装的值如何,都会激活到 .verifyingPayment
的转换。 然后,该包装的值将传递到可以检查它的 verifyPayment
函数。 FSMValue
提供了一个方便的 var wrappedValue: T?
,它返回一个可选值(如果在 .any
实例上调用它或如果 T
是可选的并且为 nil,则可能为 nil)。
FSMValue
遵循 ExpressibleByIntegerLiteral
、ExpressibleByFloatLiteral
、ExpressibleByArrayLiteral
、ExpressibleByDictionaryLiteral
、ExpressionByNilLiteral
和 ExpressionByStringLiteral
协议,并在相关情况下转发到包装的类型。 它还转发对 Equatable
、Comparable
和 AdditiveArithmetic
的一致性(如果适用),以及 RandomAccessCollection
及其父协议(对于数组)和字典的下标访问。 它转发 CustomStringConvertible
,这也涵盖了 ExpressibleByStringInterpolation
的大多数用法。
一些例子
let s: FSMValue<String> = "1" // equivalent to .some("1")
let i: FSMValue<Int> = 1 // equivalent to .some(1)
let ai: FSMValue<[Int]> = [1] // equivalent to .some([1])
_ = s + "1" // "11"
_ = i + 1 // 2
_ = ai[0] // 1
_ = ai[0] == i // true
_ = ai[0] > i // false
_ = "\(i)\(s)" // "11"
警告: 如果包装类型上可以使用转发操作,请注意,如果您尝试访问 .any
实例上的值,这将导致崩溃(很像强制解包一个 nil 可选值 - 在这种意义上,.any
是一个空值)。 因此,.any
应该只出现在 define 语句中 - 在任何情况下,将带有 FSMValue.any
的事件传递给 handleEvent
都是没有用处或意义的。
在继续之前,您应该始终解包 FSMValue<T>
实例 - 事实上,所有返回值的便利方法都返回 T
的实例,而不是 FSMValue<T>
的实例。
SwiftFSM 中的 @resultBuilder
代码块不支持控制流逻辑。 尽管可以启用此类逻辑,但会产生误导。
define(.locked) {
if something { // ⛔️ does not compile
when(.pass) | then(.unlocked) | unlock
} else {
when(.pass) | then(.alarming) | alarmOn
}
...
}
如果 if/else
代码块在转换时由 FSM 评估,这将是有用的。 然而,我们正在做的是编译我们的状态转换表(SMC 代表状态机编译器)。 以这种方式使用 if
和 else
类似于使用 #if
和 #else
- 只会编译一个转换。
有关在运行时而不是编译时评估条件语句的替代系统,请参阅 扩展语法。
Swift FSM 不对其客户端的并发处理提出要求。 FSM
类上的公共方法以多态方式隔离到调用者的 Actor
(如果存在),或者根本没有 Actor
。 这是通过在所有公共方法签名中包含参数 isolation: isolated (any Actor)? = #isolation
来实现的。
Swift FSM 在任何并发或非并发环境中透明地工作。 然而,从技术上来说,可以(尽管不切实际)从不同的 actor 调用 FSM
类的每个公共方法,因为 actor 多态目前在单个函数级别而不是在类级别工作。
FSM
有一个可选的运行时并发检查器,如果您尝试从冲突的并发环境中调用其方法,则该检查器会使 precondition
检查失败。 可以通过将 enforceConcurrency: true
传递给 FSM.init
来启用此检查。 此检查仅在为调试构建时运行。
class MyClass {
let fsm: FSM<Int, Int>
init(fsm: FSM<Int, Int>) {
self.fsm = fsm
}
func one() async { await fsm.handleEvent(1) }
@MainActor
func two() async { await fsm.handleEvent(1) }
}
let fsm = FSM<Int, Int>(initialState: 1, enforceConcurrency: true)
let c = MyClass(fsm: fsm)
try fsm.buildTable {
define(1) { when(2) | then() }
}
// ✅ First call sets the actor for future calls
await c.one()
// ✅ Same 'NonIsolated' as first call
await c.two()
// 💥 Concurrency violation: handleEvent called by MainActor (expected NonIsolated)
虽然 FSM
如果从 main actor 调用其方法,则会在 main actor 上运行,但在 Swift 提供一种跨整个类统一多态 actor 行为的方法之前,Swift FSM 还提供了一个方便的包装器 FSM<State, Event>.OnMainActor
,并用 @MainActor
注释,允许编译器强制隔离,而无需使用可选的运行时检查器。
但在大多数情况下,在 main actor 上下文中,FSM
或 FSM.OnMainActor
的行为不会有任何区别 - OnMainActor
只是防止编译时出现不太可能发生的边缘情况。
系统的一些细微之处(在 Swift 6 语言模式下编译时)
@MainActor
class MyMainActorClass {
func myMethod() {
// ✅ Called with Main Actor isolation
let fsm = FSM<Int, Int>(initialState: 1)
// ✅ Called with Main Actor isolation
let mainActorFSM = FSM<Int, Int>.OnMainActor(initialState: 1)
}
func myAsyncMethod() async {
// ✅ Called with Main Actor isolation
let fsm = FSM<Int, Int>(initialState: 1)
// ✅ Called with Main Actor isolation
let mainActorFSM = FSM<Int, Int>.OnMainActor(initialState: 1)
}
}
class MyNonIsolatedClass {
func myMethod() {
// ✅ Called without isolation
let fsm = FSM<Int, Int>(initialState: 1)
// ⛔️ Call to main actor-isolated initializer 'init(type:initialState:actionsPolicy:)' in a synchronous nonisolated context
let mainActorFSM = FSM<Int, Int>.OnMainActor(initialState: 1)
}
func myAsyncMethod() async {
// ✅ Called without isolation
let fsm = FSM<Int, Int>(initialState: 1)
// ✅ Called with Main Actor isolation
let mainActorFSM = await FSM<Int, Int>.OnMainActor(initialState: 1)
}
@MainActor
func myMainActorMethod() {
// ✅ Called with Main Actor isolation
let fsm = FSM<Int, Int>(initialState: 1)
// ✅ Called with Main Actor isolation
let mainActorFSM = FSM<Int, Int>.OnMainActor(initialState: 1)
}
}
actor MyCustomActor {
func myMethod() {
// ✅ Called with MyCustomActor isolation
let fsm = FSM<Int, Int>(initialState: 1)
// ⛔️ Call to main actor-isolated initializer 'init(type:initialState:actionsPolicy:)' in a synchronous nonisolated context
let mainActorFSM = FSM<Int, Int>.OnMainActor(initialState: 1)
}
func myAsyncMethod() async {
// ✅ Called with MyCustomActor isolation
let fsm = FSM<Int, Int>(initialState: 1)
// ✅ Called with Main Actor isolation
let mainActorFSM = await FSM.OnMainActor<Int, Int>(initialState: 1)
}
@MainActor
func myMainActorMethod() {
// ✅ Called with Main Actor isolation
let fsm = FSM<Int, Int>(initialState: 1)
// ✅ Called with Main Actor isolation
let mainActorFSM = FSM<Int, Int>.OnMainActor(initialState: 1)
}
}
大多数 Swift FSM 函数调用和初始化器采用额外的“magic”参数 file: String = #file
和 line: Int = #line
。 一些还采用 isolation: isolated (any Actor)? = #isolation
。
由于这些无法隐藏,请注意,不太可能有任何理由使用替代值覆盖这些默认参数。
所有代码块必须至少包含一个语句
try turnstile.buildTable { } //💥 error: empty table
try turnstile.buildTable {
define(.locked) { } // 💥 error: empty block
}
如果转换共享相同的起始状态、事件和下一个状态,则它们是重复的。
try turnstile.buildTable {
define(.locked) {
when(.coin) | then(.unlocked) | unlock
when(.coin) | then(.unlocked) | lock
}
}
// 💥 error: duplicate transitions
当转换共享相同的起始状态和事件,但它们的下一个状态不同时,它们会发生冲突。
try turnstile.buildTable {
define(.locked) {
when(.coin) | then(.unlocked) | unlock
when(.coin) | then(.locked) | lock
}
}
// 💥 error: logical clash
虽然这两个转换是不同的,但它们不能共存 - .coin
事件必须导致 .unlocked
状态或 .locked
状态。 它不能同时导致两者。
因为 .any
匹配所有情况,所以以下代码会抛出异常
try turnstile.buildTable(initialState: .locked) {
define(.locked) {
when(.coin(.any)) | then(.verifyingPayment) | verifyPayment
when(.coin(50) | then(.unlocked) | pass
}
}
//💥 error: logical clash
.any
情况已经包括所有情况,从而产生歧义。 可以编写以下代码
try turnstile.buildTable(initialState: .locked) {
define(.locked) {
when(.coin(20) | then(.verifyingPayment) | verifyPayment
when(.coin(50) | then(.unlocked) | pass
}
}
// ✅ transitions are logically distinct
对 turnstile.buildTable { }
的其他调用将抛出 TableAlreadyBuiltError
。
每次调用 handleEvent()
的性能为 O(1)。 然而,它仍然具有相当于嵌套 switch case 语句的 2-3 倍的运行开销。 Swift FSM 以性能换取便利性,不适合资源受限的环境。
虽然 Swift FSM 匹配了 SMC 的大多数语法,但它也引入了一些自己的新可能性。
让我们想象一下对我们的旋转栅门规则的扩展:在某些时候,我们想要通过在仍然 .locked
的情况下检测到 .pass
时进入警报状态来强制执行“每个人都付费”规则。 在其他时候,也许在高峰时段,我们希望更加宽松。
我们可以在系统的其他地方实现一天中的时间检查,可能像这样
try turnstile.buildTable {
...
define(.locked) {
when(.pass) | then(.alarming) | handleAlarm
}
...
}
// elsewhere in the system...
enum Enforcement: Predicate { case weak, strong }
let enforcement = Enforcement.weak
func handleAlarm() {
switch enforcement {
case .weak: smile()
case .strong: defconOne()
}
}
但我们现在在转换表中声明了我们状态转换逻辑的一些方面,而在其他地方声明了其他方面。 并且我们仍然转换到 .alarming
状态,而不管 Enforcement
策略如何。 如果不同的策略需要完全不同的转换怎么办?
我们可以引入额外的事件来区分新策略
try turnstile.buildTable {
...
define(.locked) {
when(.passWithEnforcement) | then(.alarming) | defconOne
when(.passWithoutEnforcement) | then(.locked) | smile
}
...
}
现在我们可以调用不同的函数并转换到不同的状态,具体取决于强制执行策略,同时将我们的逻辑保留在转换表中。
最初响应 .pass
事件的每个转换现在都需要编写两次,一次用于此事件的两个新版本中的每一个,即使它们都是相同的。 状态转换表将变得难以管理,并且充满重复。
Swift FSM 解决方案
import SwiftFSM
class MyClass: ExpandedSyntaxBuilder {
enum State { case locked, unlocked }
enum Event { case coin, pass }
enum Enforcement: Predicate { case weak, strong }
let fsm = FSM<State, Event>(initialState: .locked)
func myMethod() throws {
try turnstile.buildTable {
...
define(.locked) {
matching(Enforcement.weak) | when(.pass) | then(.locked) | smile
matching(Enforcement.strong) | when(.pass) | then(.alarming) | defconOne
when(.coin) | then(.unlocked)
}
...
}
turnstile.handleEvent(.pass, predicates: Enforcement.weak)
}
}
我们引入了函数 matching
和两个协议 ExpandedSyntaxBuilder
和 Predicate
。
define(.locked) { matching(Enforcement.weak) | when(.pass) | then(.locked) | smile matching(Enforcement.strong) | when(.pass) | then(.alarming) | defconOne when(.coin) | then(.unlocked) | unlock }
假设我们处于 .locked
状态
Enforcement
是 .weak
,当我们得到 .pass
时,转换为 .locked
并调用 smile
Enforcement
是 .strong
,当我们得到 .pass
时,转换为 .alarming
并调用 defconOne
Enforcement
是什么,当我们得到 .coin
时,转换为 .unlocked
并调用 unlock
只有那些依赖于 Enforcement
策略的语句才知道已添加它 - 预先存在的语句继续按原样工作。
ExpandedSyntaxBuilder
使用相同的要求实现 SyntaxBuilder
。 Predicate
要求一致性类型为 Hashable, Sendable
和 CaseIterable
。 可以使用任何类型,但在实践中,CaseIterable
要求可能会将 Predicate
限制为没有关联类型的 Enums
。
when(.coin) | then(.unlocked)
指定 Predicate
时,它是从转换的上下文中推断出来的。 推断的范围在 turnstile.buildTable { }
的大括号之间。 这就是为什么此函数只能调用一次的原因之一。
在我们的示例中,类型 Enforcement
出现在表中的其他位置的 matching
语句中,Swift FSM 将推断出缺少的 matching
语句
when(.coin) | then(.unlocked)
// is inferred to mean:
matching(Enforcement.weak) | when(.coin) | then(.unlocked)
matching(Enforcement.strong) | when(.coin) | then(.unlocked)
因此,默认情况下,转换与 Predicate
无关,除非另有说明,否则匹配任何 Predicate
。 matching
是一个可选修饰符,它约束转换到一个或多个特定的 Predicate
大小写。
可以使用的 Predicate
类型数量没有限制(有关实际限制,请参阅 Predicate 性能)。
enum Enforcement: Predicate { case weak, strong }
enum Reward: Predicate { case positive, negative }
try turnstile.buildTable {
...
define(.locked) {
matching(Enforcement.weak) | when(.pass) | then(.locked) | lock
matching(Enforcement.strong) | when(.pass) | then(.alarming) | alarmOn
when(.coin) | then(.unlocked) | unlock
}
define(.unlocked) {
matching(Reward.positive) | when(.coin) | then(.unlocked) | thankyou
matching(Reward.negative) | when(.coin) | then(.unlocked) | idiot
when(.pass) | then(.locked) | lock
}
...
}
await turnstile.handleEvent(.pass, predicates: Enforcement.weak, Reward.positive)
相同的推理规则仍然适用
when(.coin) | then(.unlocked) | unlock
// types Enforcement and Reward appear elsewhere in context
// when(.coin) | then(.unlocked) is now equivalent to:
matching(Enforcement.weak, and: Reward.positive) | when(.coin) | then(.unlocked) | unlock
matching(Enforcement.strong, and: Reward.positive) | when(.coin) | then(.unlocked) | unlock
matching(Enforcement.weak, and: Reward.negative) | when(.coin) | then(.unlocked) | unlock
matching(Enforcement.strong, and: Reward.negative) | when(.coin) | then(.unlocked) | unlock
假设当前状态为 .locked
,则调用 handleEvent
的结果将是保持在 .locked
状态并调用 lock
函数。
可以通过填充 and: Predicate...
和 or: Predicate...
参数,在单个 matching
语句中组合多个谓词。
enum A: Predicate { case x, y, z }
enum B: Predicate { case x, y, z }
enum C: Predicate { case x, y, z }
matching(A.x, or: A.y)... // if A.x OR A.y
matching(A.x, or: A.y, A.z)... // if A.x OR A.y OR A.z
matching(A.x, and: B.x)... // if A.x AND B.x
matching(A.x, and: B.x, C.x)... // if A.x AND B.x AND C.x
matching(A.x, or: A.y, A.z, and: B.x, C.x)... // if (A.x OR A.y OR A.z) AND B.x AND C.x
matching(A.x, or: B.x)... // ⛔️ does not compile: OR types must be the same
matching(A.x, and: A.y)... // 💥 error: cannot match A.x AND A.y simultaneously
turnstile.handleEvent(.coin, predicates: A.x, B.x, C.x)
matching(and:)
意味着我们期望两个谓词同时存在,而 mathing(or:)
意味着我们期望存在任何一个且仅一个。
Swift FSM 期望传递给 handleEvent
的表中存在的每种 Predicate
类型只有一个实例,如示例中所示,其中 turnstile.handleEvent(.coin, predicates: A.x, B.x, C.x)
包含类型 A
、B
和 C
的单个实例。 因此,永远不应发生 A.x AND A.y
- 只能存在一个。 因此,传递给 matching(and:)
的谓词必须都属于不同的类型。
matching(or:)
为单个 Predicate
类型指定了多个可能性。 因此,由 or
连接的谓词必须都属于同一类型,并且尝试将不同的 Predicate
类型传递给 matching(or:)
将无法编译(有关此限制的更多信息,请参阅 隐式冲突)。
define(.locked) {
matching(Enforcement.weak) | when(.coin) | then(.unlocked)
matching(Reward.negative) | when(.coin) | then(.locked)
}
// 💥 error: implicit clash
这两个转换似乎不同,但是
define(.locked) {
matching(Enforcement.weak) | when(.coin) ...
// inferred as:
matching(Enforcement.weak, and: Reward.positive) | when(.coin) ...
matching(Enforcement.weak, and: Reward.negative) | when(.coin) ... // 💥 clash
matching(Reward.negative) | when(.coin) ...
// inferred as:
matching(Enforcement.weak, and: Reward.negative) | when(.coin) ... // 💥 clash
matching(Enforcement.strong, and: Reward.negative) | when(.coin) ...
我们可以通过消除至少一个语句的歧义来打破僵局
define(.locked) {
matching(Enforcement.weak, and: Reward.positive) | when(.coin) | then(.unlocked)
matching(Reward.negative) | when(.coin) | then(.locked)
}
// ✅ inferred as:
define(.locked) {
matching(Enforcement.weak, and: Reward.positive) | when(.coin) | then(.unlocked)
// matching(Enforcement.weak, and: Reward.negative) ... removed by disambiguation
matching(Enforcement.weak, and: Reward.negative) | when(.coin) | then(.locked)
matching(Enforcement.strong, and: Reward.negative) | when(.coin) | then(.locked)
}
在某些情况下,Swift FSM 可以消除歧义而无需打破僵局
define(.locked) {
matching(Enforcement.weak, and: Reward.positive) | when(.coin) | then(.unlocked)
matching(Enforcement.weak) | when(.coin) | then(.locked)
}
// ✅ inferred as:
define(.locked) {
matching(Enforcement.weak, and: Reward.positive) | when(.coin) | then(.unlocked)
matching(Enforcement.weak, and: Reward.negative) | when(.coin) | then(.locked)
}
Swift FSM 优先处理指定最大数量谓词的语句 - 在这种情况下,第一个语句 matching(Enforcement.weak, and: Reward.positive)
指定了两个谓词,胜过第二个语句的单个谓词 matching(Enforcement.weak)
。
从本质上讲,Reward.positive
已经被更明确的转换“声明”,只留下剩余的 Reward.negative
用于不太明确的转换。
不允许通过“OR”连接不同的类型
define(.locked) {
matching(Enforcement.weak, or: Reward.negative) | when(.coin) | then(.unlocked)
}
// ⛔️ does not compile, because it implies:
define(.locked) {
matching(Enforcement.weak) | when(.coin) | then(.unlocked)
matching(Reward.negative) | when(.coin) | then(.unlocked)
}
// 💥 error: implicit clash
matching(Enforcement.weak) | when(.pass) /* duplication */ | then(.locked)
matching(Enforcement.strong) | when(.pass) /* duplication */ | then(.alarming)
when(.pass)
是重复的。 我们可以使用上下文块来分解它
when(.pass) {
matching(Enforcement.weak) | then(.locked)
matching(Enforcement.strong) | then(.alarming)
}
完整的例子现在是
try turnstile.buildTable {
define(.locked) {
when(.pass) {
matching(Enforcement.weak) | then(.locked)
matching(Enforcement.strong) | then(.alarming)
}
when(.coin) | then(.unlocked)
}
}
then
和 matching
也支持上下文代码块
try turnstile.buildTable {
define(.locked) {
then(.unlocked) {
when(.pass) {
matching(Enforcement.weak) | doSomething
matching(Enforcement.strong) | doSomethingElse
}
}
}
// or identically:
define(.locked) {
when(.pass) {
then(.unlocked) {
matching(Enforcement.weak) | doSomething
matching(Enforcement.strong) | doSomethingElse
}
}
}
}
try turnstile.buildTable {
define(.locked) {
matching(Enforcement.weak) {
when(.coin) | then(.unlocked) | somethingWeak
when(.pass) | then(.alarming) | somethingElseWeak
}
matching(Enforcement.strong) {
when(.coin) | then(.unlocked) | somethingStrong
when(.pass) | then(.alarming) | somethingElseStrong
}
}
}
actions
也可用于上下文代码块
try turnstile.buildTable {
define(.locked) {
actions(someCommonFunction) {
when(.coin) | then(.unlocked)
when(.pass) | then(.alarming)
}
}
}
matching(predicate) {
// everything in scope matches 'predicate'
}
when(event) {
// everything in scope responds to 'event'
}
then(state) {
// everything in scope transitions to 'state'
}
actions(functionCalls) {
// everything in scope calls 'functionCalls'
}
上下文代码块分为两组 - 可以进行逻辑链接(或 AND 运算)的代码块,以及不能进行逻辑链接的代码块。
一个状态转换响应单个事件并转换到单个状态。因此,多个 when { }
和 then { }
语句不能进行 AND 运算。
define(.locked) {
when(.coin) {
when(.pass) { } // ⛔️ does not compile
when(.pass) | ... // ⛔️ does not compile
matching(.something) | when(.pass) | ... // ⛔️ does not compile
matching(.something) {
when(.pass) { } // ⛔️ does not compile
when(.pass) | ... // ⛔️ does not compile
}
}
then(.unlocked) {
then(.locked) { } // ⛔️ does not compile
then(.locked) | ... // ⛔️ does not compile
matching(.something) | then(.locked) | ... // ⛔️ does not compile
matching(.something) {
then(.locked) { } // ⛔️ does not compile
then(.locked) | ... // ⛔️ does not compile
}
}
}
存在 when { }
和 then
的特定组合,这种组合无法编译,因为在响应单个事件(在本例中为 .coin
)时,除非为每个事件提供不同的 Predicate
,否则不可能转换到多个状态。
define(.locked) {
when(.coin) {
then(.unlocked) | action // ⛔️ does not compile
then(.locked) | action // ⛔️ does not compile
}
}
define(.locked) {
when(.coin) {
matching(Enforcement.weak) | then(.unlocked) | action // ✅
matching(Enforcement.strong) | then(.locked) | otherAction // ✅
}
}
这些代码块可以按如下方式构建成链
define(.locked) {
matching(Enforcement.weak) {
matching(Reward.positive) { } // ✅ matches Enforcement.weak AND Reward.positive
matching(Reward.positive) | ... // ✅ matches Enforcement.weak AND Reward.positive
}
actions(doSomething) {
actions(doSomethingElse) { } // ✅ calls doSomething and doSomethingElse
... | doSomethingElse // ✅ calls doSomething and doSomethingElse
}
}
嵌套的 actions
代码块会累加动作并执行所有动作。
嵌套的 matching
语句通过 AND 运算组合在一起,这可能会无意中造成冲突。
define(.locked) {
matching(A.x) {
matching(A.y) {
// 💥 error: cannot match A.x AND A.y simultaneously
}
}
}
matching(or:)
语句也使用 AND 运算组合
define(.locked) {
matching(A.x, or: A.y) {
matching(A.z) {
// 💥 error: cannot match A.x AND A.z simultaneously
// 💥 error: cannot match A.y AND A.z simultaneously
}
}
}
有效的嵌套 matching(or:)
语句按如下方式组合
define(.locked) {
matching(A.x, or: A.y) {
matching(B.x, or: B.y) {
// ✅ logically matches (A.x OR A.y) AND (B.x OR B.y)
// internally translates to:
// 1. matching(A.x, and: B.x)
// 2. matching(A.x, and: B.y)
// 3. matching(A.y, and: B.x)
// 4. matching(A.y, and: B.y)
}
}
}
管道可以而且必须在代码块内部使用,而代码块不能在管道之后打开。
define(.locked) {
when(.coin) | then(.unlocked) { } // ⛔️ does not compile
when(.coin) | then(.unlocked) | actions(doSomething) { } // ⛔️ does not compile
matching(.something) | when(.coin) { } // ⛔️ does not compile
}
使用 Predicate 是一种通用的解决方案,但在某些情况下,它可能比解决给定问题所需的复杂性更高(有关 matching
开销的说明,请参阅Predicate 性能)。
如果需要在运行时使特定转换成为条件性的,则 condition
语句可能就足够了。
define(.locked) {
condition(complexDecisionTree) | when(.pass) | then(.locked) | lock
}
complexDecisionTree()
是一个返回 Bool
的函数。如果为 true
,则执行转换,否则,不执行任何操作。
condition
在语法上与 matching
可互换 - 它可以与管道和代码块语法一起使用,并且可以链接。
matching
和 condition
可以自由组合
define(.locked) {
condition({ reward == .positive }) {
matching(Enforcement.weak) | then(.unlocked) | action
matching(Enforcement.strong) | then(.locked) | otherAction
}
}
condition
在它可以表达的逻辑方面比 matching
更有限
define(.locked) {
when(.coin) {
matching(Enforcement.weak) | then(.unlocked) | action
matching(Enforcement.strong) | then(.locked) | otherAction
}
} // ✅ all good here
...
define(.locked) {
when(.coin) {
condition { enforcement == .weak } | then(.unlocked) | action
condition { enforcement == .strong } | then(.locked) | otherAction
}
} // 💥 error: logical clash
无法区分不同的 condition
语句,因为 () -> Bool
是不透明的。剩下的就是两个 define(.locked) { when(.coin) | ... }
语句,这两个语句都转换到不同的状态 - FSM 无法决定调用哪个语句,因此会 throw
。
为了保持性能,turnstile.handleEvent(event:predicates:)
没有错误处理。因此,传入未出现在转换表中的 Predicate
实例不会导致错误。尽管如此,FSM 将无法执行任何转换,因为它不包含任何与意外 Predicate
匹配的语句。调用者有责任确保传递给 handleEvent
的谓词和转换表中使用的谓词类型和数量相同。
try turnstile.buildTable { }
执行重要的错误处理,以确保表在语法和语义上有效。
扩展语法还会抛出以下其他错误
有两种方法可以创建无效的 matching
语句。第一种是使用单个语句
matching(A.a, and: A.b) // 💥 error: cannot match A.a AND A.b simultaneously
matching(A.a, or: B.a, and: A.b) // 💥 error: cannot match A.a AND A.b simultaneously
matching(A.a, and: A.a) // 💥 error: duplicate predicate
matching(A.a, or: A.a) // 💥 error: duplicate predicate
matching(A.x, or: B.x)... // ⛔️ does not compile: OR types must be the same
matching(A.x, and: A.y)... // 💥 error: cannot match A.x AND A.y simultaneously
第二种是在代码块中 AND 运算多个 matching
语句
matching(A.a, and: B.a) { // ✅
matching(A.a) // 💥 error: duplicate predicate
matching(A.b) // 💥 error: cannot match A.a AND A.b simultaneously
}
matching(A.a, or: A.b) { // ✅
matching(A.a) // 💥 error: duplicate predicate
matching(A.b) // 💥 error: duplicate predicate
}
请参阅隐式冲突
概述:对于具有 100 个转换、3 种 Predicate
类型以及每种 Predicate
10 个用例的表,每个函数调用的操作数
.eager |
.lazy |
调度 | |
---|---|---|---|
handleEvent |
1 | 1-7 | 每个转换 |
buildTable |
100,000 | 100 | 应用程序加载时一次 |
添加谓词不会影响 handleEvent()
的性能,但会降低 fsm.buildTable { }
的性能。默认情况下,“eager” FSM 通过在创建转换表时提前完成大量工作,为所有隐含的 Predicate
组合填充缺失的转换,从而保持 handleEvent()
的运行时性能 O(1)。
假设使用了任何谓词,fsm.buildTable { }
主要由表的“填充”操作主导。因为必须为每个转换计算和过滤给定谓词的所有可能用例组合,所以性能为 O(m^n*o),其中 m 是每个谓词的平均用例数,n 是 Predicate
类型的数量,o 是转换的数量。
在具有 100 个转换的表中,使用三种具有 10 个用例的 Predicate
类型将需要 100,000 次操作才能编译。在大多数实际用例中,这不太可能成为问题。
注意:较少使用关键字 matching
没有任何优势。一旦使用了单词 matching
,并且将 Predicate
实例传递给 handleEvent()
,无论使用多少次,整个表的性能影响都将相同。
如果您的表特别大(请参阅上面的概述),Swift FSM 提供了一种更平衡的替代方案。将 .lazy
参数传递给 FSM<State, Event>(type: .lazy)
避免了前瞻算法,从而减少了内部表的大小并加快了表编译时间。代价是在每次调用 handleEvent()
时进行多次表查找操作。
handleEvent()
的性能从 O(1) 降低到 O(n!),其中 n
是使用的 Predicate
*类型* 的数量,而与用例的数量无关。相反,buildTable { }
的性能从 O(m^n*o) 提高到 O(n),其中 n
是转换的数量。
现在,在具有 100 个转换的表中,使用三种具有 10 个用例的 Predicate
类型将需要 100 次操作才能编译(从 .eager
的 100,000 次降至)。每次调用 handleEvent()
都需要执行 1 到 3! + 1
或 7 次操作(从 .eager
的 1 次升至)。因此,在这种情况下,不建议使用三种以上的 Predicate
类型,因为性能会以阶乘方式降低。
在大多数情况下,.eager
是首选解决方案,而 .lazy
则为特别大量的转换和/或 Predicate
用例保留。
如果不使用任何谓词,则两个实现都是相同的。
尽管 Swift FSM 运行时错误包含对问题的详细描述,但对于消除编译器错误的歧义,几乎无法提供帮助。
熟悉 @resultBuilder
的工作方式,以及它倾向于生成的编译时错误的种类,将有助于理解您可能遇到的任何错误。几乎所有特定于 Swift FSM 的编译时错误都将由未识别的 @resultBuilder
参数和过度重载的 |
运算符的未识别参数产生。
为了提供帮助,这里简要列出如果您尝试构建 Swift FSM 在编译时不允许构建的内容,您可能会遇到的一些常见错误
在对静态方法“buildExpression”的调用中没有完全匹配
这是 @resultBuilder
代码块中的常见编译时错误。如果您将代码块提供给它不支持的参数,则会发生这种情况。记住此类代码块中的每一行实际上都是提供给静态方法的参数是很有用的。
例如
try turnstile.buildTable {
actions(thankyou) { }
// ⛔️ No exact matches in call to static method 'buildExpression'
}
这里,actions
代码块作为参数提供给支持 buildTable
函数的 @resultBuilder
上的隐藏静态函数 buildExpression
。define
语句已被跳过,并且 actions
返回外部代码块不支持的类型,因此无法编译。
无法将类型 <T1> 的值转换为预期的参数类型 <T2>
这在将不受支持的参数传递给管道重载的情况下很常见。
例如
try turnstile.buildTable {
define(.locked) {
then(.locked) | unlock
// ⛔️ Cannot convert value of type 'Internal.Then<TurnstileState>' to expected argument type 'Internal.MatchingWhenThen'
// ⛔️ No exact matches in call to static method 'buildExpression'
}
}
在调用 then(.locked)
之前没有 matching
和/或 when
语句。没有 |
重载在左侧接受 then(.locked)
的输出,在右侧接受代码块 () -> ()
,因此无法编译。
不幸的是,该错误会输出一些无法隐藏的内部实现细节(见下文)
它还会产生一个次要错误 - 因为它无法确定 then(.locked) | unlock
的输出是什么,所以它声明没有可用于 buildExpression
的重载。修复底层的 |
错误,此错误也会消失。
引用 'SIMD' 上的运算符函数 '|' 要求 'Internal.When<TurnstileEvent>' 符合 'SIMD'
try turnstile.buildTable {
define(.locked) {
when(.coin) | matching(P.a) | then(.locked) | unlock
// ⛔️ Referencing operator function '|' on 'SIMD' requires that 'Internal.When<TurnstileEvent>' conform to 'SIMD’
}
}
when
和 matching
的顺序颠倒且不受支持。这与之前的错误没有什么不同,但编译器对问题的解释不同。它从一个不相关的模块中选择一个 |
重载,并声明它被误用。
编译器无法帮助识别链中哪个管道导致了问题。通常,只需删除并重写语句,而不是试图弄清楚问题是什么,会更简单。
try turnstile.buildTable {
let resetable = SuperState {
when(.reset) | then(.locked)
}
define(.locked, adopts: resetable, onEntry: [lock]) {
when(.coin) | then(.unlocked)
when(.pass) | then(.alarming)
}
define(.unlocked, adopts: resetable, onEntry: [unlock]) {
when(.coin) | then(.unlocked) | thankyou
when(.pass) | then(.locked)
}
define(.alarming, adopts: resetable, onEntry: [alarmOn], onExit: [🦤])
}
这是来自进入和退出操作的原始示例,在末尾插入了一个小错误。这可能会也可能不会在渡渡鸟旁边产生适当的错误
无法在作用域中找到 '🦤'
它还会生成 SuperState
声明中的多个虚假错误和修复,类似于此错误
闭包中调用方法“then”需要显式使用“self”来明确捕获语义
显式引用“self.” [修复]
显式捕获“self”以在此闭包中启用隐式“self”
忽略这些错误,如果没有显示其他错误,您可能需要寻找未识别的参数。
该项目主要在于需要捕获客户端函数。通过 Swift 5 后半部分的演进引入的并发规则越来越限制可以执行此操作的方式,以防止数据竞争。
这些规则并不一致,在某些情况下,Swift 5.10 的行为比 Swift 6.0 更具限制性。因此,仅当使用 Swift 6 语言模式时,才能保证 Swift FSM 按预期工作。
使用 Swift 5 语言模式可能会有效,但不能保证。
为了构建语法,SyntaxBuilder
和 ExpandedSyntaxBuilder
中声明的每个方法都需要返回一个中间对象,FSM 使用该对象将转换表中的每个条目链接在一起。每次调用 |
都必须输出“某些东西”,即使该东西与用户无关。尽管它们的实现被标记为 internal
,并且应该无法访问或修改,但您可能会在编译错误和自动完成建议中看到对其中一些对象的引用。
Swift FSM 是使用测试驱动开发编写的,作为一个非 UI 框架,它保持 100% 代码覆盖率的要求。覆盖率不能保证测试质量,但*缺乏*覆盖率确实保证了*缺乏*测试质量。
“100%”规则的例外情况是未执行的代码 - Swift 对抽象类的拒绝仍然需要使用 fatalError("subclasses must implement")
模式,在协议无法胜任工作或无法干净利落地完成工作的情况下使用。
尽管如此,该项目仍然尽可能地尊重标准的 Swift 实践,只要这些实践不影响可测试性或产生重复代码。如果出现影响或重复,可测试性和代码去重始终优先。随着时间的推移,目标是在出现合理机会时,将“非 Swift”解决方案重构为“更 Swift”的解决方案。
如果您确实遇到已执行但未被测试覆盖的代码,请提交问题,因为缺乏覆盖率是一个严重的错误和流程故障。