VBA短路"And"的替代方案

21

VBA不支持短路

据称VBA不支持短路,因为它只具有位And/Or/Not等操作。根据VBA语言规范:"逻辑运算符是对操作数执行按位计算的简单数据运算符。"考虑到这一点,使用true =&H1111false =&H0000来设计VBA是有意义的:这样逻辑语句就可以作为按位运算进行评估。

缺乏短路会导致问题

  1. 性能:当评估此语句时,ReallyExpensiveFunction()将始终运行,即使左侧条件的结果并不需要它

    If IsNecessary() And ReallyExpensiveFunction() Then '... End If

  2. 错误:如果MyObj为Nothing,则此条件语句将导致运行时错误,因为VBA仍然会尝试检查Property的值

    If Not MyObj Is Nothing And MyObj.Property = 5 Then '... End If

我使用的解决方案是嵌套If语句来实现短路行为

If cond1 And cond2 Then
    '...
End If
成为
If cond1 Then
    If cond2 Then
        '...
    End If
End If

这种方式使得 If 语句具有类似短路的行为,如果 cond1False,则不必评估 cond2

如果有 Else 子句,则会创建重复的代码块

If Not MyObj Is Nothing And MyObj.Property = 5 Then
    MsgBox "YAY"
Else
    MsgBox "BOO"
End If
成为
If Not MyObj Is Nothing Then
    If MyObj.Property = 5 Then
        MsgBox "YAY"
    Else
        MsgBox "BOO" 'Duplicate
    End If
Else
    MsgBox "BOO" 'Duplicate
End If

有没有一种方法可以重新编写If语句以保留短路行为,但避免代码重复?

也许可以使用另一个分支语句,比如Select Case


为了给问题添加背景,这里是我关注的特定情况。我正在实现一个哈希表,通过在链表中链接它们来处理冲突。底层数组大小被强制为2的幂,并且散列值通过将其截断为适当的长度分布到当前数组大小中。

例如,假设数组长度为16(二进制为10000)。如果我有一个散列到27(二进制为11011)的键,则可以通过仅保留该数组大小限制内的位来将其存储在我的16个插槽数组中。此项目将存储的索引是(散列值)And(数组长度-1),在本例中为(二进制11011)And(1111),即1011,即11。实际的哈希码与键一起存储在插槽中。

在哈希表中查找项的时候,必须检查哈希和键以确定已找到正确的项。但是,如果哈希不匹配,则没有理由检查键。我希望通过嵌套If来获得短路行为,以获得微小而难以捉摸的性能提升:

While Not e Is Nothing
    If keyhash = e.hash Then
        If Key = e.Key Then
            e.Value = Value
            Exit Property
        Else
            Set e = e.nextEntry
        End If
    Else
        Set e = e.nextEntry
    End If
Wend

你可以看到 Set... 是重复的,因此出现了这个问题。


3
也许这是一个天真的问题,但是你能不能把 SET 这一行移到if语句外面,在while循环中放在最后一行之前呢?除了退出时不设置之外,其他时候都需要设置。顺便说一下,问得好! - Ioannis
@Ioannis - (拍打头部)。请将其作为答案。 :) - hnk
@loannis 我做不到,它太有意义了 ;) 谢谢指出我特定问题的真正解决方案,现在我正在更改我的代码...我会让剩下的问题自己保留下来,供后人参考。 - Blackhawk
3个回答

14

作为一种更通用的方法,我建议引入条件标志并利用将比较结果赋值给布尔值的用法:

dim cond1 as boolean
dim cond2 as boolean

cond1 = false
cond2 = false

' Step 1
cond1 = MyObj Is Nothing

' Step 2: do it only if step 1 was sucessful 
if cond1 then
    cond2 = MyObj.Property = 5
end if

' Final result:
if cond2 then
   msgbox "Yay"
else
   msgbox "Boo"
end if

通过“链接”这些条件标志,每一步都是安全的,您可以在最后一个条件标志中看到最终结果,并且不需要进行不必要的比较。对我来说,它保持了可读性。

编辑 2014-07-09

通常我从不省略块分隔符,因此我会将控制结构的每个语句都设置在新行上。但在这种情况下,您可以仔细地得到一种非常密集的符号表示法,它提醒了短路符号表示法,也因为VBA编译器启动了变量:

dim cond1 as boolean
dim cond2 as boolean
dim cond3 as boolean
dim cond4 as boolean

cond1 = MyObj Is Nothing
if cond1 then cond2 = MyObj.Property = 5
if cond2 then cond3 = MyObj.Property2 = constSomething
if cond3 then cond4 = not isNull(MyObj.Property77)

if cond4 then
   msgbox "Hyper-Yay"
else
   msgbox "Boo"
end if

我同意这个观点。它的阅读流程很清晰。

编辑于2021-03-21

感谢@Tom的评论,这可以写得更简单:

dim cond as boolean

cond = MyObj Is Nothing
if cond then cond = MyObj.Property = 5
if cond then cond = MyObj.Property2 = constSomething
if cond then cond = not isNull(MyObj.Property77)

if cond then
   msgbox "Hyper-Yay"
else
   msgbox "Boo"
end if

@Tom在下面的评论中解释了优点。我完全同意。只有在调试时,我才能想象出一些情况,当我希望具有条件的分离结果,并且因此明确使用四个不同的变量。


我喜欢这个。在适当的情况下使用条件标志,有时嵌套Ifs,函数和子程序采用ByRef而不是ByVal参数等。有许多方法可以解决这个问题,但它们都取决于特定的情况。 - David Zemens
2
谢谢。我同意有更多的方法,这取决于情况。嗯...还有一个通用的 function IfShortcut(ParamArray conditions()) as boolean ... 会很好...在那种情况下,您可以循环和 exit - peter_the_oak
嗯...不知怎么的,你必须检查最终状态。你可以像Tim Burton一样准备最终参数,并尝试避免使用if语句。但是这个模式需要最后的检查。 - peter_the_oak
我会为所有表达式使用一个单独的布尔变量。 a)没有必要(甚至在进行短路运算的语言中也不可能)先了解表达式的结果。 b)它消除了以下各项中错误输入错误的布尔变量后缀的重大可能性:b.1)最终的If语句,b.2)每个前导If语句和b.3)每个前导赋值语句。 c)这样可以消除为每个短路表达式声明额外的布尔变量的需要。 - Tom
1
谢谢@Tom,我已在答案中包含了你的提示。 - peter_the_oak
关于“我只能想象在调试时的某些情况,当我希望有条件的分离结果,并因此明确地使用四个不同的变量。”:由于每个表达式都被分配给一个变量,因此您仍然可以通过设置断点或逐步执行每个前导 If 语句来获得该值,您只是不能等到 之后 所有表达式都被评估后再检查先前的值。这种编码方式的优点之一是它比利用短路特性更好(即,您可以只在单个 Bool 变量上设置观察点(而不是每个表达式),并逐步执行它)。 - Tom

6

有一种方法。不能保证你会喜欢它。但在这种精心构建的情况下,Goto很实用。

If Not MyObj Is Nothing Then
    If MyObj.Property = 5 Then
        MsgBox "YAY"
    Else
        Goto JUMPHERE
    End If
Else
JUMPHERE:
    MsgBox "BOO" 'Duplicate
End If

一个用于实现短路条件的简短代码!

或者,如果不是MsgBox "BOO"而是一些冗长复杂的代码,则可以将其封装在一个函数中,并且可以最小化影响/开销地重复两次。


关于特定用例,多个Set操作将具有最小的性能影响,因此,如果想要避免使用Goto仍然是全球效率最高的方法,代码大小+性能方面,避免创建虚拟变量等-对于这么小的代码没有影响),只需重复命令即可。

仅为了分析(您的示例代码)不同方法可以获得多少收益...

  • 如果两个条件都为真:有2个比较,1个赋值,0个跳转
  • 如果仅第一个条件为真:有2个比较,1个指针赋值,1个跳转
  • 如果仅第二个条件为真:有1个比较,1个指针赋值,1个跳转
  • 如果两个条件都为假:有1个比较,1个指针赋值,1个跳转(与上面相同)

就性能而言,跳转通常比比较更昂贵(在ALU中非常快速地发生比较,而跳转可能会导致代码缓存中断,也许在这些大小下不会有影响,但跳转仍然很昂贵)。

并且普通赋值按值的速度最好与指针赋值一样快,或者有时更差(这是VBA,无法100%确定p-code实现)

因此,根据您的用例/预期数据,您可以尝试将循环中每次迭代的平均跳转次数最小化,并重新排列代码。


1
函数包装器 +1。我使用它们(可能过度),但它确实有助于保持您的代码可读性和易于维护。 - David Zemens
2
+1 "你不一定会喜欢它" :P 这肯定能解决问题,但是那个 Goto 如果我不小心的话就会让我的代码出现很多问题。你认为跳出分支或循环语句会导致问题吗? - Blackhawk
2
不,内部的C++/VBA的p-code等都会将你的分支转换为Goto,所以没有实现问题。只有哲学问题。但是反对Goto的论点(主要是Djikstra的)是从避免意大利面条代码的角度出发的。我进行任何优化的方式是,先编写整洁的代码。然后将其全部注释掉,并将超级优化的代码放在旁边。这样,可以阅读常规代码以获取逻辑和调试信息,而超级优化的代码则经过彻底审核以获得真正的性能。在这种情况下,特别是Goto使阅读更容易-去除了代码中的意大利面条! - hnk
3
对于纯粹主义者:您可以使用布尔标志而不是GOTO来获得相同的结果。 - Joel Coehoorn
1
有效使用GoTo...我不想说,但++。 - RubberDuck
显示剩余3条评论

0

这样怎么样:

s = "BOO"

If Not MyObj Is Nothing Then
    If MyObj.Property = 5 Then s = "YAY"
End If

MsgBox s

但是,“BOO”可能是您希望尽可能避免执行的“昂贵”函数。是否有替代方法来解决这种情况? - hnk
不幸的是,在我处理的实际案例中,Else子句实际上有一个Set赋值 :( 不过,像这样的方法在那里肯定能起作用,但我不确定它是否会取消我使用嵌套的If所希望的微小性能提升。我会修改问题并添加特定案例以提供更多背景信息。看看你的想法! - Blackhawk
我知道这已经很老了,但对于未来的读者来说,避免昂贵的函数以及混乱的版本(例如GoTo)所需的全部是检查我们试图创建的变量的null版本。 在这种情况下,“如果s = vbNullString,则s = ExpensiveFunction”。 干净而且合乎逻辑。 - Brandon Barney

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接