Tại sao chúng ta có thể nhận được cảnh báo tham chiếu null, khi tham chiếu null dường như không thể?


9

Đọc câu hỏi này trên HNQ, tôi tiếp tục đọc về các loại tham chiếu không thể đọc được trong C # 8 và thực hiện một số thử nghiệm.

Tôi rất ý thức rằng 9 lần trong số 10, hoặc thậm chí thường xuyên hơn, khi ai đó nói rằng "Tôi đã tìm thấy một lỗi trình biên dịch!" Đây thực sự là do thiết kế, và sự hiểu lầm của chính họ. Và kể từ khi tôi bắt đầu xem xét tính năng này chỉ ngày hôm nay, rõ ràng tôi không hiểu rõ lắm về nó. Với cách này, hãy xem mã này:

#nullable enable
class Program
{
    static void Main()
    {
        var s = "";
        var b = s == null; // If you comment this line out, the warning on the line below disappears
        var i = s.Length; // warning CS8602: Dereference of a possibly null reference
    }
}

Sau khi đọc tài liệu tôi liên kết ở trên, tôi mong muốn s == nulldòng này đưa ra cảnh báo cho tôi sau khi tất cả srõ ràng là không thể rỗng, vì vậy việc so sánh nó với nullnó không có ý nghĩa gì.

Thay vào đó, tôi nhận được một cảnh báo trên dòng tiếp theo và cảnh báo nói rằng đó scó thể là một tài liệu tham khảo vô giá trị, mặc dù, đối với một con người, rõ ràng là không phải vậy.

Hơn nữa, cảnh báo không được hiển thị nếu chúng ta không so sánh svới null.

Tôi đã thực hiện một số Googling và tôi gặp phải sự cố GitHub , hóa ra là về một thứ hoàn toàn khác, nhưng trong quá trình tôi đã có một cuộc trò chuyện với một cộng tác viên để hiểu rõ hơn về hành vi này (ví dụ: "Kiểm tra Null thường là một cách hữu ích nói với trình biên dịch để thiết lập lại suy luận trước đó về tính vô hiệu của một biến. " ). Điều này vẫn còn cho tôi với câu hỏi chính chưa được trả lời, tuy nhiên.

Thay vì tạo ra một vấn đề GitHub mới và có khả năng chiếm thời gian của những người đóng góp dự án cực kỳ bận rộn, tôi sẽ đưa vấn đề này ra cộng đồng.

Bạn có thể vui lòng giải thích cho tôi những gì đang xảy ra và tại sao? Cụ thể, tại sao không có cảnh báo nào được tạo trên s == nullđường dây và tại sao chúng ta có CS8602khi nó dường như không thể nulltham chiếu ở đây? Nếu suy luận vô hiệu không phải là bằng chứng đạn, như luồng GitHub được liên kết gợi ý, làm thế nào nó có thể sai? Điều gì sẽ là một số ví dụ về điều đó?


Có vẻ như chính trình biên dịch đặt hành vi mà tại thời điểm này, biến "s" có thể là null. Dù sao, nếu tôi đã từng sử dụng các chuỗi hoặc các đối tượng thì phải luôn có một kiểm tra trước khi bạn gọi một hàm. "s? .Lipse" nên thực hiện thủ thuật và cảnh báo sẽ biến mất.
chg

1
@chg, không cần thiết ?skhông thể null. Nó không trở nên vô giá trị, đơn giản vì chúng tôi đủ ngớ ngẩn để so sánh nó với null.
Andrew Savinykh

Tôi đã theo dõi một câu hỏi trước đó (xin lỗi, không thể tìm thấy nó) nơi nó được đặt ra rằng nếu bạn thêm một kiểm tra rằng giá trị là null, thì trình biên dịch sẽ coi đó là 'gợi ý' rằng giá trị có thể là null, ngay cả khi điều đó không phải là trường hợp.
stuartd

@stuartd, vâng, đó là những gì có vẻ như nó là. Vì vậy, bây giờ câu hỏi là: tại sao điều này hữu ích?
Andrew Savinykh

1
@chg, tốt, đó là những gì tôi nói trong phần câu hỏi, phải không?
Andrew Savinykh

Câu trả lời:


7

Đây thực sự là một bản sao của câu trả lời mà @stuartd đã liên kết, vì vậy tôi sẽ không đi sâu vào chi tiết siêu sâu ở đây. Nhưng gốc rễ của vấn đề là đây không phải là lỗi ngôn ngữ hay lỗi trình biên dịch, mà là hành vi dự định chính xác như đã thực hiện. Chúng tôi theo dõi trạng thái null của một biến. Khi ban đầu bạn khai báo biến, trạng thái đó là NotNull vì bạn khởi tạo nó một cách rõ ràng với giá trị không phải là null. Nhưng chúng tôi không theo dõi nơi mà NotNull đến từ đâu. Điều này, ví dụ, là mã tương đương hiệu quả:

#nullable enable
class Program
{
    static void Main()
    {
        M("");
    }
    static void M(string s)
    {
        var b = s == null;
        var i = s.Length; // warning CS8602: Dereference of a possibly null reference
    }
}

Trong cả hai trường hợp, bạn rõ ràng kiểm tra scho null. Chúng tôi lấy điều này làm đầu vào cho phân tích dòng chảy, giống như Mads đã trả lời trong câu hỏi này: https://stackoverflow.com/a/59328672/2672518 . Trong câu trả lời đó, kết quả là bạn nhận được một cảnh báo về lợi nhuận. Trong trường hợp này, câu trả lời là bạn nhận được một cảnh báo rằng bạn đã bỏ qua tham chiếu có thể là null.

Nó không trở nên vô giá trị, đơn giản vì chúng tôi đủ ngớ ngẩn để so sánh nó với null.

Đúng, nó thực sự làm. Để trình biên dịch. Là con người, chúng ta có thể nhìn vào mã này và rõ ràng hiểu rằng nó không thể ném ngoại lệ tham chiếu null. Nhưng cách phân tích luồng nullable được thực hiện trong trình biên dịch, nó không thể. Chúng tôi đã thảo luận về một số cải tiến cho phân tích này, trong đó chúng tôi thêm các trạng thái bổ sung dựa trên giá trị đến từ đâu, nhưng chúng tôi đã quyết định rằng điều này đã tạo ra sự phức tạp lớn cho việc triển khai không phải là một lợi ích lớn, bởi vì chỉ có những nơi duy nhất điều này sẽ hữu ích cho các trường hợp như thế này, trong đó người dùng khởi tạo một biến với một newhoặc một giá trị không đổi và sau đó kiểm tra nó cho nulldù sao đi nữa.


Cảm ơn bạn. Điều này chủ yếu giải quyết sự tương đồng với câu hỏi khác, tôi muốn giải quyết sự khác biệt nhiều hơn. Ví dụ, tại sao s == nullkhông tạo ra một cảnh báo?
Andrew Savinykh

Ngoài ra tôi chỉ nhận ra rằng với #nullable enable; string s = "";s = null;biên dịch và hoạt động (nó vẫn tạo ra một cảnh báo) lợi ích của việc triển khai cho phép gán null cho "tham chiếu không null" trong ngữ cảnh chú thích null được kích hoạt là gì?
Andrew Savinykh

Câu trả lời của Mad tập trung vào thực tế là "[trình biên dịch] không theo dõi mối quan hệ giữa trạng thái của các biến riêng biệt", chúng tôi không có các biến riêng biệt trong ví dụ này, vì vậy tôi gặp khó khăn khi áp dụng phần còn lại của câu trả lời của Mad cho trường hợp này.
Andrew Savinykh

Không cần phải nói, nhưng hãy nhớ, tôi ở đây không phải để phê bình, mà là để học hỏi. Tôi đã sử dụng C # từ khi nó ra mắt lần đầu tiên vào năm 2001. Mặc dù tôi không phải là người mới đối với ngôn ngữ, cách mà trình biên dịch ứng xử là điều đáng ngạc nhiên đối với tôi. Mục tiêu của câu hỏi này là dựa vào lý do tại sao hành vi này hữu ích cho con người.
Andrew Savinykh

Có những lý do hợp lệ để kiểm tra s == null. Có lẽ, ví dụ, bạn đang ở trong một phương thức công khai và bạn muốn thực hiện xác thực tham số. Hoặc, có lẽ bạn đang sử dụng một thư viện chú thích không chính xác và cho đến khi họ khắc phục lỗi đó, bạn phải xử lý null ở nơi không được khai báo. Trong một trong những trường hợp đó, nếu chúng tôi cảnh báo, đó sẽ là một trải nghiệm tồi tệ. Đối với việc cho phép gán: chú thích biến cục bộ chỉ để đọc. Chúng không ảnh hưởng đến thời gian chạy. Trên thực tế, chúng tôi đặt tất cả các cảnh báo đó vào một mã lỗi duy nhất để bạn có thể tắt chúng nếu bạn muốn giảm tốc độ mã.
333fred

0

Nếu suy luận vô hiệu không phải là chống đạn, [..] làm sao nó có thể sai?

Tôi vui vẻ chấp nhận các tài liệu tham khảo vô giá trị của C # 8 ngay khi chúng có sẵn. Vì tôi đã quen sử dụng ký hiệu [NotNull] (v.v.) của ReSharper, tôi đã nhận thấy một số khác biệt giữa hai loại này.

Trình biên dịch C # có thể bị đánh lừa, nhưng nó có xu hướng bị lỗi ở phía thận trọng (thông thường, không phải lúc nào cũng vậy).

Để tham khảo cho khách truy cập trong tương lai, đây là những tình huống tôi thấy trình biên dịch đang khá bối rối (tôi giả sử tất cả các trường hợp này là do thiết kế):

  • Null tha thứ null . Thường được sử dụng để tránh cảnh báo về quy định, nhưng giữ cho đối tượng không có giá trị. Có vẻ như muốn giữ chân bạn trong hai đôi giày.
    string s = null!; //No warning

  • Phân tích bề mặt . Đối lập với ReSharper (sử dụng chú thích mã ), trình biên dịch C # vẫn không hỗ trợ đầy đủ các thuộc tính để xử lý các tham chiếu nullable.
    void DoSomethingWith(string? s)
    {    
        ThrowIfNull(s);
        var split = s.Split(' '); //Dereference warning
    }

Mặc dù vậy, nó cho phép sử dụng một số cấu trúc để kiểm tra tính vô hiệu cũng loại bỏ cảnh báo:

    public static void DoSomethingWith(string? s)
    {
        Debug.Assert(s != null, nameof(s) + " != null");
        var split = s.Split(' ');  //No warning
    }

hoặc thuộc tính (vẫn khá tuyệt) (tìm tất cả ở đây ):

    public static bool IsNullOrEmpty([NotNullWhen(false)] string? value)
    {
        ...
    }

  • Phân tích mã dễ bị ảnh hưởng . Đây là những gì bạn đã đưa ra ánh sáng. Trình biên dịch phải đưa ra các giả định để hoạt động và đôi khi chúng có vẻ phản trực giác (ít nhất là đối với con người).
    void DoSomethingWith(string s)
    {    
        var b = s == null;
        var i = s.Length; // Dereference warning
    }

  • Rắc rối với thuốc generic . Đã hỏi ở đây và giải thích rất tốt ở đây (cùng bài viết như trước, đoạn "Vấn đề với T?"). Generics là phức tạp vì họ phải làm cho cả tài liệu tham khảo và giá trị hạnh phúc. Sự khác biệt chính là trong khi string?chỉ là một chuỗi, int?trở thành một Nullable<int>và buộc trình biên dịch xử lý chúng theo những cách khác nhau đáng kể. Cũng ở đây, trình biên dịch đang chọn đường dẫn an toàn, buộc bạn phải chỉ định những gì bạn đang mong đợi:
    public interface IResult<out T> : IResult
    {
        T? Data { get; } //Warning/Error: A nullable type parameter must be known to be a value type or non-nullable reference type.
    }

Đã giải quyết các ràng buộc:

    public interface IResult<out T> : IResult where T : class { T? Data { get; }}
    public interface IResult<T> : IResult where T : struct { T? Data { get; }}

Nhưng nếu chúng ta không sử dụng các ràng buộc và xóa '?' từ Dữ liệu, chúng tôi vẫn có thể đặt các giá trị null vào đó bằng từ khóa 'mặc định':

    [Pure]
    public static Result<T> Failure(string description, T data = default)
        => new Result<T>(ResultOutcome.Failure, data, description); 
        // data here is definitely null. No warning though.

Cái cuối cùng có vẻ khó hơn với tôi, vì nó cho phép viết mã không an toàn.

Hy vọng điều này sẽ giúp được ai đó.


Tôi khuyên bạn nên cung cấp cho các tài liệu về các thuộc tính không thể đọc được: docs.microsoft.com/en-us/dotnet/csharp/nullable-attribut . Họ sẽ giải quyết một số vấn đề của bạn, đặc biệt với phần phân tích bề mặt và thuốc generic.
333fred

Cảm ơn liên kết @ 333fred. Mặc dù có những thuộc tính bạn có thể chơi với, một thuộc tính giải quyết vấn đề tôi đã đăng (một cái gì đó cho tôi biết rằng ThrowIfNull(s);nó không đảm bảo với tôi rằng skhông phải là null), không tồn tại. Ngoài ra bài viết giải thích cách xử lý các tổng quát không null, trong khi tôi đang trình bày cách bạn có thể "đánh lừa" trình biên dịch, có giá trị null nhưng không có cảnh báo nào về nó.
Alvin Sartor

Trên thực tế, thuộc tính không tồn tại. Tôi đã gửi một lỗi trên các tài liệu để thêm nó. Bạn đang tìm kiếm DoesNotReturnIf(bool).
333fred

@ 333fred thực sự tôi đang tìm kiếm một cái gì đó giống như DoesNotReturnIfNull(nullable).
Alvin Sartor
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.