Sự khác biệt giữa i ++ và ++ i là gì?


204

Tôi đã nhìn thấy cả hai đang được sử dụng ở nhiều mẩu mã C #, và tôi muốn biết khi nào sử dụng i++hay ++i( ilà một biến số như int, float, double, vv). Bất cứ ai biết điều này?


13
Ngoại trừ nơi nó không có gì khác biệt, bạn không bao giờ nên sử dụng một trong hai, bởi vì điều đó sẽ khiến mọi người hỏi cùng một câu hỏi mà bạn đang hỏi bây giờ. "Đừng làm tôi suy nghĩ" áp dụng cho mã cũng như thiết kế.
Thợ săn sơ thẩm


2
@Dlaor: Bạn thậm chí đã đọc các liên kết trong bình luận của tôi? Câu đầu tiên là về C #, và câu thứ hai là không biết ngôn ngữ với câu trả lời được chấp nhận tập trung vào C #.
gnovice

7
@gnovice, người đầu tiên hỏi về sự khác biệt về hiệu năng trong khi tôi hỏi về sự khác biệt về mã thực tế, thứ hai là về sự khác biệt trong một vòng lặp trong khi tôi hỏi về sự khác biệt nói chung và thứ ba là về C ++.
Dlaor

1
Tôi tin rằng đây không phải là một bản sao của Sự khác biệt giữa i ++ và ++ i trong một vòng lặp? - như Dloar nói trong bình luận của mình phía trên câu hỏi khác hỏi cụ thể về việc sử dụng trong một vòng lặp.
chue x

Câu trả lời:


201

Điều kỳ lạ là có vẻ như hai câu trả lời khác không đánh vần ra và điều đó chắc chắn đáng nói:


i++có nghĩa là 'cho tôi biết giá trị của i, sau đó tăng lên'

++icó nghĩa là 'tăng i, sau đó cho tôi biết giá trị'


Họ là các toán tử gia tăng trước, tăng sau. Trong cả hai trường hợp, biến được tăng lên , nhưng nếu bạn lấy giá trị của cả hai biểu thức trong cùng một trường hợp, kết quả sẽ khác nhau.


11
Điều này có vẻ mâu thuẫn với những gì Eric đang nói
Evan Carroll

65
Không có tuyên bố nào là đúng. Hãy xem xét tuyên bố đầu tiên của bạn. i ++ thực sự có nghĩa là "lưu giá trị, tăng giá trị, lưu trữ trong i, sau đó cho tôi biết giá trị lưu ban đầu". Đó là, việc nói xảy ra sau khi tăng, không phải trước đó như bạn đã nói. Hãy xem xét các tuyên bố thứ hai. i ++ thực sự có nghĩa là "lưu giá trị, tăng giá trị, lưu trữ trong i và cho tôi biết giá trị tăng dần". Cách bạn nói nó làm cho nó không rõ giá trị là giá trị của i hay giá trị được gán cho i; họ có thể khác nhau .
Eric Lippert

8
@Eric, dường như với tôi ngay cả câu trả lời của bạn, câu lệnh thứ hai là chính xác. Mặc dù anh ấy loại trừ một vài bước.
Evan Carroll

20
Chắc chắn, hầu hết thời gian , các cách mô tả ngữ nghĩa cẩu thả, không chính xác cho kết quả giống như mô tả chính xác và chính xác. Đầu tiên, tôi không thấy bất kỳ giá trị hấp dẫn nào trong việc có được câu trả lời đúng thông qua lý luận không chính xác, và thứ hai, vâng, tôi đã thấy mã sản xuất có chính xác loại điều này sai. Tôi có thể nhận được một nửa tá câu hỏi mỗi năm từ các lập trình viên thực sự về lý do tại sao một số biểu thức cụ thể chứa đầy các mức tăng và giảm và các cuộc hội thảo mảng không hoạt động theo cách họ nghĩ.
Eric Lippert

12
@supercat: Cuộc thảo luận là về C #, không phải C.
Thanatos

428

Câu trả lời điển hình cho câu hỏi này, không may được đăng ở đây, là một trong những hoạt động còn lại "trước" và hoạt động còn lại thực hiện gia tăng "sau" các hoạt động còn lại. Mặc dù điều đó bằng trực giác có ý tưởng xuyên suốt, nhưng tuyên bố đó là trên mặt của nó hoàn toàn sai . Các chuỗi các sự kiện trong thời gian là vô cùng rõ ràng trong C #, và nó là một cách dứt khoát không là trường hợp mà các tiền tố (++ var) và postfix (var ++) phiên bản của ++ làm những điều theo một thứ tự khác nhau liên quan đến các hoạt động khác với.

Thật không ngạc nhiên khi bạn sẽ thấy rất nhiều câu trả lời sai cho câu hỏi này. Rất nhiều cuốn sách "dạy cho mình C #" cũng hiểu sai. Ngoài ra, cách C # làm nó khác với cách C làm điều đó. Nhiều người cho rằng C # và C là cùng một ngôn ngữ; họ không phải. Theo tôi, việc thiết kế các toán tử tăng và giảm trong C # giúp tránh các lỗi thiết kế của các toán tử này trong C.

Có hai câu hỏi phải được trả lời để xác định chính xác hoạt động của tiền tố và hậu tố ++ là gì trong C #. Câu hỏi đầu tiên là kết quả là gì? và câu hỏi thứ hai là khi nào tác dụng phụ của sự gia tăng diễn ra?

Không rõ câu trả lời cho một trong hai câu hỏi là gì, nhưng nó thực sự khá đơn giản một khi bạn nhìn thấy nó. Hãy để tôi đánh vần cho bạn chính xác những gì x ++ và ++ x làm cho một biến x.

Đối với dạng tiền tố (++ x):

  1. x được ước tính để tạo ra biến
  2. Giá trị của biến được sao chép vào một vị trí tạm thời
  3. Giá trị tạm thời được tăng lên để tạo ra một giá trị mới (không ghi đè lên tạm thời!)
  4. Giá trị mới được lưu trữ trong biến
  5. Kết quả của hoạt động là giá trị mới (tức là giá trị tăng tạm thời)

Đối với dạng postfix (x ++):

  1. x được ước tính để tạo ra biến
  2. Giá trị của biến được sao chép vào một vị trí tạm thời
  3. Giá trị tạm thời được tăng lên để tạo ra một giá trị mới (không ghi đè lên tạm thời!)
  4. Giá trị mới được lưu trữ trong biến
  5. Kết quả của hoạt động là giá trị tạm thời

Một số điều cần chú ý:

Đầu tiên, thứ tự của các sự kiện trong thời gian là hoàn toàn giống nhau trong cả hai trường hợp . Một lần nữa, hoàn toàn không phải là trường hợp thứ tự các sự kiện trong thời gian thay đổi giữa tiền tố và hậu tố. Hoàn toàn sai khi nói rằng việc đánh giá xảy ra trước các đánh giá khác hoặc sau các đánh giá khác. Các đánh giá xảy ra theo cùng một thứ tự trong cả hai trường hợp như bạn có thể thấy trong các bước từ 1 đến 4 giống hệt nhau. Sự khác biệt duy nhấtbước cuối cùng - cho dù kết quả là giá trị tạm thời hay giá trị gia tăng mới.

Bạn có thể dễ dàng chứng minh điều này với một ứng dụng bảng điều khiển C # đơn giản:

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);
    }
}

Đây là kết quả ...

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

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

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

Trong thử nghiệm đầu tiên, bạn có thể thấy rằng cả hai currentValuevà những gì được truyền vào TestMethod()phần mở rộng đều hiển thị cùng một giá trị, như mong đợi.

Tuy nhiên, trong trường hợp thứ hai, mọi người sẽ cố gắng nói với bạn rằng sự gia tăng currentValuexảy ra sau cuộc gọi đến TestMethod(), nhưng như bạn có thể thấy từ kết quả, nó xảy ra trước cuộc gọi như được chỉ ra bởi kết quả 'Hiện tại: 2'.

Trong trường hợp này, đầu tiên giá trị của currentValueđược lưu trữ tạm thời. Tiếp theo, một phiên bản gia tăng của giá trị đó được lưu trữ trở lại currentValuenhưng không chạm vào tạm thời vẫn lưu trữ giá trị ban đầu. Cuối cùng, tạm thời được chuyển đến TestMethod(). Nếu gia số xảy ra sau cuộc gọi đến TestMethod()thì nó sẽ viết ra cùng một giá trị không tăng hai lần, nhưng không.

Điều quan trọng cần lưu ý là giá trị được trả về từ cả hoạt động currentValue++++currentValuehoạt động dựa trên giá trị tạm thời và không phải là giá trị thực được lưu trữ trong biến tại thời điểm cả hai hoạt động thoát ra.

Nhớ lại theo thứ tự các thao tác ở trên, hai bước đầu tiên sao chép giá trị hiện tại của biến vào tạm thời. Đó là những gì được sử dụng để tính giá trị trả lại; trong trường hợp của phiên bản tiền tố, giá trị tạm thời tăng lên trong khi trong trường hợp của phiên bản hậu tố, đó là giá trị trực tiếp / không tăng. Biến chính nó không được đọc lại sau khi lưu trữ ban đầu vào tạm thời.

Nói một cách đơn giản hơn, phiên bản postfix trả về giá trị được đọc từ biến (tức là giá trị tạm thời) trong khi phiên bản tiền tố trả về giá trị được ghi lại cho biến (tức là giá trị tăng của tạm thời). Không trả về giá trị của biến.

Điều này rất quan trọng để hiểu bởi vì chính biến đó có thể biến động và đã thay đổi trên một luồng khác, điều đó có nghĩa là giá trị trả về của các hoạt động đó có thể khác với giá trị hiện tại được lưu trữ trong biến.

Thật đáng ngạc nhiên khi mọi người rất bối rối về sự ưu tiên, sự kết hợp và thứ tự thực hiện các tác dụng phụ, tôi nghi ngờ chủ yếu bởi vì nó rất khó hiểu trong C. C # đã được thiết kế cẩn thận để ít gây nhầm lẫn trong tất cả các vấn đề này. Đối với một số phân tích bổ sung về các vấn đề này, bao gồm cả tôi chứng minh thêm về sự giả dối của ý tưởng rằng các hoạt động tiền tố và hậu tố "di chuyển công cụ theo thời gian" xem:

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

dẫn đến câu hỏi SO này:

int [] Array = {0}; int value = Array [Array [0] ++]; Giá trị = 1?

Bạn cũng có thể quan tâm đến các bài viết trước của tôi về chủ đề này:

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

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

và một trường hợp thú vị trong đó C làm cho khó lý luận về tính đúng đắn:

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

Ngoài ra, chúng tôi gặp phải các vấn đề tinh tế tương tự khi xem xét các hoạt động khác có tác dụng phụ, chẳng hạn như các bài tập đơn giản được xâu chuỗi:

https://docs.microsoft.com/archive/bloss/ericlippert/chained-simple-assignments-is-not-so-simple

Và đây là một bài viết thú vị về lý do tại sao các toán tử gia tăng dẫn đến các giá trị trong C # thay vì các biến :

Tại sao tôi không thể làm ++ i ++ bằng các ngôn ngữ giống như C?


4
+1: Đối với những người thực sự tò mò, bạn có thể đưa ra một tham chiếu như ý của bạn về "lỗi thiết kế của các hoạt động này trong C" không?
Justin Ardini

21
@Justin: Tôi đã thêm một số liên kết. Nhưng về cơ bản: trong C, bạn không có gì đảm bảo về thứ tự xảy ra trong thời gian. Một trình biên dịch tuân thủ có thể thực hiện bất kỳ điều chết tiệt nào mà nó làm hài lòng khi có hai đột biến trong cùng một điểm trình tự và không cần phải nói với bạn rằng bạn đang làm gì đó là hành vi được xác định theo thực hiện. Điều này dẫn đến việc mọi người viết mã không thể truy cập nguy hiểm, hoạt động trên một số trình biên dịch và thực hiện một cái gì đó hoàn toàn khác với những người khác.
Eric Lippert

14
Tôi phải nói rằng đối với những người thực sự tò mò, đây là kiến ​​thức tốt, nhưng đối với ứng dụng C # trung bình, sự khác biệt giữa từ ngữ trong các câu trả lời khác và nội dung thực tế đang diễn ra thấp hơn nhiều so với mức độ trừu tượng của ngôn ngữ mà nó thực sự tạo ra không khác nhau. C # không phải là trình biên dịch, và trong số 99,9% số lần i++hoặc ++iđược sử dụng trong mã, những điều đang diễn ra trong nền chỉ là như vậy; trong nền . Tôi viết C # để leo lên các mức trừu tượng trên mức đang diễn ra ở cấp độ này, vì vậy nếu điều này thực sự quan trọng đối với mã C # của bạn, bạn có thể đã sử dụng sai ngôn ngữ.
Tomas Aschan

24
@Tomas: Trước hết, tôi quan tâm đến tất cả các ứng dụng C #, không chỉ đơn thuần là khối lượng ứng dụng trung bình. Số lượng các ứng dụng không trung bình là lớn. Thứ hai, nó không tạo ra sự khác biệt ngoại trừ trong trường hợp nó tạo ra sự khác biệt . Tôi nhận được câu hỏi về công cụ này dựa trên các lỗi thực sự trong mã thực có lẽ là nửa tá mỗi năm. Thứ ba, tôi đồng ý với quan điểm lớn hơn của bạn; toàn bộ ý tưởng của ++ là một ý tưởng cấp thấp trông rất kỳ quặc trong mã hiện đại. Nó thực sự chỉ ở đó bởi vì nó là thành ngữ cho các ngôn ngữ giống như C có một toán tử như vậy.
Eric Lippert

11
+1 nhưng tôi phải nói sự thật ... Đó là năm và năm rằng việc sử dụng duy nhất các toán tử postfix mà tôi làm không phải là một mình trong hàng (vì vậy không phải vậy i++;) là trong for (int i = 0; i < x; i++)... Và tôi rất rất hạnh phúc vì điều này (và tôi chưa từng sử dụng toán tử tiền tố). Nếu tôi phải viết một cái gì đó sẽ yêu cầu một lập trình viên cao cấp 2 phút để giải mã ... Chà ... tốt hơn là viết thêm một dòng mã hoặc giới thiệu một biến tạm thời :-) Tôi nghĩ rằng "bài báo" của bạn (Tôi đã thắng 'gọi đó là "câu trả lời") minh chứng cho sự lựa chọn của tôi :-)
xanatos

23

Nếu bạn có:

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

sau đó xsẽ được 11.

Nhưng nếu bạn có:

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

sau đó xsẽ được 10.

Lưu ý như Eric chỉ ra, sự gia tăng xảy ra cùng một lúc trong cả hai trường hợp, nhưng đó là giá trị được đưa ra do kết quả khác nhau (cảm ơn Eric!).

Nói chung, tôi thích sử dụng ++itrừ khi có lý do chính đáng để không. Ví dụ, khi viết một vòng lặp, tôi thích sử dụng:

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

Hoặc, nếu tôi chỉ cần tăng một biến, tôi muốn sử dụng:

++x;

Thông thường, cách này hay cách khác không có nhiều ý nghĩa và đi theo phong cách mã hóa, nhưng nếu bạn đang sử dụng các toán tử bên trong các bài tập khác (như trong ví dụ ban đầu của tôi), điều quan trọng là phải nhận thức được các tác dụng phụ tiềm ẩn.


2
Bạn có thể có thể làm rõ các ví dụ của mình bằng cách đặt các dòng mã theo thứ tự - nghĩa là "var x = 10; var y = ++ x;" và "var x = 10; var y = x ++;" thay thế.
Tomas Aschan

21
Câu trả lời này là sai nguy hiểm. Khi gia tăng xảy ra không thay đổi đối với các hoạt động còn lại, do đó, nói rằng trong một trường hợp, nó được thực hiện trước các hoạt động còn lại và trong trường hợp khác, nó được thực hiện sau các hoạt động còn lại là sai lệch sâu sắc. Việc tăng được thực hiện cùng một lúc trong cả hai trường hợp . Điều khác biệt là giá trị nào được đưa ra như là kết quả , không phải khi gia tăng được thực hiện .
Eric Lippert

3
Tôi đã chỉnh sửa nó để sử dụng icho tên biến chứ không phải varlà từ khóa C #.
Jeff Yates

2
Vì vậy, không cần gán biến và chỉ nêu "i ++;" hoặc "++ i;" sẽ cho kết quả chính xác như vậy hay tôi nhận sai?
Dlaor

1
@Eric: Bạn có thể làm rõ lý do tại sao từ ngữ ban đầu là "nguy hiểm"? Nó không dẫn đến việc sử dụng đúng, ngay cả khi nó là chi tiết sai.
Justin Ardini

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.

Điều này có trả lời câu hỏi của bạn không?


4
Mọi người có thể tưởng tượng rằng sự gia tăng của tiền tố và sự giảm dần của tiền tố không có bất kỳ ảnh hưởng nào đến biến số: P
Andreas

5

Cách thức hoạt động của toán tử là nó được tăng lên cùng một lúc, nhưng nếu nó nằm trước một biến, biểu thức sẽ đánh giá với biến tăng / giảm:

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

Nếu là sau biến, câu lệnh hiện tại sẽ được thực thi với biến ban đầu, như thể nó chưa được tăng / giảm:

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

Tôi đồng ý với dcp trong việc sử dụng tăng / giảm trước (++ x) trừ khi cần thiết. Thực sự lần duy nhất tôi sử dụng tăng / giảm sau là trong khi các vòng lặp hoặc vòng lặp đó. Các vòng lặp này giống nhau:

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

hoặc là

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

Bạn cũng có thể làm điều này trong khi lập chỉ mục các mảng và như vậy:

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);

Vân vân...


4

Chỉ dành cho bản ghi, trong C ++, nếu bạn có thể sử dụng (nghĩa là) bạn không quan tâm đến thứ tự các thao tác (bạn chỉ muốn tăng hoặc giảm và sử dụng nó sau) thì toán tử tiền tố hiệu quả hơn vì nó không phải tạo một bản sao tạm thời của đối tượng. Thật không may, hầu hết mọi người sử dụng posfix (var ++) thay vì prefix (++ var), chỉ vì đó là những gì chúng ta đã học ban đầu. (Tôi đã được hỏi về điều này trong một cuộc phỏng vấn). Không chắc điều này có đúng trong C # không, nhưng tôi cho rằng nó sẽ như vậy.


2
Đó là lý do tại sao tôi sử dụng ++ i trong c # để sử dụng một mình (nghĩa là không phải trong một biểu thức lớn hơn). Bất cứ khi nào tôi thấy vòng lặp ac # với i ++, tôi đều khóc một chút, nhưng bây giờ tôi biết rằng trong c # này thực sự là khá nhiều mã giống như tôi cảm thấy tốt hơn. Cá nhân tôi vẫn sẽ sử dụng ++ i vì thói quen chết cứng.
Mikle
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.