C#中的奇怪增量行为

Use*_*ser 24 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++];
}
Run Code Online (Sandbox Code Playgroud)

相反,我希望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];
}
Run Code Online (Sandbox Code Playgroud)

因为,在我的理解中,编译器首先评估RHS,将其分配给LHS,然后才增加这对我来说是一个意外的行为.或者它真的是预期的,我显然错过了什么?

Ste*_*gan 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"
Run Code Online (Sandbox Code Playgroud)

对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"
Run Code Online (Sandbox Code Playgroud)

这里,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"
Run Code Online (Sandbox Code Playgroud)

这里,在更新IndTmp之前,增量的结果已被推送到堆栈(并成为数组索引).

总之,似乎首先评估赋值的目标,然后是.

对OP提出了一个非常引人深思的问题!


ang*_*son 18

根据Eric Lippert的说法,这在C#语言中得到了很好的定义,并且很容易解释.

  1. 首先评估需要引用和记住的左序表达式,并考虑副作用
  2. 然后完成右序表达

注意:代码的实际执行可能不是这样的,重要的是要记住编译器必须创建与此等效的代码

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

  1. 左手边:
    1. newArray2 进行评估并记住结果(即,记住对我们想要存储内容的任何数组的引用,以防后来的副作用发生变化)
    2. IndTemp 进行评估并记住结果
    3. IndTemp 增加1
  2. 右侧:
    1. newArray1 进行评估并记住结果
    2. IndTemp 被评估并记住结果(但这里是1)
    3. 通过在步骤2.2的索引处从索引到步骤2.1中的数组来检索数组项
  3. 回到左侧
    1. 通过从步骤1.1的索引处索引到数组中来存储数组项

如您所见,第二次IndTemp被评估(RHS),该值已经增加了1,但这对LHS没有影响,因为它记住在增加之前值为0.

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

  1. 左手边:
    1. newArray2 进行评估并记住结果
    2. IndTemp 进行评估并记住结果
  2. 右侧:
    1. newArray1 进行评估并记住结果
    2. IndTemp 被评估并记住结果(但这里是1)
    3. IndTemp 增加1
    4. 通过在步骤2.2的索引处从索引到步骤2.1中的数组来检索数组项
  3. 回到左侧
    1. 通过从步骤1.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 + "]");
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

输出是:

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


Pet*_*nov 13

newArray2[IndTmp] = newArray1[IndTmp++];
Run Code Online (Sandbox Code Playgroud)

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

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

等等.

RHS ++运算符立即递增,但它在递增之前返回值.用于在数组中索引的值是RHS ++运算符返回的值,因此非递增值.

您描述的内容(引发的异常)将是LHS ++的结果:

newArray2[IndTmp] = newArray1[++IndTmp]; //throws exception
Run Code Online (Sandbox Code Playgroud)


Eri*_*ert 12

确切地看到错误的位置是有益的:

在将赋值分配给左侧变量之前,必须始终评估赋值的右侧

正确.显然,在计算分配的值之后才能发生分配的副作用.

增量操作如++和 - 总是在评估后立即执行

差不多正确.目前尚不清楚"评价"是什么意思 - 评价什么?原始值,递增值或表达式的值?考虑它的最简单方法是计算原始值,然后计算增量值,然后发生副作用.然后,最终值是选择原始值或递增值之一,具体取决于运算符是前缀还是后缀.但是你的基本前提非常好:在确定最终值之后立即发生增量的副作用,然后产生最终值.

然后你似乎从这两个正确的前提中得出了一个错误,即左手边的副作用是在评估右手边之后产生的.但这两个前提中没有任何内容暗示这个结论!你刚刚凭空捏造了这个结论.

如果你说出第三个正确的前提,那就更清楚了:

与左手侧变量相关联的存储位置分配发生之前必须是已知的.

显然这是事实.在分配发生之前,您需要了解件事:分配了什么值,以及正在变异的内存位置.你无法同时解决这两件事; 你必须首先找出其中一个,然后我们找出左边的一个 - 变量 - 首先在C#中.如果找出存储所在的位置会产生副作用,那么在我们弄清楚第二件事 - 分配给变量的值之前,会产生副作用.

简而言之,在C#中,对变量赋值的求值顺序如下:

  • 发生左侧的副作用和一个可变产生
  • 发生的右手侧的副作用和一个产生
  • 该值被隐式转换为左侧的类型,这可能产生第三个副作用
  • 分配的副作用 - 变量的变异具有正确类型的值 - 发生,并且产生一个值 - 刚分配给左侧的值.