让我说得很清楚,因为人们一直误解这一点:
子表达式的求值顺序独立独立于关联性和优先级。关联性和优先级决定 运算符 的执行顺序,但 不 决定 子表达式 的评估顺序。您的问题是关于评估 子表达式 的顺序。
考虑A() + B() + C() * D()。乘法的优先级高于加法,加法是左结合的,所以这相当于(A() + B()) + (C() * D()) 但知道这只会告诉你第一次加法将在第二次加法之前发生,并且乘法将在第二次加法之前发生。 它不会告诉你 A()、B()、C() 和 D() 的调用顺序!(它也不会告诉你乘法是发生在第一个之前还是之后另外。)通过将其编译为:
完全有可能遵守
优先级和关联性的规则:
d = D() // these four computations can happen in any order
b = B()
c = C()
a = A()
sum = a + b // these two computations can happen in any order
product = c * d
result = sum + product // this has to happen last
这里遵循所有优先级和关联性规则——第一次加法发生在第二次加法之前,乘法发生在第二次加法之前。显然,我们可以按任何顺序调用 A()、B()、C() 和 D(),并且仍然遵守优先级和关联性规则!
我们需要一个与优先级和关联性规则无关的规则来解释子表达式的计算顺序。 Java(和 C#)中的相关规则是“从左到右评估子表达式”。 由于 A() 出现在 C() 的左侧,因此首先评估 A(),不管事实上,C() 涉及乘法,而 A() 仅涉及加法。
所以现在您有足够的信息来回答您的问题。在a[b] = b = 0 中,关联规则说这是a[b] = (b = 0);,但这并不意味着b=0 首先运行!优先级规则说索引的优先级高于赋值,但这并不意味着索引器在最右边的赋值之前运行。
(更新:此答案的早期版本在我已更正的后面部分中有一些小的且实际上不重要的遗漏。我还写了一篇博客文章,描述为什么这些规则在 Java 和 C# 中是明智的:@ 987654321@)
优先级和关联性仅告诉我们零分配到b必须发生在之前分配到a[b],因为零分配计算值在索引操作中分配的。单独的优先级和关联性并不能说明a[b] 是在之前 还是在 b=0 之后进行评估。
同样,这与:A()[B()] = C() 相同——我们所知道的是索引必须在分配之前发生。我们不知道 A()、B() 还是 C() 是否首先运行基于优先级和关联性。我们需要另一个规则来告诉我们。
同样,规则是“当你可以选择先做什么时,总是从左到右”。但是,在这种特定情况下存在一个有趣的问题。 空集合或超出范围的索引导致的抛出异常的副作用是否被认为是赋值左侧计算的一部分,还是赋值本身计算的一部分? Java 选择了后者。 (当然,这个区别只在代码已经错误的情况下才有意义,因为正确的代码首先不会取消引用 null 或传递错误的索引。)
那么会发生什么?
-
a[b] 位于b=0 的左侧,因此a[b] 运行首先,导致a[1]。但是,检查此索引操作的有效性会有所延迟。
- 然后
b=0 发生。
- 然后验证
a 有效且a[1] 在范围内
- 将值分配给
a[1] 最后发生。
因此,尽管在这个特定案例中,对于那些原本不应该出现在正确代码中的罕见错误案例,需要考虑一些微妙之处,但通常你可以推理:左边的事情发生在右边的事情之前。这就是您要寻找的规则。谈论优先级和关联性既令人困惑又无关紧要。
人们总是弄错这些东西,即使是那些应该知道得更多的人。我编辑了 太多 错误地说明规则的编程书籍,所以很多人对优先级/关联性和评估顺序之间的关系有完全错误的信念也就不足为奇了——也就是说,实际上没有这种关系;他们是独立的。
如果您对此主题感兴趣,请参阅我关于该主题的文章以进一步阅读:
http://blogs.msdn.com/b/ericlippert/archive/tags/precedence/
它们是关于 C# 的,但其中大部分内容同样适用于 Java。