Следующие два фрагмента кода C# дают разные результаты (при условии, что уровень переменной используется как до, так и после рекурсивного вызова). Почему?
public DoStuff(int level)
{
// ...
DoStuff(level++);
// ...
}
,
public DoStuff(int level)
{
// ...
DoStuff(level+1);
// ...
}
Прочитав некоторые из приведенных ниже ответов, я подумал, что было бы целесообразно опубликовать трассировки стека для уровней ++, ++ и + 1, чтобы подчеркнуть, насколько обманчива эта проблема.
Я упростил их для этого поста. Последовательность рекурсивных вызовов начинается с DoStuff (1).
// уровень ++
DoStuff(int level = 1)
DoStuff(int level = 2)
DoStuff(int level = 2)
DoStuff(int level = 2)
// ++ уровень
DoStuff(int level = 4)
DoStuff(int level = 4)
DoStuff(int level = 3)
DoStuff(int level = 2)
// уровень + 1
DoStuff(int level = 4)
DoStuff(int level = 3)
DoStuff(int level = 2)
DoStuff(int level = 1)
Трассировка вашего стека неверна. level ++ должен быть 1, 1, 1, 1; ++ уровень должен быть 1, 2, 3, 4; а уровень + 1 должен быть 1, 2, 3, 4
Орион - трассировки стека взяты прямо из VS2008. Я проследил вызовы функций до четырех уровней рекурсии и вырезал и вставил.
Ах .. Я пишу трассировку стека, причем последний вызов находится внизу, а не вверху. Я смотрю не на значения уровня, а на то, какое значение передается каждой функции. Чтобы правильно оценить стек, вы должны поставить точку останова в первой строке функции.





уровень ++ передаст уровень в DoStuff, а затем увеличит уровень для использования в остальной части функции. Это может быть довольно неприятной ошибкой, поскольку рекурсия никогда не закончится (из того, что показано, DoStuff всегда передается одно и то же значение). Возможно, вместо этого имеется в виду ++ уровень, поскольку это противоположно уровень ++ (увеличивает уровень и передает увеличенное значение в DoStuff)?
уровень + 1 передаст уровень + 1 в DoStuff и оставит уровень неизменным для остальной части функции.
Уровень ++ также производит другое поведение. Смотрите правки к моему вопросу.
Первый - использовать значение уровня и ЗАТЕМ его увеличивать.
Последний использует уровень + 1 в качестве переданной переменной.
В первом фрагменте кода используется оператор приращения после операции, поэтому вызов выполняется как DoStuff (level) ;. Если вы хотите использовать здесь оператор приращения, используйте DoStuff (уровень ++) ;.
level++ возвращает текущее значение level, затем увеличивает level на единицу.
level+1 вообще не изменяет level, но DoStuff вызывается со значением (level + 1).
Потому что первый пример действительно эквивалентен:
public DoStuff(int level)
{
// ...
int temp = level;
level = level + 1;
DoStuff(temp);
// ...
}
Обратите внимание, что вы также можете написать уровень ++; это было бы эквивалентно:
public DoStuff(int level)
{
// ...
level = level + 1;
DoStuff(level);
// ...
}
На мой взгляд, лучше не злоупотреблять операторами ++ и -; он быстро запутывается и / или становится неопределенным, что происходит на самом деле, и современные компиляторы C++ в любом случае не генерируют более эффективный код с этими операторами.
Согласились не злоупотреблять ими. Что также является «большим развлечением», так это перегрузка постов и pre ++ классом, так как тогда все ставки отключены.
Я должен не согласиться; ++ и -- чрезвычайно интуитивно понятны и просты. Проблемы возникают только тогда, когда люди либо пытаются выглядеть милыми, либо даже не утруждают себя поиском поведения операторов, которые они используют.
Так что же в этом интуитивного и легкого? :-) DoMoreStuff (уровень ++, уровень ++);
Я думаю, это классифицируется как «попытаться стать милым» :)
Вдобавок его пример неверен. Меня не удивляет, как часто люди не совсем понимают постинкремент (переменную ++), но я почти каждый раз вижу, что это неправильно. Постинкремент не оценивается после вызова. Он оценивает перед вызовом, а именно: int temp = a; а = а + 1; DoStuff (темп);
Это довольно тонкое различие для локальной переменной, хотя
Эти 2 эквивалентны для текущего примера (за исключением, конечно, нового временного создания). Однако более сложные примеры изменили бы это.
Я должен согласиться с Орионом. Этот ответ слегка демонстрирует суть дела, но вводит в заблуждение относительно того, что происходит на самом деле.
Это один из тех случаев, когда он имеет тенденцию учить людей неправильному порядку вещей (который становится шатким, когда люди пытаются обобщить). Это также наводит на мысль, что использование i ++ увеличивается после цикла for, а ++ i увеличивается до цикла for; и прочее подобное безумие.
Однако ваш второй фрагмент кода не эквивалентен исходному вопросу. В исходном вопросе переменная уровня никогда не изменяется. Если он используется где-то в другом месте (мы не знаем), тогда ваш способ даст другие результаты.
Хорошо, я отредактировал пример, чтобы он был более правильным. Я действительно считаю, что это обсуждение показывает, почему вы не должны злоупотреблять операторами до / после инкремента: есть тонкие побочные эффекты, которые не очевидны.
@Herms: Это не должно быть эквивалентным исходному вопросу.
Однако даже при редактировании поста ваш пример все равно неверен. После вызова DoStuff не должно быть кода, так как все параметры оцениваются до фактического вызова функции. Утверждать иное неверно.
level + 1 отправляет функции level + 1. level ++ отправляет уровень функции, а затем увеличивает его.
Вы можете пройти уровень ++, и это, скорее всего, даст вам желаемый результат.
Уровень ++ дает другой результат. См. Трассировку стека в моем исходном вопросе.
В первом примере используется значение «index», увеличивается значение и обновления «index».
Во втором примере используется значение index плюс 1, но содержание index не изменяется.
Так что, в зависимости от того, что вы хотите здесь сделать, вас ждут сюрпризы!
в примере используется «уровень», а не индекс. Предлагаете вам отредактировать этот ответ, чтобы подписаться?
Насколько я знаю, сначала вычисляется выражение параметра и получает значение уровня. Сама переменная увеличивается до вызова функции, потому что компилятору все равно, используете ли вы выражение в качестве параметра или иначе ... Все, что он знает, это то, что он должен увеличить значение и получить старое значение как результат выражение.
Однако, на мой взгляд, такой код действительно небрежный, поскольку, пытаясь быть умным, вы должны дважды подумать о том, что на самом деле происходит.
возвращаемое значение level++ будет level, а therefore передает level в DoStuff. Это может быть довольно неприятной ошибкой, поскольку рекурсия никогда не закончится (из того, что показано, DoStuff всегда передается с одним и тем же значением). Возможно, вместо этого имеется в виду ++level или level + 1?
level + 1 передаст level + 1 в DoStuff и оставит level без изменений для остальной части функции.
Оператор постинкремента (переменная ++) в точности эквивалентен функции
int post_increment(ref int value)
{
int temp = value;
value = value + 1
return temp;
}
в то время как оператор предварительного приращения (переменная ++) в точности эквивалентен функции
int pre_increment(ref int value)
{
value = value + 1;
return value;
}
Следовательно, если вы развернете оператор в коде, операторы будут эквивалентны:
DoStuff(a + 1)
int temp = a + 1;
DoStuff(temp);
DoStuff(++a)
a = a + 1;
DoStuff(a);
DoStuff(a++);
int temp = a;
a = a + 1;
DoStuff(temp);
Важно отметить, что постинкремент нет эквивалентен:
DoStuff(a);
a = a + 1;
Кроме того, из соображений стиля не следует увеличивать значение, если только намерение не состоит в использовании увеличенного значения (конкретная версия правила: «не присваивайте значение переменной, если вы не планируете использовать это значение» ). Если значение i + 1 больше не используется, предпочтительным вариантом будет DoStuff(i + 1), а не DoStuff(++i).
То, что вы говорите, на 100% правда. Но стоит упомянуть, что для версии с постинкрементом компилятору разрешено опускать временное и перемещать inc до его использования в простых ситуациях (например, при использовании базовых типов).
Эван, это своего рода оптимизация, которую МОЖЕТ сделать компилятор, но также такая оптимизация, которая может вызвать очень тонкие проблемы.
Это также не оптимизация, на которую можно положиться. Это деталь реализации компилятора, и поэтому вы не должны сказать, что это определенно происходит, если вы также не хотите сказать, что это происходит в этих версиях этих компиляторов.
В вашем первом примере кода есть ошибка. temp объявлен, но никогда не используется.
Orion - ваша трассировка стека уровня ++ неверна. Первый вызов DoStuff помещает в стек значение 1. Затем это значение изменяется до 2 до того, как произойдет второй вызов, но после того, как значение будет помещено в стек для следующего вызова. Ваш стек в конечном итоге представляет собой серию 2 с.
Orion - Ваш стек уровней ++ также неверен. Рассуждения аналогичны моему предыдущему комментарию, за исключением того, что результат 5,4,3,2 (или 4,4,3,2 в зависимости от того, где в вызове вы делаете снимок стека). Это можно подтвердить в VS2005 или VS2008.
Я не совсем слежу за тобой здесь. Если вы посмотрите на версию уровня ++, если первый вызов - это DoStuff (1), то следующим вызовом (первым рекурсивным вызовом) будет DoStuff (1), потому что возвращаемое значение 1 ++ равно 1, а не 2.
Чтобы прояснить все остальные ответы:
+++++++++++++++++++++
DoStuff(a++);
Эквивалентно:
DoStuff(a);
a = a + 1;
+++++++++++++++++++++
DoStuff(++a);
Эквивалентно:
a = a + 1;
DoStuff(a);
+++++++++++++++++++++
DoStuff(a + 1);
Эквивалентно:
b = a + 1;
DoStuff(b);
+++++++++++++++++++++
За исключением того, что ваш пример для DoStuff(a++) неверен. Это должно быть: int temp = a; а = а + 1; DoStuff (темп);
Параметры никогда не оцениваются после вызова функции, которой они принадлежат. Оптимизация компилятора может изменить порядок вызовов, но это выходит за рамки этого простого примера. Любое количество вещей может быть реорганизовано.
a ++ создает временную переменную перед вызовом со старым значением и сразу увеличивает ее, а не увеличивает ее впоследствии. В определенных ситуациях разница очень заметна.
действительно ли первый пример верен? в С ++, по крайней мере, ваш пример неверен. оценки аргументов завершаются до того, как будет сделан вызов (непосредственно перед вызовом есть точка последовательности). Если это верно и для C#, то ваш пример (первый) неверен.
Если a, например, является переменной-членом и DoStuff обращается к ней, он найдет старое значение, используя ваш пример, в то время как (по крайней мере, в C++) он найдет новое значение (хотя параметр, конечно, содержит старое значение) в реальный код.
Вероятно, это пример того, что общепринятое знание неверно, потому что кажется, что, несмотря на явно выраженную ошибочность этого ответа, он по-прежнему остается наиболее популярным. Иногда общеизвестные знания - это не правда.
@Orion Adrian: По крайней мере, в простом старом C (согласно K&R), a ++ увеличивает a и возвращает значение до приращения, а ++ a увеличивает значение и дает новое значение с приращением. Я ожидаю, что по крайней мере C++ будет работать так же, но я не уверен. Так что пример должен быть правильным (если C# работает так же).
@Kimmo: Проблема в ++. DoStuff (a ++) сделает приращение немедленно, но результатом функции a ++ будет неувеличенное значение. Вышесказанное относится к тому, что DoStuff (a ++) не будет увеличивать переменную a до тех пор, пока DoStuff не будет выполнен. См. Ответ Фредерика или мой для более точного перевода.
Смысл обеспечения того, что f (x ++) не эквивалентен f (x); х = х + 1; когда вы дойдете до чего-то вроде f (x ++, x ++), вы не подумаете, что в итоге получите f (x, x); х = х + 1; х = х + 1; Вместо этого вы получите temp = x; х = х + 1; temp2 = x; х = х + 1; f (temp, temp2) ;. Вот почему следующее неверное предположение. Это также приводит к «магическому» мышлению, поскольку параметры могут быть изменены после возврата вызова.
public DoStuff(int level)
{
// DoStuff(level);
DoStuff(level++);
// level = level + 1;
// here, level's value is 1 greater than when it came in
}
Фактически увеличивает значение уровня.
public DoStuff(int level)
{
// int iTmp = level + 1;
// DoStuff(iTmp);
DoStuff(level+1);
// here, level's value hasn't changed
}
фактически не увеличивает значение уровня.
Перед вызовом функции это не большая проблема, но после вызова функции значения будут другими.
Вы ошиблись с первым: сначала он вызовет DoStuff (level), а затем увеличит уровень.
Вупс. Ха-ха, поспешный ответ с моей стороны :-p
Хотя заманчиво переписать как:
DoStuff(++level);
Я лично считаю, что это менее читабельно, чем увеличение переменной до вызова метода. Как было отмечено парой ответов выше, яснее будет следующее:
level++;
DoStuff(level);
Операторы пре-инкремента и пост-инкремента предназначены для повышения лаконичности кода, не обязательно для удобочитаемости. Если вы стремитесь к удобочитаемости, вообще не используйте этот уровень оператора. Используйте level = level + 1;
Я не сказал, что он был более кратким, просто для удобства чтения. Я не согласен с использованием level = level + 1, так как это требует большего набора текста :) - Я думаю, что большинство людей знают, что делает ++, но (согласно исходному вопросу) иногда путаются с порядком.
Когда вы используете язык, который допускает перегрузку оператора, и '+ <integer>' был определен для выполнения чего-то другого, кроме пост- и префикса '++'.
Опять же, я видел такие мерзости только в школьных проектах *, если вы столкнетесь с этим в дикой природе, у вас, вероятно, есть действительно хорошая, хорошо задокументированная причина.
[* стопка целых чисел, если не ошибаюсь. '++' и '-' нажимаются и выталкиваются, а '+' и '-' выполняют обычную арифметику]
На уровне ++ вы используете постфиксный оператор. Этот оператор работает после использования переменной. То есть после того, как он помещен в стек для вызываемой функции, он увеличивается. С другой стороны, level + 1 - это простое математическое выражение, оно вычисляется, а результат передается вызываемой функции. Если вы хотите сначала увеличить переменную, а затем передать ее вызываемой функции, вы можете использовать префиксный оператор: ++ level
Проще говоря, ++var является префиксным оператором и будет увеличивать переменные перед, остальная часть выражения вычисляется. var++, постфиксный оператор, увеличивает переменную после, остальная часть выражения оценивается. И, как уже упоминали другие, конечно, var+1 создает только временную переменную (отдельную в памяти), которая инициируется с помощью var и увеличивается с помощью константы 1.
Отличный вопрос и отличный ответ! Я много лет использую C++, а совсем недавно - C#, но понятия не имел!