注意:请注意,以下代码基本上没有意义,仅供参考。
基于以下事实:在将赋值分配给左侧变量之前,必须始终评估赋值的右侧,以及++
和--
之类的递增操作总是在评估后执行,我不希望以下代码工作:
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]
,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,然后才增加这对我来说是一个意想不到的行为。或者它真的是预期的,我显然错过了什么?
答案 0 :(得分:21)
ILDasm可能是你最好的朋友,有时候; - )
我编译了你的两个方法并比较了产生的IL(汇编语言)。
重要的细节在循环中,不出所料。你的第一个方法编译并运行如下:
Code Description Stack
ldloc.1 Load ref to newArray2 newArray2
ldloc.2 Load value of IndTmp newArray2,0
ldloc.0 Load ref to newArray1 newArray2,0,newArray1
ldloc.2 Load value of IndTmp newArray2,0,newArray1,0
dup Duplicate top of stack newArray2,0,newArray1,0,0
ldc.i4.1 Load 1 newArray2,0,newArray1,0,0,1
add Add top 2 values on stack newArray2,0,newArray1,0,1
stloc.2 Update IndTmp newArray2,0,newArray1,0 <-- IndTmp is 1
ldelem.ref Load array element newArray2,0,"1"
stelem.ref Store array element <empty>
<-- newArray2[0] = "1"
对newArray1中的每个元素重复此操作。重要的一点是,在IndTmp递增之前,源数组中元素的位置已被推送到堆栈。
将此与第二种方法进行比较:
Code Description Stack
ldloc.1 Load ref to newArray2 newArray2
ldloc.2 Load value of IndTmp newArray2,0
dup Duplicate top of stack newArray2,0,0
ldc.i4.1 Load 1 newArray2,0,0,1
add Add top 2 values on stack newArray2,0,1
stloc.2 Update IndTmp newArray2,0 <-- IndTmp is 1
ldloc.0 Load ref to newArray1 newArray2,0,newArray1
ldloc.2 Load value of IndTmp newArray2,0,newArray1,1
ldelem.ref Load array element newArray2,0,"2"
stelem.ref Store array element <empty>
<-- newArray2[0] = "2"
这里,IndTmp在源数组中元素的位置被推送到堆栈之前递增,因此行为差异(以及后续异常)。
为了完整性,我们将其与
进行比较newArray2[IndTmp] = newArray1[++IndTmp];
Code Description Stack
ldloc.1 Load ref to newArray2 newArray2
ldloc.2 Load IndTmp newArray2,0
ldloc.0 Load ref to newArray1 newArray2,0,newArray1
ldloc.2 Load IndTmp newArray2,0,newArray1,0
ldc.i4.1 Load 1 newArray2,0,newArray1,0,1
add Add top 2 values on stack newArray2,0,newArray1,1
dup Duplicate top stack entry newArray2,0,newArray1,1,1
stloc.2 Update IndTmp newArray2,0,newArray1,1 <-- IndTmp is 1
ldelem.ref Load array element newArray2,0,"2"
stelem.ref Store array element <empty>
<-- newArray2[0] = "2"
这里,在更新IndTmp之前,增量的结果已被推送到堆栈(并成为数组索引)。
总之,似乎首先评估作业的目标,然后是源。
赞成OP以获得一个真正令人深思的问题!
答案 1 :(得分:18)
根据Eric Lippert的说法,这在C#语言中得到了很好的定义,并且很容易解释。
注意:代码的实际执行可能不是这样,重要的是要记住编译器必须创建等效的代码
那么第二段代码中会发生什么:
newArray2
被评估并记住结果(即,记住对我们想要存储内容的任何数组的引用,以防后来的副作用发生变化)IndTemp
并记住结果IndTemp
增加1 newArray1
并记住结果IndTemp
并记住结果(但这里是1)正如您所看到的,第二次IndTemp
被评估(RHS),该值已经增加了1,但这对LHS没有影响,因为它在增加前记住值为0
在第一段代码中,顺序略有不同:
newArray2
并记住结果IndTemp
并记住结果newArray1
并记住结果IndTemp
并记住结果(但这里是1)IndTemp
增加1 在这种情况下,步骤2.3中变量的增加对当前循环迭代没有影响,因此您将始终从索引N
复制到索引N
,而在第二部分中代码总是从索引N+1
复制到索引N
。
Eric有一篇名为Precedence vs order, redux的博客文章,应该阅读。
这是一段代码,说明了我基本上将变量转换为类的属性,并实现了一个自定义的“数组”集合,所有这些集合都只是转发到控制台发生的事情。
void Main()
{
Console.WriteLine("first piece of code:");
Context c = new Context();
c.newArray2[c.IndTemp] = c.newArray1[c.IndTemp++];
Console.WriteLine();
Console.WriteLine("second piece of code:");
c = new Context();
c.newArray2[c.IndTemp++] = c.newArray1[c.IndTemp];
}
class Context
{
private Collection _newArray1 = new Collection("newArray1");
private Collection _newArray2 = new Collection("newArray2");
private int _IndTemp;
public Collection newArray1
{
get
{
Console.WriteLine(" reading newArray1");
return _newArray1;
}
}
public Collection newArray2
{
get
{
Console.WriteLine(" reading newArray2");
return _newArray2;
}
}
public int IndTemp
{
get
{
Console.WriteLine(" reading IndTemp (=" + _IndTemp + ")");
return _IndTemp;
}
set
{
Console.WriteLine(" setting IndTemp to " + value);
_IndTemp = value;
}
}
}
class Collection
{
private string _name;
public Collection(string name)
{
_name = name;
}
public int this[int index]
{
get
{
Console.WriteLine(" reading " + _name + "[" + index + "]");
return 0;
}
set
{
Console.WriteLine(" writing " + _name + "[" + index + "]");
}
}
}
输出是:
first piece of code:
reading newArray2
reading IndTemp (=0)
reading newArray1
reading IndTemp (=0)
setting IndTemp to 1
reading newArray1[0]
writing newArray2[0]
second piece of code:
reading newArray2
reading IndTemp (=0)
setting IndTemp to 1
reading newArray1
reading IndTemp (=1)
reading newArray1[1]
writing newArray2[0]
答案 2 :(得分:13)
newArray2[IndTmp] = newArray1[IndTmp++];
导致首先分配然后递增变量。
等等。
RHS ++运算符立即递增,但它在递增之前返回值。用于在数组中索引的值是RHS ++运算符返回的值,因此非递增值。
你描述的内容(引发的异常)将是LHS ++的结果:
newArray2[IndTmp] = newArray1[++IndTmp]; //throws exception
答案 3 :(得分:12)
确切地看到错误的位置是有益的:
在将赋值分配给左侧变量之前,必须始终评估赋值的右侧
正确。显然,在计算分配的值之前,分配的副作用才会发生。
增量操作如++和 - 总是在评估后立即执行
几乎正确。目前尚不清楚“评价”是什么意思 - 评价什么?原始值,递增值或表达式的值?考虑它的最简单方法是计算原始值,然后计算增量值,然后发生副作用。然后,最终值是选择原始值或递增值之一,具体取决于运算符是前缀还是后缀。但是你的基本前提非常好:在确定最终值之后立即发生增量的副作用,然后产生最终值。
然后你似乎从这两个正确的前提中得出了一个谎言,即左手边的副作用是在评估右手边后产生的。但这两个前提中没有任何内容暗示这个结论!你只是凭空捏造了这个结论。
如果你说出第三个正确的前提,那就更清楚了:
在分配发生之前,必须知道与左侧变量 关联的存储位置。
显然这是真的。在分配发生之前,您需要知道两个事物:正在分配什么值,以及正在变异的内存位置。你无法同时解决这两件事;你必须弄清楚其中一个第一个,我们找出左边的一个 - 变量 - 首先在C#中。如果找出存储所在的位置会产生副作用,那么在我们弄清楚第二件事 - 分配给变量的值之前,会产生副作用。
简而言之,在C#中,对变量赋值的求值顺序如下:
答案 4 :(得分:4)
显然,假设rhs总是在lhs之前进行评估是错误的。如果你看这里http://msdn.microsoft.com/en-us/library/aa691315(v=VS.71).aspx,似乎在索引器访问的情况下,索引器访问表达式的参数,即lhs,在rhs之前被计算。
换句话说,首先确定在哪里存储rhs的结果,然后才评估rhs。
答案 5 :(得分:3)
它会引发异常,因为您在索引1处开始索引newArray1
。因为您正在迭代newArray1
中的每个元素,所以最后一个赋值会引发异常,因为IndTmp
等于{ {1}},即一个超过数组末尾的一个。在索引变量被用于从newArray1.Length
中提取元素之前,您将其递增,这意味着您将崩溃并错过newArray1
中的第一个元素。