Неожиданный порядок работы в стеке <T>, связанный с одним вкладышем - PullRequest
5 голосов
/ 16 февраля 2010

Вызывая 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;
}

Класс Element действительно базовый:

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

С этим кодом я получаю следующий результат (.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 
}

Я не взломщик IL, но для меня этот код по-прежнему выглядит хорошо, и следует оставить один элемент в стеке со значением, равным «два».

Может кто-нибудь объяснить мне причину, по которой метод Unexpected() делает что-то отличное от Expected()?

Большое спасибо!

Lukas

Ответы [ 2 ]

9 голосов
/ 16 февраля 2010

В 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

5 голосов
/ 16 февраля 2010

Ваше выражение эквивалентно

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

При вызове метода экземпляра для ссылочного типа экземпляр оценивается первым .

РЕДАКТИРОВАТЬ : Как указывает Эрик Липперт, все выражения оцениваются слева направо.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...