24

注意:请注意,下面的代码本质上是无意义的,仅用于说明目的。

基于这样一个事实,即在赋值给左侧变量之前必须始终评估赋值的右侧,并且始终在评估后立即执行诸如++和之类的递增操作--,我不希望出现以下情况工作代码:

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

4

6 回答 6

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竖起大拇指,提出一个真正发人深省的问题!

于 2011-07-02T18:51:56.700 回答
18

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

  1. 对需要引用和记忆的第一左序表达式的事物进行求值,并考虑副作用
  2. 然后右序表达式完成

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

那么第二段代码中发生的事情是这样的:</p>

  1. 左手边:
    1. newArray2被评估并记住结果(即记住对我们想要存储东西的任何数组的引用,以防副作用稍后改变它)
    2. IndTemp被评估并记住结果
    3. IndTemp增加 1
  2. 右侧:
    1. newArray1被评估并记住结果
    2. IndTemp被评估并记住结果(但这里是 1)
    3. 通过在步骤 2.2 中的索引处从步骤 2.1 中对数组进行索引来检索数组项
  3. 回到左侧
    1. 数组项是通过在步骤 1.2 中的索引处索引到步骤 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.2 中的索引处索引到步骤 1.1 中的数组来存储的

在这种情况下,步骤 2.3 中变量的增加对当前循环迭代没有影响,因此您将始终从 index 复制N到 index N,而在第二段代码中,您将始终从 index 复制N+1到 index 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]
于 2011-07-02T20:16:25.870 回答
13
newArray2[IndTmp] = newArray1[IndTmp++];

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

  1. 新阵列2[0] = 新阵列1[0]
  2. 增量
  3. 新阵列2[1] = 新阵列1[1]
  4. 增量

等等。

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

您所描述的(抛出的异常)将是 LHS ++ 的结果:

newArray2[IndTmp] = newArray1[++IndTmp]; //throws exception
于 2011-07-02T18:08:13.787 回答
12

准确查看您的错误在哪里是有启发性的:

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

正确的。很明显,赋值的副作用只有在被赋值的值被计算之后才会发生。

诸如 ++ 和 -- 之类的递增操作总是在求值后立即执行

几乎正确。不清楚您所说的“评估”是什么意思——评估什么?原始值、增量值还是表达式的值?考虑它的最简单方法是计算原始值,然后是递增值,然后发生副作用。然后最终值是选择原始值或增量值之一,具体取决于运算符是前缀还是后缀。但是你的基本前提很好:增量的副作用在确定最终值后立即发生,然后产生最终值。

然后,您似乎从这两个正确前提得出了一个错误结论,即左侧的副作用是在评估右侧之后产生的。但是这两个前提中没有任何东西暗示这个结论!你只是凭空得出了这个结论。

如果你陈述第三个正确的前提会更清楚:

在赋值发生之前,与左侧变量关联的存储位置也必须是已知的。

显然这是真的。在分配发生之前,您需要知道件事:分配什么值,以及改变什么内存位置。你不能同时弄清楚这两件事。您必须首先找出其中一个,然后我们首先在 C# 中找出左侧的那个——变量。如果弄清楚存储的位置会导致副作用,那么在我们弄清楚第二件事之前就会产生副作用 - 分配给变量的值。

简而言之,在 C# 中,赋值给变量的计算顺序如下:

  • 发生左侧的副作用并产生变量
  • 右手边的副作用发生并产生一个
  • 该值被隐式转换为左侧的类型,这可能会产生第三个副作用
  • 赋值的副作用 - 变量的突变具有正确类型的值 - 发生,并产生一个值 - 刚刚分配给左侧的值 - 产生。
于 2011-07-05T14:20:45.453 回答
4

显然,总是在 lhs 之前评估 rhs 的假设是错误的。如果你看这里http://msdn.microsoft.com/en-us/library/aa691315(v=VS.71).aspx似乎在索引器访问索引器访问表达式的参数的情况下,这是lhs,在 rhs 之前评估。

换句话说,首先确定将rhs的结果存储在哪里,然后才评估rhs。

于 2011-07-02T18:42:02.687 回答
3

它会引发异常,因为您开始newArray1在索引 1 处进行索引。由于您在最后一个赋值中迭代每个元素,newArray1因此会引发异常,因为IndTmp等于newArray1.Length,即超过数组末尾的一个。在索引变量用于从 中提取元素之前增加它newArray1,这意味着您将崩溃并且还会错过 中的第一个元素newArray1

于 2011-07-02T18:10:40.887 回答