Tại sao tôi không nhận được cảnh báo về khả năng hủy đăng ký có thể có trong C # 8 với một thành viên của lớp?


8

Trong dự án C # 8 với các loại tham chiếu nullable được bật, tôi có đoạn mã sau mà tôi nghĩ sẽ đưa ra cảnh báo về khả năng hủy bỏ null có thể xảy ra, nhưng không:

public class ExampleClassMember
{
    public int Value { get; }
}

public struct ExampleStruct
{
    public ExampleClassMember Member { get; }
}

public class Program
{
    public static void Main(string[] args)
    {
        var instance = new ExampleStruct();
        Console.WriteLine(instance.Member.Value);  // expected warning here about possible null dereference
    }
}

Khi instanceđược khởi tạo với hàm tạo mặc định, instance.Memberđược đặt thành giá trị mặc định ExampleClassMembernull. Vì vậy, instance.Member.Valuesẽ ném một NullReferenceExceptionlúc chạy. Vì tôi hiểu phát hiện vô hiệu của C # 8, tôi sẽ nhận được cảnh báo về trình biên dịch về khả năng này, nhưng tôi thì không; tại sao vậy?


Bạn đã nộp điều này như là một vấn đề trên repo Roslyn GitHub?
Đại

@Dai tôi chưa (chưa); Nếu đó là một lỗi hợp pháp và không phải là thứ tôi thiếu, tôi sẽ làm.
DylanSp

FWIW, mã này không biên dịch trong C # 7.0 - Tôi gặp lỗi về hai loại thiếu hàm tạo để đặt giá trị tự động '. Mặc dù vậy, nó biên dịch với trình biên dịch Roslyn 3.0 và .NET Core 3.0, và thực sự nó chạy với NRE trong cả hai trường hợp sau. Tôi đang sử dụng IDE dựa trên web mà không có khả năng thiết lập các tùy chọn trình biên dịch.
Đại

Trình biên dịch C # 8.0 cảnh báo tôi khi tôi thay đổi ExampleStructtừ structsang class.
Đại

1
@tymtam đó là phiên bản xem trước. Trong phiên bản phát hành, nóNullable
Panagiotis Kanavos

Câu trả lời:


13

Lưu ý rằng không có lý do gì để có cảnh báo về cuộc gọi đến Console.WriteLine(). Thuộc tính loại tham chiếu không phải là loại nullable và do đó, trình biên dịch không cần phải cảnh báo rằng nó có thể là null.

Bạn có thể lập luận rằng trình biên dịch nên cảnh báo về tham chiếu trong structchính nó. Điều đó có vẻ hợp lý với tôi. Nhưng, nó không. Đây có vẻ là một lỗ hổng, do khởi tạo mặc định cho các loại giá trị, nghĩa là phải luôn có một hàm tạo mặc định (không tham số), luôn luôn chỉ ra tất cả các trường (null cho trường loại tham chiếu, số 0 cho kiểu số, v.v. ).

Tôi gọi nó là một lỗ hổng, bởi vì trong lý thuyết, các giá trị tham chiếu không thể rỗng nên trên thực tế luôn luôn là không rỗng! Tât nhiên. :)

Lỗ hổng này dường như được giải quyết trong bài viết trên blog này: Giới thiệu các loại tham chiếu không có giá trị trong C #

Tránh null cho đến nay, các cảnh báo là về việc bảo vệ null trong các tài liệu tham khảo nullable khỏi bị hủy đăng ký. Mặt khác của đồng tiền là để tránh có tất cả các giá trị rỗng trong các tài liệu tham khảo không thể hoàn thành.

Có một vài cách Null giá trị có thể đi vào cuộc sống, và hầu hết trong số đó là cảnh báo trị giá khoảng, trong khi một vài trong số họ sẽ gây ra một “biển cảnh báo” có nghĩa là tốt hơn để tránh:
...

  • Sử dụng hàm tạo mặc định của một cấu trúc có một trường loại tham chiếu không thể hoàn thành. Cái này là lén lút, vì hàm tạo mặc định (không có cấu trúc) thậm chí có thể được sử dụng ngầm ở nhiều nơi. Có lẽ tốt hơn là không cảnh báo [nhấn mạnh của tôi - PD] , nếu không, nhiều kiểu cấu trúc hiện có sẽ trở nên vô dụng.

Nói cách khác, vâng, đây là một lỗ hổng, nhưng không, nó không phải là một lỗi. Các nhà thiết kế ngôn ngữ nhận thức được điều đó, nhưng đã chọn loại bỏ kịch bản này ra khỏi các cảnh báo, bởi vì nếu không thì sẽ không thực tế khi đưa ra cách structkhởi tạo.

Lưu ý rằng điều này cũng phù hợp với triết lý rộng hơn đằng sau tính năng này. Từ cùng một bài viết:

Vì vậy, chúng tôi muốn nó phàn nàn về mã hiện tại của bạn. Nhưng không đáng ghét. Đây là cách chúng tôi sẽ cố gắng để tấn công sự cân bằng đó:
...

  1. Không có sự an toàn null được đảm bảo [nhấn mạnh của tôi - PD] , ngay cả khi bạn phản ứng và loại bỏ tất cả các cảnh báo. Có nhiều lỗ hổng trong phân tích bởi sự cần thiết, và cũng có một số do sự lựa chọn.

Đến điểm cuối cùng: Đôi khi, một cảnh báo là điều chính xác về việc làm, nhưng sẽ kích hoạt mọi lúc trên mã hiện có, ngay cả khi nó thực sự được viết theo cách an toàn không có giá trị. Trong những trường hợp như vậy, chúng tôi sẽ sai về phía thuận tiện, không chính xác. Chúng ta không thể mang lại một biển cảnh báo trên YouTube về mã hiện có: quá nhiều người sẽ tắt các cảnh báo và không bao giờ được hưởng lợi từ nó.

Cũng lưu ý rằng vấn đề tương tự này tồn tại với các mảng tham chiếu danh nghĩa không thể rỗng (ví dụ string[]). Khi bạn tạo mảng, tất cả các giá trị tham chiếu là null, nhưng điều này là hợp pháp và sẽ không tạo ra bất kỳ cảnh báo nào.


Rất nhiều để giải thích lý do tại sao mọi thứ là như vậy. Sau đó, câu hỏi trở thành, phải làm gì về nó? Điều đó chủ quan hơn rất nhiều và tôi không nghĩ có câu trả lời đúng hay sai. Mà nói…

Cá nhân tôi sẽ đối xử với structcác loại của tôi trên cơ sở từng trường hợp. Đối với những người mà ý định thực sự là một loại tham chiếu vô giá trị, tôi sẽ áp dụng ?chú thích. Nếu không, tôi sẽ không.

Về mặt kỹ thuật, mọi giá trị tham chiếu trong một structnên là "nullable", nghĩa là bao gồm ?chú thích nullable với tên loại. Nhưng cũng như nhiều tính năng tương tự (như async / await trong C # hoặc consttrong C ++), điều này có khía cạnh "lây nhiễm", trong đó bạn sẽ cần ghi đè chú thích đó sau (với !chú thích) hoặc bao gồm kiểm tra null rõ ràng hoặc chỉ bao giờ gán giá trị đó cho một biến loại tham chiếu nullable khác.

Đối với tôi, điều này đánh bại rất nhiều mục đích cho phép các loại tham chiếu nullable. Vì các thành viên của các structloại như vậy sẽ yêu cầu xử lý trường hợp đặc biệt vào một lúc nào đó, và vì cách duy nhất để thực sự xử lý an toàn trong khi vẫn có thể sử dụng các loại tham chiếu không null là đặt séc null ở mọi nơi bạn sử dụng struct, tôi cảm thấy rằng đó là một lựa chọn triển khai hợp lý để chấp nhận rằng khi mã khởi tạo struct, đó là trách nhiệm của mã đó để thực hiện chính xác và đảm bảo rằng thành viên loại tham chiếu không null thực tế được khởi tạo thành giá trị không null.

Điều này có thể được hỗ trợ bằng cách cung cấp một phương tiện khởi tạo "chính thức", chẳng hạn như một hàm tạo không mặc định (nghĩa là một hàm có tham số) hoặc phương thức xuất xưởng. Vẫn sẽ có rủi ro khi sử dụng hàm tạo mặc định hoặc hoàn toàn không có hàm tạo (như trong phân bổ mảng), nhưng bằng cách cung cấp một phương tiện thuận tiện để khởi tạo structchính xác, điều này sẽ khuyến khích mã sử dụng nó để tránh các tham chiếu null trong không biến nullable.

Điều đó nói rằng, nếu những gì bạn muốn là 100% an toàn đối với các loại tài liệu tham khảo nullable, sau đó rõ ràng cách tiếp cận chính xác cho rằng mục tiêu cụ thể là phải luôn luôn chú thích mọi thành viên kiểu tham chiếu trong một structvới ?. Điều này có nghĩa là mọi trường và mọi thuộc tính được triển khai tự động, cùng với bất kỳ phương thức hoặc thuộc tính getter nào trả về trực tiếp các giá trị đó hoặc sản phẩm của các giá trị đó. Sau đó, mã tiêu thụ sẽ cần bao gồm kiểm tra null hoặc toán tử tha thứ null tại mọi điểm nơi các giá trị đó được sao chép thành các biến không null.


Phân tích tốt, và cảm ơn vì đã tìm thấy bài đăng trên blog đó - đó là một câu trả lời khá kết luận.
DylanSp

1

Theo câu trả lời xuất sắc của @ peter-duniho, có vẻ như kể từ tháng 10-2019, tốt nhất là đánh dấu tất cả các thành viên không thuộc loại giá trị là một tài liệu tham khảo vô giá trị.

#nullable enable
public class C
{
    public int P1 { get; } 
}

public struct S
{
    public C? Member { get; } // Reluctantly mark as nullable reference because
                              // https://devblogs.microsoft.com/dotnet/nullable-reference-types-in-csharp/
                              // states:
                              // "Using the default constructor of a struct that has a
                              // field of nonnullable reference type. This one is 
                              // sneaky, since the default constructor (which zeroes 
                              // out the struct) can even be implicitly used in many
                              // places. Probably better not to warn, or else many
                              // existing struct types would be rendered useless."
}

public class Program
{
    public static void Main()
    {
        var instance = new S();
        Console.WriteLine(instance.Member.P1); // Warning
    }
}
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.