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

int[] a = {4,4};
int b = 1;
a[b] = b = 0;


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

我不确定我是否理解:评估是如何发生的?

评论

下面关于发生这种情况的原因的困惑是,顺便说一句,您永远不要这样做,因为许多人将不得不思考它的实际作用,而不是显而易见。

对于这样的问题,正确的答案是“不要那样做!”作业应视为陈述;使用赋值作为返回值的表达式应该引起编译器错误,或者至少是警告。别那样。

#1 楼

让我说得很清楚,因为人们一直误解这一点:

子表达式的求值顺序既与关联性无关,又与优先级无关。关联性和优先级确定操作符的执行顺序,但不确定子表达式的计算顺序。您的问题与子表达式的计算顺序有关。

考虑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()的左侧,因此无论C()参与乘法而A()仅涉及加法,都将首先评估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()首先基于优先级和关联性运行。我们需要另外一条规则来告诉我们。

规则再次是,“当您选择首先要做什么时,请始终从左到右”。但是,在这种特定情况下会有一个有趣的皱纹。由空集合或超出范围的索引引起的引发异常的副作用是否被视为分配左侧计算的一部分,还是分配本身计算的一部分? 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。

评论


我个人更喜欢心理模型,在第一步中,您要使用优先级和关联性来构建表达式树。在第二步中,从根开始递归地评估该树。评估一个节点:从左到右评估直接子节点,然后评估注释本身。 |该模型的一个优点是它可以轻松地处理二进制运算符具有副作用的情况。但是主要优点是它更适合我的大脑。

– CodesInChaos
2011年7月23日在19:56



我对C ++不能保证这一点吗?那Python呢?

–尼尔G
2011年7月23日在21:05



@Neil:C ++不保证任何有关评估顺序的事情,也从未做过任何保证。 (C也没有。)Python严格按优先级顺序进行保证。与其他所有东西不同,分配是R2L。

–研究员
2011年7月23日23:00

@aroth对我来说你听起来很困惑。优先权规则仅意味着必须先对孩子进行评估,然后再对父母进行评估。但是他们对孩子的评估顺序一无所知。 Java和C#从左到右选择,C和C ++选择未定义的行为。

– CodesInChaos
2011年7月24日在16:12



@noober:好的,考虑:M(A()+ B(),C()* D(),E()+ F())。您希望以什么顺序评估子表达式?是否应该在A(),B(),E()和F()之前评估C()和D(),因为乘法的优先级高于加法?可以很容易地说“显然”顺序应该不同。提出涵盖所有情况的实际规则要困难得多。 C#和Java的设计者选择了一个简单的,易于解释的规则:“从左到右”。您提议的替代方案是什么?为什么您认为自己的规则更好?

–埃里克·利珀特
11年8月18日在15:12

#2 楼

但是,埃里克·利珀特(Eric Lippert)的出色回答并没有帮助他们,因为它在谈论另一种语言。这是Java,其中Java语言规范是对语义的明确描述。尤其是§15.26.1是相关的,因为它描述了=运算符的评估顺序(我们都知道它是右关联的,是吗?)。将其减少到我们在此问题中关心的位:

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

首先,评估左侧操作数数组访问表达式的数组引用子表达式。如果该评估突然完成,则赋值表达式由于相同的原因而突然完成;不评估(左操作数数组访问表达式的)索引子表达式和右操作数,并且不进行赋值。
否则,对左操作数数组访问表达式的索引子表达式进行求值。如果此评估突然完成,则出于相同的原因,赋值表达式也会突然完成,并且不会评估右侧操作数并且不会发生赋值。
否则,将评估右侧操作数。如果该评估突然完成,则由于相同的原因,赋值表达式也会突然完成,并且不会发生赋值。简而言之,我们在这里可以忽略……]
简而言之,Java具有非常紧密定义的评估顺序,该顺序几乎在任何运算符或方法调用的参数内从左到右。数组分配是更复杂的情况之一,但即使在这里,它仍然是L2R。 (JLS建议您不要编写需要这种复杂语义约束的代码,我也一样:您只需为每个语句分配一个指令就可以解决很多麻烦!)
C和C ++在这方面肯定与Java不同:它们的语言定义故意使评估顺序未定义,以实现更多优化。 C#显然类似于Java,但是我对它的文献了解不足,无法指向正式的定义。 (这实际上因语言而异,但Ruby严格来说是T2R,Tcl也是严格的L2R(尽管由于本身与此处无关,它本身没有赋值运算符),Python在赋值方面是L2R却是R2L,我觉得很奇怪,但是您去了。)

评论


那么,您所说的Eric的答案是错误的,因为Java专门将其定义为与他所说的完全一样?

–配置器
2011年8月7日上午10:29

Java(和C#)中的相关规则是“子表达式从左到右求值”-听起来像他在谈论这两个。

–配置器
2011年8月7日在16:22

这里有些困惑-这是否会使埃里克·利珀特(Eric Lippert)的上述答案变得不那么正确,还是仅仅引用了关于其正确性的具体参考?

– GreenieMeanie
11年8月16日在17:59

@Greenie:Eric的回答是正确的,但是正如我所说,您不能不小心地从该领域的一种语言中获得见识,而将其应用于另一种语言。因此,我引用了明确的消息来源。

–研究员
11年8月16日在18:53

奇怪的是,在解析左手变量之前先评估右手;在a [-1] = c中,在将-1识别为无效之前对c进行求值。

–中宇
2015年11月10日,下午3:13

#3 楼

a[b] = b = 0;


1)数组索引运算符的优先级高于赋值运算符(请参见此答案):

(a[b]) = b = 0;


2)根据15.26。 JLS的赋值运算符


有12个赋值运算符。在语法上都是右关联的(它们从右到左分组)。因此,a = b = c意味着a =(b = c),它将c的值分配给b,然后将b的值分配给a。


(a[b]) = (b=0);


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


Java编程语言保证运算符的操作数似乎按照特定的评估顺序(即从左到右)进行评估。





二进制运算符的左操作数似乎在对右操作数的任何部分求值之前已被完全求值。


所以:

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

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

/> c)(a[1] = 0)评估为最新

#4 楼

您的代码等效于:

int[] a = {4,4};
int b = 1;
c = b;
b = 0;
a[c] = b;


解释结果。

评论


问题是为什么会这样。

–垫子
11年7月23日在13:20

@Mat答案是因为考虑到问题中提供的代码,这是在后台进行的操作。这就是评估的过程。

–JérômeVerstrynge
11年7月23日在13:35

是的我知道。通过IMO仍然没有回答这个问题,这就是为什么这是评估的过程。

–垫子
2011年7月23日在13:38

@Mat'为什么会这样进行评估?'不是问的问题。 “评估是如何发生的?”是问的问题。

–JérômeVerstrynge
2011年7月23日在13:44

@JVerstry:它们如何不相等?左侧操作数的数组引用子表达式是最左侧的操作数。因此,说“首先做最左边的”与说“首先做数组引用”完全相同。如果Java规范作者选择在解释此特定规则时过于冗长和冗长,这对他们有好处。这种事情很令人困惑,应该多而不是少罗word。但是我看不到我的简洁表征在语义上与prolix表征有何不同。

–埃里克·利珀特
2011年8月17日17:36



#5 楼

考虑下面的另一个更深入的示例。
作为一般经验法则:
解决这些问题时,最好先阅读一张优先顺序规则和关联性表,以供阅读。 http://introcs.cs.princeton.edu/java/11precedence/
这是一个很好的例子:
System.out.println(3+100/10*2-13);

问题:以上代码行的输出是什么?
答案:应用优先级和关联性规则
步骤1:根据优先级规则:/和*运算符优先于+-运算符。因此,执行此方程式的起点将缩小为:
100/10*2

步骤2:根据规则和优先级:/和*优先级相等。
由于/和*运算符为优先级相等时,我们需要查看这些运算符之间的关联性。
根据这两个特定运算符的关联性规则,
我们从左到右开始执行方程,即执行100/10 first:
100/10*2
=100/10
=10*2
=20

步骤3:现在方程式处于以下执行状态:
=3+20-13

根据规则和优先级:+和-优先级相等。
我们现在需要查看运算符+和-之间的关联性。根据这两个特定运算符的关联性,
我们开始从左向右执行方程,即首先执行3 + 20:
=3+20
=23
=23-13
=10

10是编译时的正确输出
/>同样,在解决这些问题时,与您一起拥有优先顺序规则和关联性表非常重要,例如http://introcs.cs.princeton.edu/java/11precedence/

评论


您是说“运算符+和-运算符之间的关联性”是“从左到右”。尝试使用该逻辑并评估10-4-3。

– Pshemo
16年11月16日在23:15

我怀疑这个错误可能是由于以下事实造成的:introcs.cs.princeton.edu/java/11precedence顶部的+是一元运算符(具有从右到左的关联性),但是加号+和-就像乘法* /%从左到右的关联性。

– Pshemo
16年11月16日23:23



很好发现并据此进行了修改,谢谢Pshemo

–马克·伯利(Mark Burleigh)
16年11月17日在12:04

这个答案解释了优先级和关联性,但是,正如埃里克·利珀特(Eric Lippert)解释的那样,问题在于评估顺序,这是非常不同的。事实上,这并不能回答问题。

–法比奥说恢复莫妮卡
19年1月10日在11:33