i ++和++ i有什么区别?


204

我看过他们在无数块的C#代码都被使用了,我想知道何时使用i++++ii是像许多变量intfloatdouble,等)。有人知道吗?


13
除非没有什么区别,否则永远不要使用任何一个,因为这只会使人们问您现在要问的相同问题。“不要让我思考”适用于代码和设计。
猎人实例


2
@Dlaor:您甚至看过我评论中的链接吗?第一个与C#有关,第二个与语言无关,其中包含针对C#的公认答案。
gnovice

7
@gnovice,第一个询问性能差异,而我询问实际的代码差异,第二个询问循环中的差异,而我询问一般的差异,第三个询问C ++。
Dlaor

1
我相信这不是循环中i ++和++ i之间差异的重复吗?-正如Dloar在他的评论上方所说,另一个问题专门询问循环中的用法。
chux x 2014年

Answers:


201

奇怪的是,其他两个答案似乎并没有清楚说明,这绝对是值得一说的:


i++表示“告诉我的值i,然后递增”

++i意思是“递增i,然后告诉我价值”


它们是预增,后增运算符。 在这两种情况下,变量都是递增的,但是如果在完全相同的情况下采用两个表达式的值,结果将有所不同。



65
两种说法都不正确。考虑您的第一个陈述。i ++实际上意味着“保存值,将其增加,将其存储在i中,然后告诉我原来保存的值”。也就是说,诉说发生递增,而不是之前,你已经说了。考虑第二条语句。i ++实际上意味着“保存值,将其增加,将其存储在i中,然后告诉我增加的值”。您所说的方式使您不清楚该值是i的值还是分配给i的值。他们可能会有所不同
埃里克·利珀特

8
@Eric,即使您的回答在我看来,第二个说法绝对正确。尽管他排除了一些步骤。
埃文·卡罗尔

20
当然,在大多数情况下,草率,错误的描述操作语义的方法会产生与精确而正确的描述相同的结果。首先,通过不正确的推理获得正确答案,我看不到任何令人信服的价值,其次,是的,我已经看到生产代码将这种事情弄错了。每年我可能会从真正的程序员那里收到六个问题,这些问题为什么某些特定的表达式充满了增量,减量和数组取消引用,却不能像他们想象的那样起作用。
埃里克·利珀特

12
@supercat:讨论是关于C#而不是C。–
Thanatos

428

不幸的是,此问题的典型答案是已经在此处发布,一个答案是在剩余操作之前“递增”,而另一个则在“剩余”操作之后递增。尽管从直觉上可以理解这个想法,但是从表面上看,这种说法是完全错误的。该时间事件的顺序是在C#中非常明确的,这是强调认为++的前缀(++ VAR)和后缀(VAR ++)版本都以不同的顺序的东西相对于其他操作的情况。

毫不奇怪,您会看到很多关于该问题的错误答案。许多“自学C#”书籍也弄错了。同样,C#的执行方式与C的执行方式不同。很多人认为C#和C是同一语言。他们不是。在我看来,C#中的递增和递减运算符的设计避免了C中这些运算符的设计缺陷。

要确定C#中前缀和后缀++的确切操作,必须回答两个问题。第一个问题是结果什么?第二个问题是增量的副作用何时发生?

这两个问题的答案并不明显,但是一旦您看到它,实际上就很简单了。让我为您详细说明x ++和++ x对变量x的作用。

对于前缀形式(++ x):

  1. 计算x以产生变量
  2. 变量的值被复制到一个临时位置
  3. 临时值递增以产生一个新值(不覆盖临时值!)
  4. 新值存储在变量中
  5. 操作的结果是新值(即临时值的递增值)

对于后缀形式(x ++):

  1. 计算x以产生变量
  2. 变量的值被复制到一个临时位置
  3. 临时值递增以产生一个新值(不覆盖临时值!)
  4. 新值存储在变量中
  5. 操作的结果是临时值

注意事项:

首先,两种情况下事件的时间顺序完全相同。同样,事件的时间顺序在前缀和后缀之间变化绝对不是绝对的情况。说评估在其他评估之前或之后进行是完全错误的。在两种情况下,评估都以完全相同的顺序进行,步骤1到步骤4完全相同。的唯一区别就是最后一步 -结果是否是临时或新的,增加值的价值。

您可以使用简单的C#控制台应用程序轻松演示这一点:

public class Application
{
    public static int currentValue = 0;

    public static void Main()
    {
        Console.WriteLine("Test 1: ++x");
        (++currentValue).TestMethod();

        Console.WriteLine("\nTest 2: x++");
        (currentValue++).TestMethod();

        Console.WriteLine("\nTest 3: ++x");
        (++currentValue).TestMethod();

        Console.ReadKey();
    }
}

public static class ExtensionMethods 
{
    public static void TestMethod(this int passedInValue) 
    {
        Console.WriteLine("Current:{0} Passed-in:{1}",
            Application.currentValue,
            passedInValue);
    }
}

结果如下...

Test 1: ++x
Current:1 Passed-in:1

Test 2: x++
Current:2 Passed-in:1

Test 3: ++x
Current:3 Passed-in:3

在第一个测试中,您可以看到currentValueTestMethod()扩展中传递的内容和预期值都显示相同的值。

但是,在第二种情况下,人们会尝试告诉您,对的调用之后currentValue发生的增量是增加的,但是正如您从结果中看到的那样,它发生调用之前,如'Current:2'结果所示。TestMethod()

在这种情况下,首先将的值currentValue存储在一个临时文件中。接下来,该值的递增版本将被存储回去,currentValue但不触摸仍存储原始值的临时项。最后,该临时变量传递给TestMethod()。如果增量是调用TestMethod()之后发生的,则它将写出相同的非增量值两次,但不会。

请务必注意,从currentValue++++currentValue操作返回的值都是基于临时值,而不是基于任一操作退出时存储在变量中的实际值。

回想一下上面操作的顺序,前两个步骤将变量的当前值复制到临时变量中。那就是用来计算返回值的东西。在前缀版本的情况下,临时值是递增的,而在后缀版本的情况下,临时值是直接/非递增。初始存储到临时变量后,不会再次读取变量本身。

简而言之,后缀版本返回从变量读取的值(即临时值),而前缀版本返回写回变量的值(即临时值递增)。都不返回变量的值。

了解这一点很重要,因为变量本身可能是易失性的,并且已在另一个线程上更改,这意味着这些操作的返回值可能与存储在变量中的当前值不同。

人们总是对优先级,关联性和执行副作用的顺序感到困惑,这是令人惊讶的普遍现象,我怀疑这主要是因为它在C中是如此令人困惑。C#经过精心设计,在所有这些方面都不会造成混乱。有关这些问题的其他分析,包括我进一步说明了前缀和后缀操作“及时移动”的想法是虚假的,请参阅:

https://ericlippert.com/2009/08/10/precedence-vs-order-redux/

这导致了这样的问题:

int [] arr = {0}; int值= arr [arr [0] ++]; 值= 1?

您可能还对我以前关于该主题的文章感兴趣:

https://ericlippert.com/2008/05/23/precedence-vs-associativity-vs-order/

https://ericlippert.com/2007/08/14/c-and-the-pit-of-despair/

还有一个有趣的情况,其中C使得很难推理正确性:

https://docs.microsoft.com/archive/blogs/ericlippert/bad-recursion-revisited

此外,在考虑其他具有副作用的操作(例如链接的简单分配)时,我们也会遇到类似的细微问题:

https://docs.microsoft.com/archive/blogs/ericlippert/chaining-simple-assignments-is-not-so-simple

而这里就是为什么增量运营商造成了一个有趣的岗位在C#中,而不是在变量

为什么我不能用C语言编写++ i ++?


4
+1:出于真正的好奇,您能否提及“ C中这些操作的设计缺陷”的含义?
贾斯汀·阿迪尼

21
@贾斯汀:我添加了一些链接。但基本上:在C语言中,您无法保证事情按时间顺序发生。当同一序列点中有两个突变时,合格的编译器可以执行任何令人讨厌的事情,并且无需告诉您您所做的事情是实现定义的行为。这导致人们编写危险的,不可移植的代码,这些代码可在某些编译器上工作,而对另一些编译器却做完全不同的事情。
埃里克·利珀特

14
我必须说,对于真正好奇的人,这是一个很好的知识,但是对于普通的C#应用​​程序,其他答案中的措辞与实际发生的事情之间的差异远低于其真正使之成为语言的抽象级别。没有不同。C#不是汇编程序,在99.9%的时间i++++i用于代码中,C#只是在后台运行;在后台。我编写C#时要升到高于该级别的抽象水平,因此,如果这对于您的C#代码确实很重要,则您可能已经使用了错误的语言。
Tomas Aschan 2010年

24
@Tomas:首先,我关注所有 C#应用程序,而不仅仅是普通应用程序的数量。非平均应用程序的数量很大。其次,除了有区别的情况外,别无其他。我可能会每年根据实际代码中的实际错误收到有关这些东西的问题,大概每年六次。第三,我同意你的观点。++的整个概念是一个非常低级的概念,在现代代码中看起来很古朴。实际上只有那里,因为类C语言具有这样的运算符是惯用的。
埃里克·利珀特

11
+1,但我必须说实话...多年来,我所做的唯一使用postfix运算符的行为并不仅仅存在于行中(因此不是i++;),for (int i = 0; i < x; i++)而且……我非常非常很高兴!(而且我从未使用过前缀运算符)。如果我必须写一些需要高级程序员2分钟才能解密的内容,那么……最好再写一行代码或引入一个临时变量:-)我认为您的“文章”(我赢了“不称其为”答案”)证明了我的选择:-)
xanatos

23

如果你有:

int i = 10;
int x = ++i;

然后x11

但是,如果您有:

int i = 10;
int x = i++;

然后x10

请注意,正如Eric所指出的,在两种情况下,增量都是同时发生的,但是结果是给出的值是不同的(感谢Eric!)。

通常,++i除非有充分的理由,否则我会喜欢使用。例如,在编写循环时,我喜欢使用:

for (int i = 0; i < 10; ++i) {
}

或者,如果我只需要增加一个变量,我喜欢使用:

++x;

通常,一种或另一种方法没有多大意义,只能归结为编码风格,但是,如果您在其他分配中使用运算符(例如在我的原始示例中),请务必注意潜在的副作用。


2
您可能可以通过按顺序排列代码行来阐明示例-也就是说,“ var x = 10; var y = ++ x;” 和“ var x = 10; var y = x ++;” 代替。
Tomas Aschan 2010年

21
这个答案很危险。当增量发生时,相对于其余操作不会改变,因此说,在一种情况下,它是其余操作之前完成的而在另一种情况下,是其余操作之后完成的这会产生严重的误导。在两种情况下,增量都是同时进行的。不同的是作为结果给出的值,而不是完成增量时给出的值
埃里克·利珀特

3
我已经对其进行了编辑,以i用于变量名,而不是var因为它是C#关键字。
杰夫·耶茨

2
因此,无需分配变量而仅声明“ i ++;”。或“ ++ i;” 会给出完全相同的结果,还是我弄错了?
Dlaor

1
@Eric:您能澄清一下原始措词为什么“危险”吗?即使细节有误,也确实可以正确使用。
贾斯汀·阿迪尼

7
int i = 0;
Console.WriteLine(i++); // Prints 0. Then value of "i" becomes 1.
Console.WriteLine(--i); // Value of "i" becomes 0. Then prints 0.

这回答了你的问题了吗 ?


4
可以想象后缀增量和前缀递减对变量没有任何影响:P
Andreas 2010年

5

运算符的工作方式是在同一时间递增,但是如果在变量之前,则表达式将使用递增/递减的变量求值:

int x = 0;   //x is 0
int y = ++x; //x is 1 and y is 1

如果在变量之后,则当前语句将使用原始变量执行,就好像尚未递增/递减一样:

int x = 0;   //x is 0
int y = x++; //'y = x' is evaluated with x=0, but x is still incremented. So, x is 1, but y is 0

我同意dcp除非需要使用预增减(++ x)。实际上,我唯一使用后递增/后递减的时间是在while循环或此类循环中。这些循环是相同的:

while (x < 5)  //evaluates conditional statement
{
    //some code
    ++x;       //increments x
}

要么

while (x++ < 5) //evaluates conditional statement with x value before increment, and x is incremented
{
    //some code
}

您还可以在索引数组等时执行此操作:

int i = 0;
int[] MyArray = new int[2];
MyArray[i++] = 1234; //sets array at index 0 to '1234' and i is incremented
MyArray[i] = 5678;   //sets array at index 1 to '5678'
int temp = MyArray[--i]; //temp is 1234 (becasue of pre-decrement);

等等...


4

仅出于记录目的,在C ++中,如果可以使用(即)您不关心操作的顺序(您只想递增或递减并在以后使用它),则前缀运算符会更有效,因为它不会必须创建对象的临时副本。不幸的是,大多数人使用posfix(var ++)而不是前缀(++ var),只是因为这是我们最初学到的。(在一次采访中有人问我)。不知道在C#中是否正确,但我认为应该是这样。


2
这就是为什么我在单独使用c#时使用++ i(即不在较大的表达式中使用)的原因。每当我看到ac#与i ++循环时,我都会哭泣一些,但现在我知道在c#中,这实际上是我感觉更好的相同代码。就个人而言,我仍然会使用++ i,因为习惯会死定。
Mikle
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.