C#中的奇怪增量行为

时间:2011-07-02 18:04:49

标签: c# post-increment

注意:请注意,以下代码基本上没有意义,仅供参考。

基于以下事实:在将赋值分配给左侧变量之前,必须始终评估赋值的右侧,以及++--之类的递增操作总是在评估后执行,我不希望以下代码工作:

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,然后才增加这对我来说是一个意想不到的行为。或者它真的是预期的,我显然错过了什么?

6 个答案:

答案 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#语言中得到了很好的定义,并且很容易解释。

  1. 评估需要引用和记住的第一个左序表达式,并考虑副作用
  2. 然后完成右序表达
  3. 注意:代码的实际执行可能不是这样,重要的是要记住编译器必须创建等效的代码

    那么第二段代码中会发生什么:

    1. 左手边:
      1. newArray2被评估并记住结果(即,记住对我们想要存储内容的任何数组的引用,以防后来的副作用发生变化)
      2. 评估
      3. IndTemp并记住结果
      4. IndTemp增加1
    2. 右侧:
        评估
      1. newArray1并记住结果
      2. 评估
      3. IndTemp并记住结果(但这里是1)
      4. 通过索引从步骤2.1的索引到步骤2.2的索引来检索数组项目
    3. 回到左侧
      1. 通过索引从步骤1.1索引到步骤1.2
      2. 的数组来存储数组项
    4. 正如您所看到的,第二次IndTemp被评估(RHS),该值已经增加了1,但这对LHS没有影响,因为它在增加前记住值为0

      在第一段代码中,顺序略有不同:

      1. 左手边:
          评估
        1. newArray2并记住结果
        2. 评估
        3. IndTemp并记住结果
      2. 右侧:
          评估
        1. newArray1并记住结果
        2. 评估
        3. IndTemp并记住结果(但这里是1)
        4. IndTemp增加1
        5. 通过索引从步骤2.1的索引到步骤2.2的索引来检索数组项目
      3. 回到左侧
        1. 通过索引从步骤1.1索引到步骤1.2
        2. 的数组来存储数组项
      4. 在这种情况下,步骤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++];

导致首先分配然后递增变量。

  1. newArray2 [0] = newArray1 [0]
  2. 增量
  3. newArray2 [1] = newArray1 [1]
  4. 增量
  5. 等等。

    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中的第一个元素。