C# 中奇怪的增量行为
注意:请注意,下面的代码本质上是无意义的,仅用于说明目的。
基于这样的事实:在将赋值给左侧变量之前,必须始终对赋值的右侧进行求值,并且增量操作如 ++
和 - -
总是在评估后立即执行,我不希望以下代码起作用:
string[] newArray1 = new[] {"1", "2", "3", "4"};
string[] newArray2 = new string[4];
int IndTmp = 0;
foreach (string TmpString in newArray1)
{
newArray2[IndTmp] = newArray1[IndTmp++];
}
相反,我希望将 newArray1[0]
分配给 newArray2[1]< /code>,
newArray1[1]
到 newArray[2]
等等,直到抛出 System.IndexOutOfBoundsException
。相反,令我非常惊讶的是,抛出异常的版本是
string[] newArray1 = new[] {"1", "2", "3", "4"};
string[] newArray2 = new string[4];
int IndTmp = 0;
foreach (string TmpString in newArray1)
{
newArray2[IndTmp++] = newArray1[IndTmp];
}
因为,根据我的理解,编译器首先评估 RHS,将其分配给 LHS,然后才递增,这对我来说是一种意外的行为。或者这确实是我所期望的,而我显然错过了一些东西?
Note: Please note that the code below is essentially non-sense, and just for illustration purposes.
Based on the fact that the right-hand side of an assignment must always be evaluated before it's value is assigned to the left-hand side variable, and that increment operations such as ++
and --
are always performed right after evaluation, I would not expect the following code to work:
string[] newArray1 = new[] {"1", "2", "3", "4"};
string[] newArray2 = new string[4];
int IndTmp = 0;
foreach (string TmpString in newArray1)
{
newArray2[IndTmp] = newArray1[IndTmp++];
}
Rather, I would expect newArray1[0]
to be assigned to newArray2[1]
, newArray1[1]
to newArray[2]
and so on up to the point of throwing a System.IndexOutOfBoundsException
. Instead, and to my great surprise, the version that throws the exception is
string[] newArray1 = new[] {"1", "2", "3", "4"};
string[] newArray2 = new string[4];
int IndTmp = 0;
foreach (string TmpString in newArray1)
{
newArray2[IndTmp++] = newArray1[IndTmp];
}
Since, in my understanding, the compiler first evaluates the RHS, assigns it to the LHS and only then increments this is to me an unexpected behaviour. Or is it really expected and I am clearly missing something?
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(6)
有时,ILDasm 可以成为您最好的朋友;-)
我编译了您的两种方法并比较了生成的 IL(汇编语言)。
毫不奇怪,重要的细节在循环中。第一个方法的编译和运行如下:
对 newArray1 中的每个元素重复此操作。重要的一点是,在 IndTmp 递增之前,源数组中元素的位置已被推送到堆栈。
将此与第二种方法进行比较:
这里,在将源数组中的元素的位置推入堆栈之前,IndTmp 会递增,因此行为有所不同(以及随后的异常)。
为了完整起见,我们将其与
此处进行比较,在 IndTmp 更新之前,增量的结果已被推送到堆栈(并成为数组索引)。
总之,似乎首先评估分配的目标,然后是源。
为OP提出一个真正发人深省的问题点赞!
ILDasm can be your best friend, sometimes ;-)
I compiled up both your methods and compared the resulting IL (assembly language).
The important detail is in the loop, unsurprisingly. Your first method compiles and runs like this:
This is repeated for each element in newArray1. The important point is that the location of the element in the source array has been pushed to the stack before IndTmp is incremented.
Compare this to the second method:
Here, IndTmp is incremented before the location of the element in the source array has been pushed to the stack, hence the difference in behaviour (and the subsequent exception).
For completeness, let's compare it with
Here, the result of the increment has been pushed to the stack (and becomes the array index) before IndTmp is updated.
In summary, it seems to be that the target of the assignment is evaluated first, followed by the source.
Thumbs up to the OP for a really thought provoking question!
根据 Eric Lippert 的说法,这在 C# 语言中得到了明确的定义,并且很容易解释。
注意:代码的实际执行可能不是这样的,重要的是要记住的是,编译器必须创建与此等效的代码
所以第二段代码中发生的事情是这样的:
newArray2
被评估并记住结果(即记住对我们想要存储内容的任何数组的引用,以防以后的副作用改变它)IndTemp
被评估并记住结果IndTemp
增加 1newArray1
被评估并记住结果IndTemp
被评估并记住结果(但这里是 1)如您所见,第二次计算
IndTemp
(RHS) 时,该值已增加1,但这对 LHS 没有影响,因为它会记住增加之前的值是 0。在第一段代码中,顺序略有不同:
newArray2
被评估并记住结果IndTemp
被评估并记住结果newArray1
被评估并记住结果IndTemp
被评估并记住结果(但这里是 1)IndTemp
增加 1在这种情况下,步骤 2.3 中变量的增加对当前循环迭代没有影响,因此您将始终从将
N
索引到索引N
中,而在第二段代码中,您始终会从索引N+1
复制到索引N< /代码>。
Eric 有一个博客条目,标题为 优先级vs order, redux 应该阅读。
这是一段代码,说明了我基本上将变量转换为类的属性,并实现了自定义“数组”集合,所有这些都只是将正在发生的事情转储到控制台。
输出是:
This is well-defined in the C# language according to Eric Lippert and is easily explained.
Note: The actual execution of code might not be like this, the important thing to remember is that the compiler must create code that is equivalent to this
So what happens in the second piece of code is this:
newArray2
is evaluated and the result is remembered (ie. the reference to whatever array we want to store things in is remembered, in case side-effects later change it)IndTemp
is evaluated and the result is rememberedIndTemp
is increased by 1newArray1
is evaluated and the result is rememberedIndTemp
is evaluated and the result is remembered (but this is 1 here)As you can see, the second time
IndTemp
is evaluated (RHS), the value has already been increased by 1, but this has no impact on the LHS since it is remembering that the value was 0 before increased.In the first piece of code, the order is slightly different:
newArray2
is evaluated and the result is rememberedIndTemp
is evaluated and the result is rememberednewArray1
is evaluated and the result is rememberedIndTemp
is evaluated and the result is remembered (but this is 1 here)IndTemp
is increased by 1In this case, the increase of the variable at step 2.3 has no impact on the current loop iteration, and thus you will always copy from index
N
into indexN
, whereas in the second piece of code you will always copy from indexN+1
into indexN
.Eric has a blog entry titled Precedence vs order, redux that should be read.
Here is a piece of code that illustrates, I basically turned variables into properties of a class, and implemented a custom "array" collection, that all just dump to the console what is happening.
Output is:
导致首先分配变量,然后递增变量。
,依此类推。
RHS ++ 运算符立即递增,但它返回递增之前的值。用于在数组中索引的值是 RHS ++ 运算符返回的值,因此是非递增值。
您所描述的(引发的异常)将是 LHS ++ 的结果:
leads to first assinging and then incrementing the variable.
and so on.
The RHS ++ operator increments right away, but it returns the value before it was incremented. The value used to index in the array is the value returned by the RHS ++ operator, so the non incremented value.
What you describe (the exception thrown) will be a result of a LHS ++:
准确地查看错误所在是有启发性的:
Correct 之前,必须始终先评估赋值的右侧。显然,在计算所分配的值之前,分配的副作用不会发生。
几乎正确。不清楚你所说的“评估”是什么意思——评估什么?原始值、增量值还是表达式的值?最简单的思考方法是计算原始值,然后计算增量值,然后发生副作用。然后最终值是选择原始值或增量值之一,具体取决于运算符是前缀还是后缀。但你的基本前提很好:增量的副作用在最终值确定后立即发生,然后产生最终值。
那么,您似乎从这两个正确的前提得出了一个错误的结论,即左侧的副作用是在评估右侧之后产生的。但这两个前提并没有暗示这个结论!你只是凭空得出这个结论。
如果你说出第三个正确的前提,那就更清楚了:
显然这是真的。在进行赋值之前,您需要知道两件事:正在分配什么值,以及正在改变什么内存位置。你无法同时解决这两件事;你必须首先找出其中一个,而我们在 C# 中首先找出左侧的变量——变量。如果弄清楚存储位置会导致副作用,那么在我们弄清楚第二件事(分配给变量的值)之前,就会产生副作用。
简而言之,在 C# 中,变量赋值的求值顺序如下:
It is instructive to see exactly where your error is:
Correct. Clearly the side effect of the assignment cannot happen until after the value being assigned has been computed.
Almost correct. It is not clear what you mean by "evaluation" -- evaluation of what? The original value, the incremented value, or the value of the expression? The easiest way to think about it is that the original value is computed, then the incremented value, then the side effect happens. Then the final value is that one of the original or the incremented value is chosen, depending on whether the operator was prefix or postfix. But your basic premise is pretty good: that the side effect of the increment happens immediately after the final value is determined, and then the final value is produced.
You then seem to be concluding a falsehood from these two correct premises, namely, that the side effects of the left hand side are produced after the evaluation of the right hand side. But nothing in those two premises implies this conclusion! You've just pulled that conclusion out of thin air.
It would be more clear if you stated a third correct premise:
Clearly this is true. You need to know two things before an assignment can happen: what value is being assigned, and what memory location is being mutated. You can't figure those two things out at the same time; you have to figure out one of them first, and we figure out the one on the left hand side -- the variable -- first in C#. If figuring out where the storage is located causes a side effect then that side effect is produced before we figure out the second thing -- the value being assigned to the variable.
In short, in C# the order of evaluations in an assignment to a variable goes like this:
显然,总是在左侧之前评估右侧的假设是错误的。如果您查看此处 http://msdn.microsoft。 com/en-us/library/aa691315(v=VS.71).aspx 似乎在索引器访问索引器访问表达式的参数的情况下,这是lhs 在 rhs 之前评估。
换句话说,首先确定将 rhs 的结果存储在哪里,然后才对 rhs 进行求值。
Obviously the assumption that the rhs is always evaluated before the lhs is wrong. If you look here http://msdn.microsoft.com/en-us/library/aa691315(v=VS.71).aspx it seems like in the case of indexer access the arguments of the indexer access expression, which is the lhs, are evaluated before the rhs.
in other words, first it is determined where to store the result of the rhs, only then the rhs is evaluated.
它会引发异常,因为您在索引 1 处开始对
newArray1
进行索引。由于您正在迭代newArray1
中的每个元素,因此最后一个赋值会引发异常,因为IndTmp 等于 newArray1.Length,即超出数组末尾 1。您在使用索引变量从
newArray1
中提取元素之前递增索引变量,这意味着您将崩溃并且还会错过newArray1
中的第一个元素。It throws an exception because you start indexing into
newArray1
at index 1. Since you are iterating over each element innewArray1
the last assignment throws an exception becauseIndTmp
is equal tonewArray1.Length
, i.e., one past the end of the array. You increment the index variable before it is ever used to extract an element fromnewArray1
, which means you will crash and also miss the first element innewArray1
.