Stack <T>中的意外操作顺序与一个衬管有关

Luk*_*Ith 5 c# stack

通过调用Push()Pop()实例Stack<T>在一行中我得到比在两行执行恕我直言相同的代码不同的行为.

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

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

Element类非常基本:

public class Element
{
 public string Value
 {
  get;
  set;
 }
}
Run Code Online (Sandbox Code Playgroud)

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

  • 调用Expected()(带有两行的版本)在堆栈上留下一个元素,Value设置为"two".
  • 当调用Unexpected()(带有一行的版本)时,我在堆栈中得到一个元素,Value设置为 "one".

我能想象的差异的唯一原因是运算符优先级.由于赋值运算符(=)具有最低优先级,因此我认为没有理由为什么这两个方法的行为应该不同.

我还看了一下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 
}
Run Code Online (Sandbox Code Playgroud)

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

任何人都可以解释一下这个方法Unexpected()有什么不同的原因Expected()吗?

非常感谢!

卢卡斯

Eri*_*ert 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;
Run Code Online (Sandbox Code Playgroud)

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

明白了吗?

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

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


SLa*_*aks 5

你的表达相当于

stack.Peek().set_Value(stack.Pop().Value);
Run Code Online (Sandbox Code Playgroud)

在引用类型上调用实例方法时,首先计算实例.

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