Tại sao trình biên dịch C # lại dịch cái này! = So sánh như thể nó là một so sánh?


147

Tôi đã tình cờ phát hiện ra rằng trình biên dịch C # biến phương thức này:

static bool IsNotNull(object obj)
{
    return obj != null;
}

Liên kết vào CIL này :

.method private hidebysig static bool IsNotNull(object obj) cil managed
{
    ldarg.0   // obj
    ldnull
    cgt.un
    ret
}

Nếu bạn muốn xem mã C # đã dịch ngược:

static bool IsNotNull(object obj)
{
    return obj > null;   // (note: this is not a valid C# expression)
}

Làm thế nào mà !=được dịch là " >"?

Câu trả lời:


201

Câu trả lời ngắn:

Không có hướng dẫn "so sánh không bằng" trong IL, vì vậy !=toán tử C # không có sự tương ứng chính xác và không thể được dịch theo nghĩa đen.

Tuy nhiên, có một lệnh "so sánh bằng" ( ceq, tương ứng trực tiếp với ==toán tử), vì vậy trong trường hợp chung, x != yđược dịch giống như tương đương dài hơn một chút (x == y) == false.

Ngoài ra còn có một lệnh "so sánh lớn hơn" trong IL ( cgt) cho phép trình biên dịch thực hiện một số phím tắt nhất định (nghĩa là tạo mã IL ngắn hơn), một trong số đó là so sánh bất bình đẳng của các đối tượng với null obj != null, được dịch như thể chúng là " obj > null".

Chúng ta hãy đi vào chi tiết hơn.

Nếu không có lệnh "so sánh không bằng" trong IL, thì phương thức sau đây sẽ được dịch bởi trình biên dịch như thế nào?

static bool IsNotEqual(int x, int y)
{
    return x != y;
}

Như đã nói ở trên, trình biên dịch sẽ biến x != ythành (x == y) == false:

.method private hidebysig static bool IsNotEqual(int32 x, int32 y) cil managed 
{
    ldarg.0   // x
    ldarg.1   // y
    ceq
    ldc.i4.0  // false
    ceq       // (note: two comparisons in total)
    ret
}

Nó chỉ ra rằng trình biên dịch không phải lúc nào cũng tạo ra mô hình khá dài này. Hãy xem điều gì xảy ra khi chúng ta thay thế ybằng hằng số 0:

static bool IsNotZero(int x)
{
    return x != 0;
}

IL được sản xuất có phần ngắn hơn so với trường hợp chung:

.method private hidebysig static bool IsNotZero(int32 x) cil managed 
{
    ldarg.0    // x
    ldc.i4.0   // 0
    cgt.un     // (note: just one comparison)
    ret
}

Trình biên dịch có thể lợi dụng thực tế là các số nguyên đã ký được lưu trữ trong phần bù hai (trong đó, nếu các mẫu bit kết quả được hiểu là các số nguyên không dấu - đó là những gì .uncó nghĩa là - 0 có giá trị nhỏ nhất có thể), vì vậy nó dịch x == 0như thể nó là unchecked((uint)x) > 0.

Hóa ra trình biên dịch có thể làm tương tự đối với kiểm tra bất đẳng thức đối với null:

static bool IsNotNull(object obj)
{
    return obj != null;
}

Trình biên dịch tạo ra gần như cùng IL với IsNotZero:

.method private hidebysig static bool IsNotNull(object obj) cil managed 
{
    ldarg.0
    ldnull   // (note: this is the only difference)
    cgt.un
    ret
}

Rõ ràng, trình biên dịch được phép giả định rằng mẫu bit của nulltham chiếu là mẫu bit nhỏ nhất có thể cho bất kỳ tham chiếu đối tượng nào.

Phím tắt này được đề cập rõ ràng trong Tiêu chuẩn cơ sở hạ tầng ngôn ngữ chung (phiên bản 1 từ tháng 10 năm 2003) (trên trang 491, như một chú thích của Bảng 6-4, "So sánh nhị phân hoặc Hoạt động chi nhánh"):

" cgt.unđược cho phép và có thể kiểm chứng trên ObjectRefs (O). Điều này thường được sử dụng khi so sánh ObjectRef với null (không có hướng dẫn" so sánh không bằng ", nếu không sẽ là một giải pháp rõ ràng hơn)."


3
Câu trả lời tuyệt vời, chỉ một nit: bổ sung của hai không liên quan ở đây. Chỉ có vấn đề là số nguyên đã ký được lưu trữ theo cách mà các giá trị không âm trong intphạm vi có cùng biểu diễn intgiống như khi chúng thực hiện uint. Đó là một yêu cầu yếu hơn nhiều so với bổ sung của hai.

3
Các kiểu không dấu không bao giờ có bất kỳ số âm nào, do đó, một phép toán so sánh bằng 0 không thể coi bất kỳ số nào khác không nhỏ hơn 0. Tất cả các biểu diễn tương ứng với các giá trị không âm intđã được đưa lên bởi cùng một giá trị uint, vì vậy tất cả các biểu diễn tương ứng với các giá trị âm intphải tương ứng với một giá trị uintlớn hơn 0x7FFFFFFF, nhưng thực sự không quan trọng giá trị nào Là. (Trên thực tế, tất cả những gì thực sự cần thiết là số 0 được thể hiện theo cùng một cách trong cả hai intuint.)

3
@hvd: Cảm ơn đã giải thích. Bạn nói đúng, đó không phải là bổ sung của hai vấn đề; đó là yêu cầu mà bạn đã đề cập thực tế cgt.uncoi nó intnhư một uintmà không thay đổi mẫu bit bên dưới. (Hãy tưởng tượng rằng cgt.unđầu tiên sẽ cố gắng underflows sửa chữa bằng cách ánh xạ tất cả các số tiêu cực đến 0. Trong trường hợp đó bạn rõ ràng là không thể thay thế > 0cho != 0.)
stakx - không còn góp

2
Tôi thấy ngạc nhiên khi so sánh một tham chiếu đối tượng với một đối tượng khác bằng cách sử dụng >IL có thể kiểm chứng được. Bằng cách đó, người ta có thể so sánh hai đối tượng không null và nhận được kết quả boolean (không xác định). Đó không phải là vấn đề an toàn bộ nhớ nhưng có vẻ như thiết kế ô uế không theo tinh thần chung của mã được quản lý an toàn. Thiết kế này rò rỉ thực tế là các tham chiếu đối tượng được thực hiện như con trỏ. Có vẻ như là một lỗ hổng thiết kế của .NET CLI.
usr

3
@usr: Tuyệt đối! Mục III.1.1.4 của tiêu chuẩn CLI nói rằng "Tham chiếu đối tượng (loại O) hoàn toàn mờ đục" và rằng "các hoạt động so sánh duy nhất được phép là bình đẳng và bất bình đẳng." Có lẽ vì tham chiếu đối tượng đang không xác định theo địa chỉ bộ nhớ, tiêu chuẩn cũng sẽ chăm sóc cho khái niệm giữ tham chiếu null ngoài 0 (xem ví dụ như định nghĩa về ldnull, initobjnewobj). Vì vậy, việc sử dụng cgt.unđể so sánh các tham chiếu đối tượng so với tham chiếu null dường như mâu thuẫn với phần III.1.1.4 theo nhiều cách.
stakx - không còn đóng góp vào
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.