Java中评估顺序的规则是什么?

ipk*_*iss 83 java operator-precedence

我正在阅读一些Java文本并获得以下代码:

int[] a = {4,4};
int b = 1;
a[b] = b = 0;
Run Code Online (Sandbox Code Playgroud)

在文中,作者没有给出明确的解释,最后一行的效果是: a[1] = 0;

我不太清楚我理解:评估是如何发生的?

Eri*_*ert 169

让我这么说清楚,因为人们总是误解这个:

子表达式的评估顺序与关联性和优先级无关.结合性和优先级确定以何种顺序的运营执行,但以什么顺序确定的子表达式进行评估.您的问题是关于子表达式的评估顺序.

考虑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
Run Code Online (Sandbox Code Playgroud)

所有的优先级和关联性规则都遵循 - 第一次加法发生在第二次加法之前,乘法发生在第二次加法之前.显然,我们可以按任何顺序调用A(),B(),C()和D(),并且仍然遵守优先级和关联性规则!

我们需要一个优先级和关联性规则无关的规则来解释子表达式的计算顺序.Java(和C#)中的相关规则是"从左到右评估子表达式".由于A()出现在C()的左侧,因此首先计算A(),而不管C()是否涉及乘法,而A()仅涉及加法.

所以现在你有足够的信息来回答你的问题.在a[b] = b = 0关联性规则中说这是a[b] = (b = 0);但这并不意味着b=0先运行!优先级规则说索引的优先级高于赋值,但这并不意味着索引器在最右边的赋值之前运行.

(更新:这个答案的早期版本在我接下来的部分中有一些小的,实际上不重要的遗漏.我还写了一篇博客文章,描述了为什么这些规则在Java和C#中是明智的:https:// ericlippert.com/2019/01/18/indexer-error-cases/)

优先级和结合只是告诉我们,零分配b必须发生之前的分配a[b],因为零分配计算可以在索引操作分配的值.优先级和结合单独只字不提是否在a[b]评估之前之后b=0.

同样,这与以下内容相同:A()[B()] = C()- 我们所知道的是索引必须在赋值之前发生.我们不知道A(),B()或C()是否首先根据优先级和关联性运行.我们需要另一条规则来告诉我们.

该规则同样是"当你可以选择先做什么时,总是从左到右".然而,在这个特定场景中有一个有趣的皱纹.由null集合或超出范围索引引起的抛出异常的副作用是否被认为是赋值左侧计算的一部分,或者是赋值本身计算的一部分?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.

  • 就个人而言,我更喜欢心理模型,在第一步中,您使用优先级和关联性构建表达式树.并在第二步递归评估从根开始的树.对节点的评估是:从左到右评估*immediate*子节点,然后评估注释本身.| 该模型的一个优点是它可以轻松处理二元运算符具有副作用的情况.但主要的优点是它更适合我的大脑. (6认同)
  • @noober:好的,考虑:M(A()+ B(),C()*D(),E()+ F()).您希望以什么顺序评估子表达式?是否应在A(),B(),E()和F()之前评估C()和D(),因为乘法优先于加法?很容易说"明显"订单应该是不同的.提出涵盖所有案例的实际规则要困难得多.C#和Java的设计者选择了一个简单易懂的规则:"从左到右".您建议的替代品是什么,为什么您认为您的规则更好? (6认同)
  • @Neil:C++不保证评估的顺序,也从来没有.(C也不是.)Python严格按优先顺序保证它; 与其他一切不同,任务是R2L. (2认同)
  • @aroth对我来说你听起来很困惑.优先规则只暗示孩子需要在父母面前进行评估.但他们对评估儿童的顺序一无所知.Java和C#从左到右选择,C和C++选择了未定义的行为. (2认同)

Don*_*ows 32

尽管如此,Eric Lippert的精湛答案并没有得到很好的帮助,因为它正在谈论另一种语言.这是Java,其中Java语言规范是语义的权威描述.特别是,§15.26.1是相关的,因为它描述了=运算符的评估顺序(我们都知道它是右关联的,是吗?).在这个问题中将它减少到我们关心的位:

如果左侧操作数表达式是数组访问表达式(第15.13节),则需要执行许多步骤:

  • 首先,评估左侧操作数数组访问表达式的数组引用子表达式.如果此评估突然完成,则赋值表达式出于同样的原因突然完成; 不评估索引子表达式(左侧操作数数组访问表达式)和右侧操作数,也不进行赋值.
  • 否则,将评估左侧操作数数组访问表达式的索引子表达式.如果此评估突然完成,则赋值表达式会出于同样的原因突然完成,并且不会评估右侧操作数并且不会进行赋值.
  • 否则,将评估右侧操作数.如果此评估突然完成,则赋值表达式会出于同样的原因突然完成,并且不会发生任何分配.

[...然后继续描述赋值本身的实际含义,为简洁起见,我们可以忽略它......]

简而言之,Java有一个非常严格定义的评估顺序,它在任何运算符或方法调用的参数中几乎完全是从左到右.数组赋值是更复杂的情况之一,但即便如此,它仍然是L2R.(JLS确实建议您不要编写需要这些复杂语义约束的代码,我也是如此:每个语句只需要一个赋值,就会遇到很多麻烦!)

在这个领域,C和C++肯定与Java不同:它们的语言定义使得评估顺序无意地被定义,以实现更多优化.C#显然与Java类似,但我不能很好地了解它的文献,无法指出正式的定义.(这实际上因语言而异,Ruby严格来说是L2R,就像Tcl一样 - 尽管由于这里没有相关的原因缺少赋值运算符本身 - 而且Python是L2R但R2L就赋值而言,我觉得很奇怪但是你去了.)

  • 所以你所说的是Eric的答案是错误的,因为Java专门定义它正是他所说的? (11认同)
  • _Java(和C#)中的相关规则是"从左到右评估子表达式"_ - 听起来像他在谈论两者. (8认同)
  • @Greenie:Eric的回答是正确的,但正如我所说,你不能从这个领域的一种语言中获取洞察力并将其应用于另一种语言而不需要小心.所以我引用了权威来源. (5认同)
  • 这里有点困惑 - 这是否使得Eric Lippert的上述答案不那么真实,或者只是引用一个特定的参考来说明为什么它是真的? (2认同)

小智 5

a[b] = b = 0;
Run Code Online (Sandbox Code Playgroud)

1)数组索引运算符具有更高的优先级然后赋值运算符(请参阅此答案):

(a[b]) = b = 0;
Run Code Online (Sandbox Code Playgroud)

2)根据15.26.JLS的分配运算符

有12个赋值运算符; 所有这些都是语法上的右关联(他们从右到左分组).因此,a = b = c表示a =(b = c),它将c的值赋给b,然后将b的值赋给a.

(a[b]) = (b=0);
Run Code Online (Sandbox Code Playgroud)

3)根据15.7.JLS的评估顺序

Java编程语言保证运算符的操作数似乎以特定的评估顺序进行评估,即从左到右.

在评估右侧操作数的任何部分之前,似乎完全评估了二元运算符的左侧操作数.

所以:

a)(a[b])首先评估a[1]

b)然后(b=0)评估为0

c)(a[1] = 0)最后评估