C # ổn với việc so sánh các loại giá trị với null


85

Tôi gặp phải vấn đề này ngày hôm nay và không biết tại sao trình biên dịch C # không gặp lỗi.

Int32 x = 1;
if (x == null)
{
    Console.WriteLine("What the?");
}

Tôi bối rối không biết làm thế nào mà x có thể là null. Đặc biệt là vì bài tập này chắc chắn gây ra lỗi trình biên dịch:

Int32 x = null;

Có khả năng x có thể trở thành null, Microsoft chỉ quyết định không đưa kiểm tra này vào trình biên dịch, hay nó đã bị bỏ lỡ hoàn toàn?

Cập nhật: Sau khi lộn xộn với mã để viết bài viết này, đột nhiên trình biên dịch đưa ra cảnh báo rằng biểu thức sẽ không bao giờ đúng. Bây giờ tôi thực sự lạc lối. Tôi đặt đối tượng vào một lớp và bây giờ cảnh báo đã biến mất nhưng vẫn để lại câu hỏi, liệu một kiểu giá trị có thể kết thúc bằng null không.

public class Test
{
    public DateTime ADate = DateTime.Now;

    public Test ()
    {
        Test test = new Test();
        if (test.ADate == null)
        {
            Console.WriteLine("What the?");
        }
    }
}

9
Bạn cũng có thể viết if (1 == 2). Công việc của trình biên dịch không phải là thực hiện phân tích đường dẫn mã; đó là mục đích của các công cụ phân tích tĩnh và kiểm thử đơn vị.
Aaronaught

Để biết tại sao cảnh báo biến mất, hãy xem câu trả lời của tôi; và không - nó không thể là giá trị rỗng.
Marc Gravell

1
Đồng ý trên (1 == 2), tôi đã tự hỏi thêm về tình hình (1 == null)
Joshua Belden

Cảm ơn tất cả mọi người đã trả lời. Tất cả đều có ý nghĩa ngay bây giờ.
Joshua Belden

Về vấn đề cảnh báo hoặc không có cảnh báo: Nếu cấu trúc được đề cập là một "loại đơn giản", chẳng hạn như int, trình biên dịch tạo ra các cảnh báo tốt. Đối với các kiểu đơn giản, ==toán tử được xác định bởi đặc tả ngôn ngữ C #. Đối với các cấu trúc (không phải loại đơn giản) khác, trình biên dịch quên phát ra cảnh báo. Xem Cảnh báo trình biên dịch sai khi so sánh struct với null để biết chi tiết. Đối với các cấu trúc không phải là kiểu đơn giản, ==toán tử phải được nạp chồng bởi một opeartor ==phương thức là thành viên của cấu trúc (nếu không thì không ==được phép).
Jeppe Stig Nielsen

Câu trả lời:


119

Điều này là hợp pháp vì giải quyết quá tải của nhà điều hành có một nhà điều hành tốt nhất duy nhất để lựa chọn. Có một toán tử == nhận hai giá trị int null. Int local có thể chuyển đổi thành int nullable. Chữ null có thể chuyển đổi thành int nullable. Do đó, đây là cách sử dụng hợp pháp của toán tử == và sẽ luôn dẫn đến sai.

Tương tự, chúng tôi cũng cho phép bạn nói "if (x == 12,6)", điều này cũng sẽ luôn là false. Int local có thể chuyển đổi thành double, nghĩa đen là chuyển đổi thành double, và rõ ràng là chúng sẽ không bao giờ bằng nhau.



5
@James: (Tôi rút lại nhận xét sai lầm trước đó của mình, mà tôi đã xóa.) Các loại giá trị do người dùng xác định có toán tử bình đẳng do người dùng xác định cũng theo mặc định có toán tử bình đẳng nâng do người dùng xác định được tạo cho chúng. Toán tử bình đẳng được nâng cao do người dùng xác định có thể áp dụng vì lý do bạn nêu: tất cả các kiểu giá trị đều có thể chuyển đổi hoàn toàn thành kiểu nullable tương ứng của chúng, cũng như chữ null. Không phải là trường hợp kiểu giá trị do người dùng xác định thiếu toán tử so sánh do người dùng xác định có thể so sánh với chữ null.
Eric Lippert

3
@James: Chắc chắn rồi, bạn có thể triển khai toán tử == và toán tử! = Của riêng mình để lấy cấu trúc nullable. Nếu chúng tồn tại thì trình biên dịch sẽ sử dụng chúng thay vì tự động tạo chúng cho bạn. (Và tình cờ tôi lấy làm tiếc rằng cảnh báo cho vô nghĩa dỡ bỏ hành vào toán hạng không nullable không tạo ra một cảnh báo, đó là một lỗi trong trình biên dịch mà chúng tôi đã không nhận xung quanh để sửa chữa.)
Eric Lippert

2
Chúng tôi muốn cảnh báo của chúng tôi! Chúng ta xứng đáng.
Jeppe Stig Nielsen

3
@JamesDunne: Còn việc xác định a static bool operator == (SomeID a, String b)và gắn thẻ nó với Obsolete? Nếu toán hạng thứ hai là một chữ không có kiểu null, thì đó sẽ là một kết hợp tốt hơn bất kỳ dạng nào yêu cầu sử dụng các toán tử nâng lên, nhưng nếu nó là một toán tử SomeID?bằng nhau null, thì toán tử nâng sẽ thắng.
supercat

17

Nó không phải là một lỗi, vì có một int?chuyển đổi ( ); nó tạo ra một cảnh báo trong ví dụ đã cho:

Kết quả của biểu thức luôn là 'false' vì giá trị của kiểu 'int' không bao giờ bằng 'null' của kiểu 'int?'

Nếu bạn kiểm tra IL, bạn sẽ thấy rằng nó loại bỏ hoàn toàn nhánh không thể truy cập - nó không tồn tại trong bản phát hành.

Tuy nhiên, lưu ý rằng nó không tạo cảnh báo này cho các cấu trúc tùy chỉnh có toán tử bình đẳng. Nó từng có trong 2.0, nhưng không có trong trình biên dịch 3.0. Mã vẫn bị xóa (vì vậy nó biết rằng mã không thể truy cập được), nhưng không có cảnh báo nào được tạo:

using System;

struct MyValue
{
    private readonly int value;
    public MyValue(int value) { this.value = value; }
    public static bool operator ==(MyValue x, MyValue y) {
        return x.value == y.value;
    }
    public static bool operator !=(MyValue x, MyValue y) {
        return x.value != y.value;
    }
}
class Program
{
    static void Main()
    {
        int i = 1;
        MyValue v = new MyValue(1);
        if (i == null) { Console.WriteLine("a"); } // warning
        if (v == null) { Console.WriteLine("a"); } // no warning
    }
}

Với IL (cho Main) - lưu ý mọi thứ ngoại trừ MyValue(1)(có thể có tác dụng phụ) đã bị loại bỏ:

.method private hidebysig static void Main() cil managed
{
    .entrypoint
    .maxstack 2
    .locals init (
        [0] int32 i,
        [1] valuetype MyValue v)
    L_0000: ldc.i4.1 
    L_0001: stloc.0 
    L_0002: ldloca.s v
    L_0004: ldc.i4.1 
    L_0005: call instance void MyValue::.ctor(int32)
    L_000a: ret 
}

về cơ bản đây là:

private static void Main()
{
    MyValue v = new MyValue(1);
}

1
Ai đó đã báo cáo nội bộ điều này với tôi gần đây. Tôi không biết tại sao chúng tôi ngừng sản xuất cảnh báo đó. Chúng tôi đã nhập nó như một lỗi.
Eric Lippert

1
Đây là kết quả của bạn: connect.microsoft.com/VisualStudio/feedback/…
Marc Gravell

5

Thực tế là so sánh không bao giờ có thể đúng không có nghĩa là nó bất hợp pháp. Tuy nhiên, không, một loại giá trị có thể là null.


1
Nhưng một loại giá trị có thể là bình đẳng để null. Hãy xem xét int?, cái nào là đường cú pháp Nullable<Int32>, cái nào là một kiểu giá trị. Một biến kiểu int?chắc chắn có thể bằng null.
Greg

1
@Greg: Có, nó có thể bằng null, giả sử rằng "bằng" mà bạn đang đề cập đến là kết quả của ==toán tử. Điều quan trọng cần lưu ý là phiên bản này không thực sự là rỗng.
Adam Robinson


1

Một kiểu giá trị không thể là null, mặc dù nó có thể bằng null(xem xét Nullable<>). Trong trường hợp của bạn, intbiến và nullđược truyền ngầm đến Nullable<Int32>và so sánh.


0

Tôi nghi ngờ rằng thử nghiệm cụ thể của bạn chỉ đang được trình biên dịch tối ưu hóa khi nó tạo IL vì thử nghiệm sẽ không bao giờ sai.

Lưu ý bên: Có thể có Int32 có thể sử dụng Int32 không? thay vào đó x.


0

Tôi đoán điều này là do "==" là một đường cú pháp thực sự đại diện cho lời gọi System.Object.Equalsphương thức chấp nhận System.Objecttham số. Đặc tả Null by ECMA là một kiểu đặc biệt, tất nhiên có nguồn gốc từ System.Object.

Đó là lý do tại sao chỉ có một cảnh báo.


Điều này không chính xác, vì hai lý do. Đầu tiên, == không có cùng ngữ nghĩa với Object.Equals khi một trong các đối số của nó là kiểu tham chiếu. Thứ hai, null không phải là một kiểu. Xem phần 7.9.6 của đặc tả nếu bạn muốn hiểu cách hoạt động của toán tử bình đẳng tham chiếu.
Eric Lippert

"Chữ null (§9.4.4.6) ước tính giá trị null, được sử dụng để biểu thị một tham chiếu không trỏ đến bất kỳ đối tượng hoặc mảng nào hoặc không có giá trị. Kiểu null có một giá trị duy nhất, là null value. Do đó, một biểu thức có kiểu là kiểu null chỉ có thể đánh giá giá trị null. Không có cách nào để viết kiểu null một cách rõ ràng và do đó, không có cách nào để sử dụng nó trong kiểu đã khai báo. " - đây là trích dẫn từ ECMA. Bạn đang nói về cái gì Ngoài ra, bạn sử dụng phiên bản ECMA nào? Tôi không thấy 7.9.6 trong của tôi.
Vitaly

0

[ĐÃ CHỈNH SỬA: đưa ra các cảnh báo về lỗi và đưa ra các toán tử rõ ràng về khả năng vô hiệu thay vì tấn công chuỗi.]

Theo gợi ý thông minh của @ supercat trong một nhận xét ở trên, các toán tử quá tải sau đây cho phép bạn tạo ra lỗi khi so sánh loại giá trị tùy chỉnh của bạn thành null.

Bằng cách triển khai các toán tử so sánh với các phiên bản nullable của kiểu của bạn, việc sử dụng null trong phép so sánh sẽ khớp với phiên bản nullable của toán tử, điều này cho phép bạn tạo ra lỗi thông qua thuộc tính Obsolete.

Cho đến khi Microsoft trả lại cho chúng tôi cảnh báo trình biên dịch của chúng tôi, tôi sẽ xử lý giải pháp này, cảm ơn @supercat!

public struct Foo
{
    private readonly int x;
    public Foo(int x)
    {
        this.x = x;
    }

    public override string ToString()
    {
        return string.Format("Foo {{x={0}}}", x);
    }

    public override int GetHashCode()
    {
        return x.GetHashCode();
    }

    public override bool Equals(Object obj)
    {
        return x.Equals(obj);
    }

    public static bool operator ==(Foo a, Foo b)
    {
        return a.x == b.x;
    }

    public static bool operator !=(Foo a, Foo b)
    {
        return a.x != b.x;
    }

    [Obsolete("The result of the expression is always 'false' since a value of type 'Foo' is never equal to 'null'", true)]
    public static bool operator ==(Foo a, Foo? b)
    {
        return false;
    }
    [Obsolete("The result of the expression is always 'true' since a value of type 'Foo' is never equal to 'null'", true)]
    public static bool operator !=(Foo a, Foo? b)
    {
        return true;
    }
    [Obsolete("The result of the expression is always 'false' since a value of type 'Foo' is never equal to 'null'", true)]
    public static bool operator ==(Foo? a, Foo b)
    {
        return false;
    }
    [Obsolete("The result of the expression is always 'true' since a value of type 'Foo' is never equal to 'null'", true)]
    public static bool operator !=(Foo? a, Foo b)
    {
        return true;
    }
}

Trừ khi tôi thiếu một cái gì đó, cách tiếp cận của bạn sẽ khiến trình biên dịch kêu lên Foo a; Foo? b; ... if (a == b)..., mặc dù so sánh như vậy là hoàn toàn hợp pháp. Lý do tôi đề xuất "hack chuỗi" là nó sẽ cho phép so sánh ở trên nhưng squawk tại if (a == null). Thay vì sử dụng string, người ta có thể thay thế bất kỳ kiểu tham chiếu nào khác ngoài Objecthoặc ValueType; nếu muốn, người ta có thể định nghĩa một lớp giả với một phương thức khởi tạo riêng không bao giờ có thể được gọi và cho phép nóReferenceThatCanOnlyBeNull .
supercat

Bạn hoàn toàn chính xác. Tôi nên làm rõ rằng đề xuất của tôi phá vỡ việc sử dụng nullable ... mà trong codebase mà tôi đang làm việc dù sao cũng bị coi là tội lỗi (quyền anh không mong muốn, v.v.). ;)
yoyo

0

Tôi nghĩ câu trả lời tốt nhất cho việc tại sao trình biên dịch chấp nhận điều này là dành cho các lớp chung chung. Hãy xem xét lớp sau ...

public class NullTester<T>
{
    public bool IsNull(T value)
    {
        return (value == null);
    }
}

Nếu trình biên dịch không chấp nhận các so sánh đối với nullcác kiểu giá trị, thì về cơ bản nó sẽ phá vỡ lớp này, có một ràng buộc ngầm gắn với tham số kiểu của nó (nghĩa là nó sẽ chỉ hoạt động với các kiểu không dựa trên giá trị).


0

Trình biên dịch sẽ cho phép bạn so sánh bất kỳ cấu trúc nào triển khai == thành null. Nó thậm chí còn cho phép bạn so sánh một int với null (mặc dù vậy bạn sẽ nhận được một cảnh báo).

Nhưng nếu bạn tháo rời mã, bạn sẽ thấy rằng sự so sánh đang được giải quyết khi mã được biên dịch. Vì vậy, ví dụ, mã này (nơi Footriển khai cấu trúc ==):

public static void Main()
{
    Console.WriteLine(new Foo() == new Foo());
    Console.WriteLine(new Foo() == null);
    Console.WriteLine(5 == null);
    Console.WriteLine(new Foo() != null);
}

Tạo IL này:

.method public hidebysig static void  Main() cil managed
{
  .entrypoint
  // Code size       45 (0x2d)
  .maxstack  2
  .locals init ([0] valuetype test3.Program/Foo V_0)
  IL_0000:  nop
  IL_0001:  ldloca.s   V_0
  IL_0003:  initobj    test3.Program/Foo
  IL_0009:  ldloc.0
  IL_000a:  ldloca.s   V_0
  IL_000c:  initobj    test3.Program/Foo
  IL_0012:  ldloc.0
  IL_0013:  call       bool test3.Program/Foo::op_Equality(valuetype test3.Program/Foo,
                                                           valuetype test3.Program/Foo)
  IL_0018:  call       void [mscorlib]System.Console::WriteLine(bool)
  IL_001d:  nop
  IL_001e:  ldc.i4.0
  IL_001f:  call       void [mscorlib]System.Console::WriteLine(bool)
  IL_0024:  nop
  IL_0025:  ldc.i4.1
  IL_0026:  call       void [mscorlib]System.Console::WriteLine(bool)
  IL_002b:  nop
  IL_002c:  ret
} // end of method Program::Main

Bạn có thể thấy:

Console.WriteLine(new Foo() == new Foo());

Được dịch thành:

IL_0013:  call       bool test3.Program/Foo::op_Equality(valuetype test3.Program/Foo,
                                                               valuetype test3.Program/Foo)

Trong khi:

Console.WriteLine(new Foo() == null);

Được dịch thành sai:

IL_001e:  ldc.i4.0
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.