如何避免短路求值问题

32

我正在使用Ruby on Rails工作,并希望验证两个不同的模型:

if (model1.valid? && model2.valid?)
...
end

然而,"&&" 运算符使用短路评估(即它只在 "model1.valid?" 为真时评估 "model2.valid?"),这会防止在 model1 无效时执行 model2.valids。

是否有一种不使用短路评估的 "&&" 等效物?我需要对这两个表达式进行评估。


2
出于好奇,你为什么需要同时评估两者?通常情况下,如果它们没有副作用的话,短路运算是可取的。而且,#valid?为什么会有副作用呢? - Iraimbilanja
这两个模型与同一个表单相关:最终,它们都必须经过验证才能执行操作。这没有任何副作用,只是因为在单个表单上有两个模型。 - Flackou
嗯...短路计算正是你想要的。如果model1无效,那么为什么还要检查model2呢? - Iraimbilanja
9
公平起见,同时验证两个模型可以让用户分别在两个模型上报告错误。如果您在修复模型1的错误后才发现模型2的验证错误,那将会很烦人。 - Paul Smith
确实如此,但要遵循责任分离原则。 #valid? 应该 验证,而不是通知用户错误。因此,“if”子句将在需要时输入所有和仅限的情况。然后,通知必须与验证分开进行 - 即,在 if 子句内部进行。 - Iraimbilanja
2
仅供参考,#valid? 不会通知用户错误。它只是将模型的错误数组填充了错误。然后控制器再次显示新建/编辑视图,以显示错误。 - Samuel
7个回答

28

试试这个:

[model1, model2].map(&:valid?).all?

如果两个实例都有效,则返回true,并在两个实例上创建错误。


使用 map 是多余的,Enumerable#all? 可以带一个块。 - crazymykl
4
注意,map(&:valid?) 步骤很重要:[model1, model2].all?(&:valid?) 是一种短路操作(如果 model1 中存在错误,则不会收集 model2 上的错误)。 - Elliot Nelson
对我来说,这比仅将一个valid?调用的结果设置为变量更冗长和混乱。如果您不想设置变量,也不想使用&运算符,因为其他人可能不理解,那么我建议在数组中像malclocke一样多调用两次valid?。 - cesoid
另一方面,如果您有三个或更多有效的检查,这可能是值得的。删除最外层括号可能也有帮助,我认为它们不是必需的,并且没有它们对我来说也可以工作。 - cesoid
这对我来说是最好的答案。最符合 Ruby 风格。想象一下一个大小未知的数组。同时,你不能使用 all?,因为它会在第一个 false 时短路,因此你不会在每个模型上得到错误消息。 - Kris
显示剩余2条评论

24

& 工作得很好。

irb(main):007:0> def a
irb(main):008:1> puts "a"
irb(main):009:1> false
irb(main):010:1> end
=> nil

irb(main):011:0> def b
irb(main):012:1> puts "b"
irb(main):013:1> true
irb(main):014:1> end
=> nil

irb(main):015:0> a && b
a
=> false

irb(main):016:0> a & b
a
b
=> false

irb(main):017:0> a and b
a
=> false

valid是一个方法,用于在ActiveRecord实例上运行验证,填充相关的错误信息,以便可以向用户报告。 - Codebeef
3
请注意,当使用 &、| 或 ^ 运算符时,要仔细处理左操作数,因为它们是仅在 TrueClass 和 FalseClass 上定义的方法。根据我所知... - Mike Woodhouse
1
马特先生说得好 - 同时报告两个模型的错误是一个值得追求的目标。已删除“坏代码气味”的评论 :) - Paul Smith
@Mike: 很有趣 - 我需要做更多的 irb 调查。 PS 我喜欢用 irb 来调查这种事情! - Paul Smith
有趣!我从来没有仔细研究过逻辑上的&和|,因为我自动将它们读作位运算。 - cpm
我喜欢这个想法,如果我更注重提高标准而不是立即可读性,我会这样做,但我认为大多数程序员不太可能立刻理解。我可以看到不同的人根据自己的情况做出不同的选择。 - cesoid

4
如何呢:
if [model1.valid?,model2.valid?].all?
  ...
end

对我来说没问题。


3
将它们分别评估并将结果存储在变量中。然后在这些布尔值之间使用简单的 &&。 :)

0

我曾经遇到过类似的问题,需要检查两个字符串是否都不为空。短路运算符阻止了我同时检查两个字符串,但我发现在Ruby中我们可以使用if not或unless。

return nil if not string1.blank? && string2.blank?

或者

return nil unless string1.present? && string2.present?

0

使得代码片段更易于维护的关键概念之一是其表达能力

让我们看下面的例子:

([model1, model2].map(&:valid?)).all?

或者

[model1.valid?,model2.valid?].all?

这两个都能很好地完成它们的工作,但是当未来的开发人员在没有看到任何解释性注释、文档或直接联系您的情况下遇到它们中的任何一个时,这个开发人员将修改它们,而不知道目的是为了避免短路评估。

如果没有测试,情况会变得更糟。

这就是为什么我建议引入一个小包装方法,它将立即使所有事情变得清晰明了。

def without_short_circuit_evaluation(*conditions)
  conditions.all?
end

然后在你的代码库中的某个地方。

if without_short_circuit_evaluation(model1.valid?, model2.valid?)
  # do something
end

-1

不必使用 map 创建额外的数组,你可以向 all? 方法传递一个块。

[model_instance_1, model_instance_2].all? {|i| i.valid? }

1
哇,虽然不像Matt的答案那么简短,但这个答案是有效的,比&好多了。 - Samuel
为什么这个答案会被踩?这个答案有没有什么不明显的问题? - Eliza Brock Marcum
1
@Paul 性能和晦涩。在旧版本的 Ruby 中,Symbol#to_proc 比传递块要慢得多。(不过对于两个元素来说并不是真正的问题。)它是核心库的一个相对较新的补充,利用了旧的但不常用的类型强制转换语法。 - cpm

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