C# 中奇怪的增量行为

发布于 2024-11-17 23:40:09 字数 899 浏览 1 评论 0原文

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

基于这样的事实:在将赋值给左侧变量之前,必须始终对赋值的右侧进行求值,并且增量操作如 ++- - 总是在评估后立即执行,我不希望以下代码起作用:

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 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(6

拔了角的鹿 2024-11-24 23:40:09

有时,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提出一个真正发人深省的问题点赞!

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:

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"

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:

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"

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

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"

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!

黑白记忆 2024-11-24 23:40:09

根据 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.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 中变量的增加对当前循环迭代没有影响,因此您将始终从将 N 索引到索引 N 中,而在第二段代码中,您始终会从索引 N+1 复制到索引 N< /代码>。

Eric 有一个博客条目,标题为 优先级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]

This is well-defined in the C# language according to Eric Lippert and is easily explained.

  1. First left-order expression things that needs to be referenced and remembered is evaluated, and side-effects are taken into account
  2. Then right-order expression is done

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:

  1. Left-hand side:
    1. 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)
    2. IndTemp is evaluated and the result is remembered
    3. IndTemp is increased by 1
  2. Right-hand side:
    1. newArray1 is evaluated and the result is remembered
    2. IndTemp is evaluated and the result is remembered (but this is 1 here)
    3. The array item is retrieved by indexing into the array from step 2.1 at index from step 2.2
  3. Back to left-hand side
    1. The array item is stored by indexing into the array from step 1.1 at index from step 1.2

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:

  1. Left-hand side:
    1. newArray2 is evaluated and the result is remembered
    2. IndTemp is evaluated and the result is remembered
  2. Right-hand side:
    1. newArray1 is evaluated and the result is remembered
    2. IndTemp is evaluated and the result is remembered (but this is 1 here)
    3. IndTemp is increased by 1
    4. The array item is retrieved by indexing into the array from step 2.1 at index from step 2.2
  3. Back to left-hand side
    1. The array item is stored by indexing into the array from step 1.1 at index from step 1.2

In 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 index N, whereas in the second piece of code you will always copy from index N+1 into index N.

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.

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 + "]");
        }
    }
}

Output is:

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]
梦途 2024-11-24 23:40:09
newArray2[IndTmp] = newArray1[IndTmp++];

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

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

,依此类推。

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

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

newArray2[IndTmp] = newArray1[++IndTmp]; //throws exception
newArray2[IndTmp] = newArray1[IndTmp++];

leads to first assinging and then incrementing the variable.

  1. newArray2[0] = newArray1[0]
  2. increment
  3. newArray2[1] = newArray1[1]
  4. increment

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 ++:

newArray2[IndTmp] = newArray1[++IndTmp]; //throws exception
素食主义者 2024-11-24 23:40:09

准确地查看错误所在是有启发性的:

在将赋值给左侧变量之前,必须先计算右侧的赋值

Correct 之前,必须始终先评估赋值的右侧。显然,在计算所分配的值之前,分配的副作用不会发生。

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

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

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

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

在赋值之前,还必须知道与左侧变量关联的存储位置。

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

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

  • 左侧发生副作用,生成变量
  • 右侧发生副作用,< em>value 产生的
  • 值被隐式转换为左侧的类型,这可能会产生第三个副作用——
  • 赋值的副作用——变量的突变以获得正确的值type -- 发生,并且 value -- 刚刚分配给左手的值边——产生。

It is instructive to see exactly where your error is:

the right-hand side of an assignment must always be evaluated before it's value is assigned to the left-hand side variable

Correct. Clearly the side effect of the assignment cannot happen until after the value being assigned has been computed.

increment operations such as ++ and -- are always performed right after evaluation

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:

the storage location associated with the left-hand-side variable also must be known before the assignment takes place.

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:

  • side effects of the left hand side happen and a variable is produced
  • side effects of the right hand side happen and a value is produced
  • the value is implicitly converted to the type of the left hand side, which may produce a third side effect
  • the side effect of the assignment -- the mutation of the variable to have the value of the correct type -- happens, and a value -- the value just assigned to the left hand side -- is produced.
不知所踪 2024-11-24 23:40:09

显然,总是在左侧之前评估右侧的假设是错误的。如果您查看此处 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.

∞梦里开花 2024-11-24 23:40:09

它会引发异常,因为您在索引 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 in newArray1 the last assignment throws an exception because IndTmp is equal to newArray1.Length, i.e., one past the end of the array. You increment the index variable before it is ever used to extract an element from newArray1, which means you will crash and also miss the first element in newArray1.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文