一、假循环写法(替代Goto语句)
不推荐养成使用Goto语句的习惯,因为Goto语句是非结构化的,当然也可以严格写成结构化的,但是,严格的结构化Goto和直接使用Do语句几乎没有什么区别了。
Goto语句有很多缺点,比如多语言翻译障碍(很多语言已经删掉这玩意了),让代码看起来混乱,可能会导致出很多BUG等。
不要辩解说熟练的人写Goto也可以不出BUG,这东西稍微写点代码的人都不难做到,但我没必要把一个坑货属性满满的东西教给新手去用,总结下来就是:你当Goto不存在就好了。
假循环其实就是利用了 Do 语句有两个跳点的机制(Do一个跳点、Loop一个跳点),来控制代码跳来跳去,之所以叫“假循环”,是因为通常这种写法,都会自带一个 Exit Do 做结尾,整个语句不作为循环使用。
- // 经典假循环写法
- Do
- // 这里写你的执行代码
- If 1 = 1 The
- Exit Do
- End If
-
- // 无论如何,最终循环都会被跳出
- Exit Do
- Loop
复制代码当然,上面的这个例子没有有效利用到Do的跳点,在某些情况下,Do 跳点也可以很有用,使用 Continue 语句可以跳转到 Do 的位置:
- // 经典假循环写法
- Do
- // 这里写你的执行代码,这里替换为 Continue 了,所以会形成死循环,实际应用时可以产生很多种变化
- If 1 = 1 The
- Continue
- End If
-
- // 无论如何,最终循环都会被跳出
- Exit Do
- Loop
复制代码假循环在C语言代码中被广泛使用,例如 7z 源代码,就有许多运用循环语句实现巧妙跳转的写法。
单个假循环应用场景不多,看起来过于花哨似乎也没什么用,可一旦和下面的其他用法或者单纯的死循环配合起来,就可以实现多种变化。
二、带超时的特征等待
就是第一节课最后一个范例给的循环,循环负责实现超时退出功能,实现思路是在循环开始前计时,循环内发现对应的特征达成就会跳出,否则会在超时时间到达后跳出
- Dim iFlowRet = 0
- Dim ST = Timer()
- Do
- If FindElement(xxx) = 1 Then
- // 找到某个界面特征
- Log.Info("记录日志,发现了一个特征,流程将会继续执行")
- iFlowRet = 0
- Exit Do
- End If
- If FindElement(xxx) = 1 Then
- // 找到第二个界面特征
- Log.Info("记录日志,这是出错的情况,需要单独处理错误")
- iFlowRet = 1
- Exit Do
- End If
- If Timer() - ST > 30 Then
- Log.Info("xxx 功能执行超时(30秒),请检查环境或逻辑是否正常")
- iFlowRet = -1
- Exit Do
- End If
- Loop
复制代码这种循环最为基础,在工作流中往往承担着最基本的功能实现结构,日志也大多基于这类循环展开。
这种循环除了实现超时机制外,也可以负责功能实现,例如下面的代码,就是这类循环的完全体写法:
- // 伪代码,不能运行,仅展示逻辑
- Log.Info("流程点功能的描述,用于在日志里定位执行位置,开始执行 ...")
- Dim iFlowRet = 0
- Dim ST = Timer()
- Do
- If FindElement(xxx) = 1 Then
- // 找到某个界面特征
- Log.Info("记录日志,发现了一个特征(自动点击),流程将会继续执行")
- Mouse.Move(x, y)
- Mouse.Click(BTN_LEFT)
- iFlowRet = 0
- Exit Do
- End If
- If FindElement(xxx) = 1 Then
- // 找到第二个界面特征
- Log.Info("记录日志,这是出错的情况,需要单独处理错误")
- iFlowRet = 1
- Exit Do
- End If
- If Timer() - ST > 30 Then
- Log.Info("xxx 功能执行超时(30秒),请检查环境或逻辑是否正常")
- iFlowRet = -1
- Exit Do
- End If
- Loop
复制代码判断总是可以衔接一个操作,或者需要监控直到某个元素出现的情况,例如打开某个网页页面,需要关闭2个弹窗,点击一下确定,然后等待某个元素出现,则关闭弹窗的特征找到后不必跳出循环,点击确定也不必跳出循环,检测到点击确定后的界面某个元素出现,再跳出循环,则可以把一系列的操作整合在一个代码块里,并且保证结构不会过于混乱。
在这种写法的思维上稍加扩展,我们就可以实现 带优先级的特征识别了。
三、带优先级的特征识别
这类循环其实就是上一种循环的变化型。
上一种循环,我们可以检测多个特征,然后走不同的分支,跳出或者不跳出循环,由于代码执行的过程中,这一切都是不断检测执行的,没有办法确定特征出现时,代码必须执行到某个位置,因此我们可以下一个武断的结论,在这样的循环执行过程中,对于特征的检测和动作是乱序的。
但有时候,我们对操作的执行顺序非常敏感,例如第二步骤需要将鼠标悬浮在第一步骤的元素上,如果我们检测第一步骤元素是否存在,再点击第二步骤出现的元素,则无法确定会不会循环后继续执行的时候,脚本还是执行的第一步,从而破坏了已经执行过的环境变化。
这时候就需要对操作进行一个优先级分配了,比如一共3个敏感的动作,我们称为:第一步、第二步、第三步。
第三步建立在第二步的操作上,第二步又建立在第一步的操作上,这时候我们操作的优先级应该是第三步 大于 第二步 大于 第一步。
循环自带的跳点能力,让优先级实现起来非常容易,直接上一段范例代码,将上述两种类型的循环组合起来,就可以实现这样的功能了:
- // 伪代码,不能运行,仅展示逻辑
- Dim iFlowRet = 0
- Do
- Do
- If FindElement(xxx) = 1 Then
- // 找到某个界面特征,最高优先级
- Log.Info("记录日志,发现了一个特征(自动点击),流程将会继续执行")
- Mouse.Move(x, y)
- Mouse.Click(BTN_LEFT)
- iFlowRet = -1
- Exit Do
- End If
-
- If FindElement(xxx) = 1 Then
- // 找到第二个界面特征,第二优先级
- // 操作代码
- Exit Do
- End If
-
- // 不管什么情况,都会跳出这个循环
- Exit Do
- Loop
-
- // 任务完成后,跳出外层循环
- If iFlowRet = -1 Then
- Exit Do
- End If
- Loop
复制代码这也是一种非常经典的双循环结构,优先级怎么安排呢?假设一个操作是:第一步、第二步、第三步。
那么在写代码的时候,优先级最高的是第三步、其次是第二步,最后是第一步,倒着写。
为什么这样呢?因为第三步建立在第二步的基础上,第二步建立在第一步的基础上,所以执行的顺序就变成了这样:
第一轮循环:第三步(没检测到)、第二步(没检测到)、第一步(检测到了,开始操作)
……
第N次循环:第三步(没检测到)、第二步(检测到了,开始操作)、第一步(优先级较低,代码直接跳出了,所以不会执行到这里)
……
又N次循环:第三步(检测到了,开始操作)、第一步和第二步都不会执行。
但是一旦出错了,因为优先级的缘故,代码又会从第一步开始执行,从而使这种代码具备一定的自我修复能力。
四、一个任务出错重试N次
假循环是如此重要,以至于任何复杂的循环结构,都需要依赖这种技巧
任务重试机制实现起来也很简单,一个假循环负责处理逻辑,提供尾部跳点,然后再增加一个条件判断,来确定执行状态和重试次数就可以啦
和上一种用法一样,这个用法也必须先熟练的掌握假循环的用法
- Dim iReCount = 5 // 重试5次
- Dim iErrorCount = 0
- Dim iFlowRet = 0
- Do
- Do
- iFlowRet = 0
-
- // 这里执行各种检测
- If FindElement(...) = False Then
- // 检测到了失败的元素,执行失败了!
- iFlowRet = -1
- Exit Do
- End If
-
- // 工作流执行成功,跳出循环
- iFlowRet = 0
- Exit Do
-
- Loop
-
- // 重试机制实现
- If iFlowRet = 0 Then
- Log.Info(Format("【%s】流程执行成功!", sFlowName))
- Exit Do
- Else
- iErrorCount = iErrorCount + 1
- If iErrorCount > iReCount
- Log.Error(Format("【%s】流程尝试 %d 次全部失败。", sFlowName, iReCount))
- Exit Do
- Else
- Log.Error(Format("【%s】本次操作失败,将会在 10 秒后重新尝试。", sFlowName))
- Delay(5000)
- End If
- End If
-
- Loop
复制代码五、去除非必要的重试步骤
有时候我们需要流程能够记忆执行的位置,以便于通过简单的几个数据,来快速恢复调试现场。
这有点类似游戏的存档功能。
事实上,有过游戏开发经验的人都知道,游戏的逻辑一般是写死的,游戏提供一个全局的状态机(游戏全局数据管理对象),只需要将这个状态机的数据保存起来,就可以实现存档功能了,而独挡就是把数据写回到全局状态机里。
脚本多数情况下没有游戏复杂,实现这样的功能非常简单。
我们先整理一下前面几种写法的精髓:
假循环:通过循环为操作提供跳点,可以自由控制代码是跳到循环头还是循环尾,通过再嵌套一层循环,可以实现优先级控制或者出错重试机制,如果再嵌套两层循环,则两种机制可以兼得。
特征等待:一种阻塞的平坦状态机的实现,阻塞是说代码在执行的时候,除非有明确的状态,否则会在循环里反复进行状态监测,平坦是说,多个这样的语句结构组合在一起,可以吧一个非常复杂的流程,变的平坦,如果我们封装一个函数,然后依次调用这个函数执行一个流程的第一步、第二步、第三步……则整个过程是不需要条件语句和循环语句的,因此这种写法实际上是化复杂为简单。
优先级特征识别:通过双循环结构(工作循环 + 假循环),来让流程带有执行优先级属性,特别适合处理前后相关易出错的步骤,直到最后一个步骤达成,循环退出。
重试机制:通过双循环结构(重试循环 + 假循环),来实现执行出错的情况下重试多次,来提升整体的稳定性。
在组合用法的基础上,我们可以增加 Step 变量,然后将执行的数据代入,当脚本因为异常情况退出时,可以通过这个变量重新断点执行。
我们可以发现,几乎所有的功能实现,都是写在一个假循环里面的,因为假循环用来控制流程执行跳点,它很灵活,最适合用来承载逻辑,所以 Step 变量也必定是写在假循环里的。
这个写法我就不提供例子了,需要根据项目不同,自己组织数据的形式
记住一句话:程序的最终目的,一定是操作数据,各种形态的数据,包括按键的图色脚本,实际上操作的也不过是图色和坐标等数据。