Stack <t>中的意外操作顺序与一个班轮相关</t>

时间:2010-02-16 14:34:52

标签: c# stack

通过在一行中调用Push()Pop() Stack<T>的实例,我得到的行为与在两行中执行imho相同代码的行为不同。

以下代码段重现了该行为:

static void Main(string[] args)
{
 Stack<Element> stack = new Stack<Element>();
 Element e1 = new Element { Value = "one" };
 Element e2 = new Element { Value = "two" };
 stack.Push(e1);
 stack.Push(e2);

 Expected(stack);  // element on satck has value "two"
 //Unexpected(stack);  // element on stack has value "one"

 Console.WriteLine(stack.Peek().Value);
 Console.ReadLine();
}

public static void Unexpected(Stack<Element> stack)
{
 stack.Peek().Value = stack.Pop().Value;
}

public static void Expected(Stack<Element> stack)
{
 Element e = stack.Pop();
 stack.Peek().Value = e.Value;
}

Element类非常基本:

public class Element
{
 public string Value
 {
  get;
  set;
 }
}

使用此代码,我得到以下结果(.NET 3.5,Win 7,完全修补):

  • 致电Expected()(版本号为。{ 两行)留下一个元素 将Value设置为"two"的堆栈。
  • 致电Unexpected()时(版本 一行)我得到一个元素 设置为Value的堆栈 "one"

我能想到的差异的唯一原因是运算符优先级。由于赋值运算符(=)具有lowest precedence,我认为没有理由为什么这两种方法的行为应该不同。

我还看了一下IL生成的内容:

.method public hidebysig static void Unexpected(class [System]System.Collections.Generic.Stack`1<class OperationOrder.Element> stack) cil managed
{
    .maxstack 8
    L_0000: ldarg.0 
    L_0001: callvirt instance !0 [System]System.Collections.Generic.Stack`1<class OperationOrder.Element>::Peek()
    L_0006: ldarg.0 
    L_0007: callvirt instance !0 [System]System.Collections.Generic.Stack`1<class OperationOrder.Element>::Pop()
    L_000c: callvirt instance string OperationOrder.Element::get_Value()
    L_0011: callvirt instance void OperationOrder.Element::set_Value(string)
    L_0016: ret 
}

我不是IL破解,但对我来说这个代码看起来还不错,应该在堆栈上留下一个元素值设置为“2”。

有人可以解释一下方法Unexpected()做出与Expected()不同的原因吗?

非常感谢!

的Lukas

2 个答案:

答案 0 :(得分:9)

在C#中,操作数从左到右进行评估。始终始终从左到右。因此,=运算符的操作数从左到右进行评估。在“预期”示例中,Pop()表达式发生在Peek()表达式语句之前运行的语句中。在“意外”示例中,Peek()表达式位于Pop()表达式的左侧,因此首先对其进行求值。

SLaks回答说明,呼叫的接收者总是在呼叫参数之前进行评估。这是正确的 - 那是因为呼叫的接收者总是在参数的左边!但是SLaks声称这与它的属性设置器不正确这一事实有关。如果Value是一个字段,你会得到完全相同的行为;包含字段访问权限的表达式位于所分配值的左侧,因此首先计算。

你提到了“优先权”,这表明你可能订阅了一个完全神话般的,完全不真实的概念,即优先权与执行顺序有关。 它没有。放弃自己对这个神话的信仰。子表达式的执行顺序是从左到右。操作符的操作按优先顺序进行。

例如,考虑F()+ G()* H()。 *优先于+。在你的神话世界中,优先级较高的操作首先完成,所以G()被计算,然后是H(),然后它们相乘,然后是F(),然后是加法。

这完全是完全错误的。跟我说吧:优先权与执行顺序无关。子表达式从左到右进行评估,因此首先我们评估F(),然后是G(),然后是H()。然后我们计算G()和H()的结果的乘积。然后我们用F()的结果计算乘积之和。也就是说,这个表达式相当于:

temp1 = F();
temp2 = G();
temp3 = H();
temp4 = temp2 * temp3;
result = temp1 + temp4;

=运算符与其他任何运算符一样;低优先级运算符,但是运算符。它的操作数从左到右进行评估,因为它是一个低优先级运算符,所以运算符的效果 - 赋值 - 比所有其他运算符的效果要晚。运算符的效果和操作数的计算是完全不同的事情。前者按优先顺序完成。后者按从左到右的顺序完成。

这是清楚的吗?

更新:令人困惑的优先级,关联性和执行顺序非常普遍;许多具有丰富编程语言设计经验的书籍作者都错了。 C#有非常严格的规则来定义每个;如果你对这一切有用的更多细节感兴趣,你可能会对我在这个主题上写的这些文章感兴趣:

http://blogs.msdn.com/ericlippert/archive/tags/precedence/default.aspx

答案 1 :(得分:5)

你的表达相当于

stack.Peek().set_Value(stack.Pop().Value);

<击> When calling an instance method on a reference type, the instance is evaluated first

编辑:正如Eric Lippert指出的那样,所有表达式都是从左到右进行评估。