Tại sao việc hoán đổi giá trị với XOR không thành công khi sử dụng dạng kết hợp này?


76

Tôi đã tìm thấy mã này để hoán đổi hai số mà không sử dụng biến thứ ba, sử dụng toán ^tử XOR .

Mã:

int i = 25;
int j = 36;
j ^= i;       
i ^= j;
j ^= i;

Console.WriteLine("i:" + i + " j:" + j);

//numbers Swapped correctly
//Output: i:36 j:25

Bây giờ tôi đã thay đổi mã trên thành mã tương đương này.

Mã của tôi:

int i = 25;
int j = 36;

j ^= i ^= j ^= i;   // I have changed to this equivalent (???).

Console.WriteLine("i:" + i + " j:" + j);

//Not Swapped correctly            
//Output: i:36 j:0

Bây giờ, tôi muốn biết, Tại sao mã của tôi đưa ra kết quả không chính xác?


2
Nó sẽ là câu trả lời giống như được đưa ra cho: stackoverflow.com/questions/3741440 hoặc nhiều câu trả lời khác trong cột Liên quan. Mặc dù họ nói C ++ và đây là C #, các quy tắc tương tự sẽ được áp dụng.
Daemin

8
@Daemin: Không, các quy tắc tương tự không được áp dụng. Đó là hành vi không xác định trong C ++, nhưng tôi không tin rằng nó không xác định trong C #.
Jon Skeet

3
@Daemin - Đây là một bài đăng liên quan của Eric Lippert, chỉ hai ngày trước: stackoverflow.com/questions/5538193/… , cụ thể: "Các câu trả lời khác chỉ ra rằng trong ngôn ngữ lập trình C và C ++, các đặc tả ngôn ngữ không chỉ định Các tác dụng phụ xuất hiện theo thứ tự nào nếu tác dụng phụ và sự quan sát của nó nằm trong cùng một "điểm trình tự", như chúng ở đây. [...] C # không cho phép loại độ trễ đó. Trong C #, một tác dụng phụ đối với bên trái được quan sát là đã xảy ra vào thời điểm mã bên phải thực thi. "
Kobi

2
Một ngày nào đó tôi muốn nghĩ đến một câu hỏi đó là đủ thú vị cho Jon Skeet để tweet về ...
David Johnstone

8
Đối với những người đã đóng câu hỏi - câu hỏi khác liên quan đến C, C ++ trong đó kết quả không được xác định vì thiếu điểm trình tự. Trong khi câu hỏi này liên quan đến C #, nơi câu trả lời được xác định rõ ràng, nhưng khác với những gì có thể mong đợi. Vì vậy, tôi sẽ không coi nó là một câu hỏi trùng lặp, bởi vì các câu trả lời khác nhau rõ ràng.
Damien_The_Un Believer

Câu trả lời:


77

CHỈNH SỬA: Được rồi, hiểu rồi.

Điểm đầu tiên cần làm là rõ ràng bạn không nên sử dụng mã này. Tuy nhiên, khi bạn mở rộng nó, nó sẽ tương đương với:

j = j ^ (i = i ^ (j = j ^ i));

(Nếu chúng ta đang sử dụng một biểu thức phức tạp hơn chẳng hạn foo.bar++ ^= i, điều quan trọng là nó ++chỉ được đánh giá một lần, nhưng ở đây tôi tin rằng nó đơn giản hơn.)

Bây giờ, thứ tự đánh giá các toán hạng luôn từ trái sang phải, vì vậy để bắt đầu, chúng ta có:

j = 36 ^ (i = i ^ (j = j ^ i));

Đây (ở trên) là bước quan trọng nhất. Chúng tôi đã kết thúc với 36 là LHS cho hoạt động XOR được thực hiện cuối cùng. LHS không phải là "giá trị jsau khi RHS đã được đánh giá".

Việc đánh giá RHS của ^ liên quan đến biểu thức "lồng nhau một cấp", vì vậy nó trở thành:

j = 36 ^ (i = 25 ^ (j = j ^ i));

Sau đó, xem xét mức độ lồng ghép sâu nhất, chúng ta có thể thay thế cả hai ij:

j = 36 ^ (i = 25 ^ (j = 25 ^ 36));

... trở thành

j = 36 ^ (i = 25 ^ (j = 61));

Việc gán cho jtrong RHS xảy ra đầu tiên, nhưng dù sao thì kết quả sau đó vẫn được ghi đè ở cuối, vì vậy chúng ta có thể bỏ qua điều đó - không có đánh giá nào nữa jtrước khi thực hiện nhiệm vụ cuối cùng:

j = 36 ^ (i = 25 ^ 61);

Điều này bây giờ tương đương với:

i = 25 ^ 61;
j = 36 ^ (i = 25 ^ 61);

Hoặc là:

i = 36;
j = 36 ^ 36;

Trở thành:

i = 36;
j = 0;

Tôi nghĩ tất cả đều đúng và nó sẽ có câu trả lời đúng ... xin lỗi Eric Lippert nếu một số chi tiết về thứ tự đánh giá hơi sai :(


1
IL gợi ý rằng đây chính xác là những gì sẽ xảy ra.
SWeko

1
@Jon Đây không phải chỉ là một cách khác để chứng minh rằng bạn không nên có các biểu thức với các biến có tác dụng phụ, trừ khi bạn chỉ sử dụng biến một lần?
Lasse V. Karlsen

8
@Lasse: Hoàn toàn có thể. Code như thế này là kinh khủng.
Jon Skeet

8
Tại sao một trong những chuyên gia C # giỏi nhất xung quanh lại nói với tần suất như vậy "bạn không nên sử dụng mã này" trong câu trả lời SO? ;-)
Fredrik Mörk

8
@Fredrik: Trong trường hợp này, tôi không nghĩ ra mã để bắt đầu. Nó hơi khác khi ai đó hỏi làm thế nào bạn có thể đạt được một cái gì đó và tôi có nguồn gốc mã khủng khiếp :)
Jon Skeet

15

Đã kiểm tra IL được tạo và nó cho kết quả khác nhau;

Việc hoán đổi chính xác tạo ra một sự đơn giản:

IL_0001:  ldc.i4.s   25
IL_0003:  stloc.0        //create a integer variable 25 at position 0
IL_0004:  ldc.i4.s   36
IL_0006:  stloc.1        //create a integer variable 36 at position 1
IL_0007:  ldloc.1        //push variable at position 1 [36]
IL_0008:  ldloc.0        //push variable at position 0 [25]
IL_0009:  xor           
IL_000a:  stloc.1        //store result in location 1 [61]
IL_000b:  ldloc.0        //push 25
IL_000c:  ldloc.1        //push 61
IL_000d:  xor 
IL_000e:  stloc.0        //store result in location 0 [36]
IL_000f:  ldloc.1        //push 61
IL_0010:  ldloc.0        //push 36
IL_0011:  xor
IL_0012:  stloc.1        //store result in location 1 [25]

Việc hoán đổi không chính xác tạo ra mã này:

IL_0001:  ldc.i4.s   25
IL_0003:  stloc.0        //create a integer variable 25 at position 0
IL_0004:  ldc.i4.s   36
IL_0006:  stloc.1        //create a integer variable 36 at position 1
IL_0007:  ldloc.1        //push 36 on stack (stack is 36)
IL_0008:  ldloc.0        //push 25 on stack (stack is 36-25)
IL_0009:  ldloc.1        //push 36 on stack (stack is 36-25-36)
IL_000a:  ldloc.0        //push 25 on stack (stack is 36-25-36-25)
IL_000b:  xor            //stack is 36-25-61
IL_000c:  dup            //stack is 36-25-61-61
IL_000d:  stloc.1        //store 61 into position 1, stack is 36-25-61
IL_000e:  xor            //stack is 36-36
IL_000f:  dup            //stack is 36-36-36
IL_0010:  stloc.0        //store 36 into positon 0, stack is 36-36 
IL_0011:  xor            //stack is 0, as the original 36 (instead of the new 61) is xor-ed)
IL_0012:  stloc.1        //store 0 into position 1

Rõ ràng là mã được tạo trong phương thức thứ hai là không đúng, vì giá trị cũ của j được sử dụng trong một phép tính mà giá trị mới được yêu cầu.


Tôi đã kiểm tra đầu ra và nó cho kết quả khác nhau :) . Câu hỏi đặt ra là tại sao điều đó xảy ra ...
Kobi

Vì vậy, nó đang tải tất cả các giá trị mà nó cần để đánh giá toàn bộ biểu thức vào ngăn xếp trước, sau đó phân chia và lưu các giá trị trở lại các biến khi nó diễn ra (vì vậy nó sẽ sử dụng các giá trị ban đầu của i và j trong suốt quá trình đánh giá biểu thức)
Damien_The_Un Believer

Thêm lời giải thích cho IL thứ hai
SWeko

1
Thật tiếc là mã cho một sự hoán đổi không chỉ ldloc.0; ldloc.1; stloc.0; stloc.1. Trong C # dù sao; IL hoàn toàn hợp lệ. Bây giờ tôi nghĩ về nó ... tôi tự hỏi liệu C # có tối ưu hóa biến tạm thời ra khỏi hoán đổi hay không.
cHao

7

C # tải j, i, j, itrên stack, và các cửa hàng mỗi XORkết quả mà không cập nhật chồng, do đó tận cùng bên trái XORsử dụng các giá trị ban đầu cho j.


0

Viết lại:

j ^= i;       
i ^= j;
j ^= i;

Mở rộng ^=:

j = j ^ i;       
i = j ^ i;
j = j ^ i;

Thay thế:

j = j ^ i;       
j = j ^ (i = j ^ i);

Thay thế này chỉ hoạt động nếu / vì phía bên trái của toán tử ^ được đánh giá đầu tiên:

j = (j = j ^ i) ^ (i = i ^ j);

Thu gọn ^:

j = (j ^= i) ^ (i ^= j);

Đối xứng:

i = (i ^= j) ^ (j ^= i);
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.