Nếu các chức năng phải thực hiện kiểm tra null trước khi thực hiện hành vi dự định là thiết kế xấu này?


67

Vì vậy, tôi không biết đây là thiết kế mã tốt hay xấu nên tôi nghĩ tôi nên hỏi.

Tôi thường tạo các phương thức xử lý dữ liệu liên quan đến các lớp và tôi thường thực hiện nhiều kiểm tra trong các phương thức để đảm bảo rằng tôi không nhận được các tham chiếu null hoặc các lỗi khác trước khi xử lý.

Ví dụ rất cơ bản:

// fields and properties
private Entity _someEntity;
public Entity SomeEntity => _someEntity;

public void AssignEntity(Entity entity){
    _someEntity = entity;
}
public void SetName(string name)
{
    if (_someEntity == null) return; //check to avoid null ref
    _someEntity.Name = name;
    label.SetText(_someEntity.Name);
}

Vì vậy, như bạn có thể thấy tôi đang kiểm tra null mỗi lần. Nhưng phương pháp không nên có kiểm tra này?

Ví dụ, mã bên ngoài nên làm sạch dữ liệu trước khi sử dụng để các phương thức không cần xác thực như dưới đây:

 if(entity != null) // this makes the null checks redundant in the methods
 {
     Manager.AssignEntity(entity);
     Manager.SetName("Test");
 }

Tóm lại, các phương thức nên là "xác thực dữ liệu" và sau đó thực hiện xử lý dữ liệu của chúng hoặc nên được bảo đảm trước khi bạn gọi phương thức và nếu bạn không xác thực trước khi gọi phương thức thì nó sẽ báo lỗi (hoặc bắt lỗi lỗi)?


7
Điều gì xảy ra khi ai đó không thực hiện kiểm tra null và SetName vẫn ném ngoại lệ?
Robert Harvey

5
Điều gì xảy ra nếu tôi thực sự muốn một sốEntity là Null? Bạn quyết định những gì bạn muốn, một sốEntity có thể là Null hoặc không thể, và sau đó bạn viết mã của mình cho phù hợp. Nếu bạn muốn nó không bao giờ là Null, bạn có thể thay thế kiểm tra bằng các xác nhận.
gnasher729

5
Bạn có thể xem Ranh giới của Gary Bernhardt nói về việc phân chia rõ ràng giữa vệ sinh dữ liệu (nghĩa là kiểm tra null, v.v.) trong lớp vỏ bên ngoài và có một hệ thống lõi bên trong mà không phải quan tâm đến nội dung đó - và chỉ cần thực hiện công việc.
mgarciaisaia

1
Với C # 8 bạn sẽ có khả năng không bao giờ cần phải lo lắng về trường hợp ngoại lệ tham chiếu null với các thiết lập bật đúng: blogs.msdn.microsoft.com/dotnet/2017/11/15/...
JᴀʏMᴇᴇ

3
Đây là một trong những lý do tại sao nhóm ngôn ngữ C # muốn giới thiệu các loại tham chiếu không thể bỏ qua (và các loại không thể
null

Câu trả lời:


168

Vấn đề với ví dụ cơ bản của bạn không phải là kiểm tra null, đó là sự thất bại thầm lặng .

Lỗi con trỏ / tham chiếu Null, thường xuyên hơn không, là lỗi lập trình viên. Lỗi lập trình viên thường được xử lý tốt nhất bằng cách thất bại ngay lập tức và lớn tiếng. Bạn có ba cách chung để xử lý vấn đề trong tình huống này:

  1. Đừng bận tâm kiểm tra và chỉ để cho bộ thực thi ném ngoại lệ nullpulum.
  2. Kiểm tra và ném một ngoại lệ với một tin nhắn tốt hơn tin nhắn NPE cơ bản.

Giải pháp thứ ba là công việc nhiều hơn nhưng mạnh mẽ hơn nhiều:

  1. Cấu trúc lớp của bạn sao cho gần như không _someEntitythể ở trạng thái không hợp lệ. Một cách để làm điều đó là loại bỏ AssignEntityvà yêu cầu nó như một tham số để khởi tạo. Các kỹ thuật tiêm phụ thuộc khác cũng có thể hữu ích.

Trong một số trường hợp, nên kiểm tra tất cả các đối số chức năng để xác thực tính hợp lệ trước bất kỳ công việc nào bạn đang làm và trong các trường hợp khác, việc nói với người gọi rằng họ có trách nhiệm đảm bảo đầu vào của họ hợp lệ và không kiểm tra. Phần cuối của phổ bạn sẽ phụ thuộc vào miền vấn đề của bạn. Tùy chọn thứ ba có một lợi ích đáng kể ở một mức độ nào đó, bạn có thể yêu cầu trình biên dịch thực thi rằng người gọi thực hiện mọi thứ đúng cách.

Nếu tùy chọn thứ ba không phải là một lựa chọn, thì theo tôi, nó thực sự không thành vấn đề miễn là nó không âm thầm thất bại. Nếu không kiểm tra null sẽ khiến chương trình phát nổ ngay lập tức, nhưng nếu nó lặng lẽ làm hỏng dữ liệu để gây rắc rối trên đường thì tốt nhất bạn nên kiểm tra và xử lý ngay lúc đó.


10
@aroth: Bất kỳ thiết kế của bất cứ điều gì, bao gồm phần mềm, sẽ đi kèm với các ràng buộc. Chính hành động thiết kế là chọn nơi mà những ràng buộc đó đi, và đó không phải là trách nhiệm của tôi và tôi cũng không nên chấp nhận bất kỳ đầu vào nào và dự kiến ​​sẽ làm điều gì đó hữu ích với nó. Tôi biết liệu nullsai hay không hợp lệ vì trong thư viện giả định của tôi, tôi đã xác định các loại dữ liệu và tôi đã xác định cái gì là hợp lệ và cái gì không. Bạn gọi đó là "giả định bắt buộc" nhưng hãy đoán xem: đó là điều mà mọi thư viện phần mềm tồn tại đều làm. Đôi khi null là một giá trị hợp lệ, nhưng đó không phải là "luôn luôn".
tên gì là

4
"rằng mã của bạn bị hỏng vì nó không xử lý nullđúng cách" - Đây là một sự hiểu lầm về việc xử lý đúng nghĩa là gì. Đối với hầu hết các lập trình viên Java, điều đó có nghĩa là ném một ngoại lệ cho bất kỳ đầu vào không hợp lệ nào . Sử dụng @ParametersAreNonnullByDefault, nó cũng có nghĩa cho mọi null, trừ khi đối số được đánh dấu là @Nullable. Làm bất cứ điều gì khác có nghĩa là kéo dài con đường cho đến khi cuối cùng nó thổi vào nơi khác và do đó cũng kéo dài thời gian lãng phí cho việc gỡ lỗi. Vâng, bằng cách bỏ qua các vấn đề, bạn có thể đạt được rằng nó không bao giờ thổi, nhưng hành vi không chính xác sau đó được đảm bảo.
maaartinus

4
@aroth Chúa ơi, tôi rất ghét làm việc với bất kỳ mã nào bạn viết. Vụ nổ là vô cùng tốt hơn so với làm một cái gì đó vô nghĩa, đó là lựa chọn duy nhất khác cho đầu vào không hợp lệ. Các ngoại lệ buộc tôi, người gọi, quyết định điều gì là đúng trong những trường hợp đó, khi phương thức không thể đoán được tôi dự định gì. Các trường hợp ngoại lệ được kiểm tra và mở rộng không cần thiết Exceptionlà những cuộc tranh luận hoàn toàn không liên quan. (Tôi đồng ý cả hai đều rắc rối.) Đối với ví dụ cụ thể này, nullrõ ràng không có ý nghĩa gì ở đây nếu mục đích của lớp là gọi các phương thức trên đối tượng.
jpmc26

3
"Mã của bạn không nên buộc các giả định ra thế giới của người gọi" - Không thể không buộc các giả định ra cho người gọi. Đó là lý do tại sao chúng ta cần làm cho những giả định này càng rõ ràng càng tốt.
Diogo Kollross

3
@aroth mã của bạn bị hỏng vì nó chuyển mã của tôi thành null khi nó sẽ truyền một cái gì đó có Tên là thuộc tính. Bất kỳ hành vi nào khác ngoài việc phát nổ là một loại phiên bản thế giới ngược của kiểu gõ động IMHO.
mbrig

56

_someEntitycó thể được sửa đổi ở bất kỳ giai đoạn nào, nên sẽ rất hợp lý khi kiểm tra nó mỗi khi SetNameđược gọi. Rốt cuộc, nó có thể đã thay đổi kể từ lần cuối cùng phương thức đó được gọi. Nhưng hãy lưu ý rằng mã trong SetNameluồng không an toàn cho luồng, vì vậy bạn có thể thực hiện kiểm tra đó trong một luồng, đã _someEntityđược đặt nullbởi một luồng khác và sau đó mã sẽ NullReferenceExceptionkhông hoạt động.

Do đó, một cách tiếp cận vấn đề này là phòng thủ nhiều hơn và thực hiện một trong các cách sau:

  1. tạo một bản sao cục bộ _someEntityvà tham chiếu bản sao đó thông qua phương thức,
  2. sử dụng khóa xung quanh _someEntityđể đảm bảo nó không thay đổi khi phương thức thực thi,
  3. sử dụng a try/catchvà thực hiện cùng một hành động đối với bất kỳ NullReferenceExceptionđiều gì xảy ra trong phương thức như bạn thực hiện trong kiểm tra null ban đầu (trong trường hợp này là một nophành động).

Nhưng điều bạn thực sự nên làm là dừng lại và tự hỏi mình câu hỏi: bạn có thực sự cần phải cho phép _someEntityghi đè không? Tại sao không đặt nó một lần, thông qua các nhà xây dựng. Bằng cách đó, bạn chỉ cần bao giờ làm điều đó null kiểm tra một lần:

private readonly Entity _someEntity;

public Constructor(Entity someEntity)
{
    if (someEntity == null) throw new ArgumentNullException(nameof(someEntity));
    _someEntity = someEntity;
}

public void SetName(string name)
{
    _someEntity.Name = name;
    label.SetText(_someEntity.Name);
}

Nếu có thể, đây là cách tiếp cận tôi khuyên bạn nên thực hiện trong các tình huống như vậy. Nếu bạn không thể chú ý đến sự an toàn của luồng khi chọn cách xử lý các null có thể.

Là một nhận xét tiền thưởng, tôi cảm thấy mã của bạn là lạ và có thể được cải thiện. Tất cả:

private Entity _someEntity;
public Entity SomeEntity => _someEntity;

public void AssignEntity(Entity entity){
    _someEntity = entity;
}

có thể được thay thế bằng:

public Entity SomeEntity { get; set; }

Có cùng chức năng, tiết kiệm để có thể đặt trường thông qua trình thiết lập thuộc tính, thay vì một phương thức có tên khác.


5
Tại sao điều này trả lời câu hỏi? Các câu hỏi địa chỉ kiểm tra null và đây là một đánh giá mã khá nhiều.
IEatBagels

17
@TopinFrassi nó giải thích rằng phương pháp kiểm tra null hiện tại không phải là luồng an toàn. Sau đó, nó chạm vào các kỹ thuật lập trình phòng thủ khác để khắc phục điều đó. Sau đó, nó kết thúc với đề nghị sử dụng tính bất biến để tránh sự cần thiết phải lập trình phòng thủ đó ngay từ đầu. Và như một phần thưởng bổ sung, nó cũng cung cấp lời khuyên về cách cấu trúc mã tốt hơn.
David Arno

23

Nhưng phương pháp không nên có kiểm tra này?

Đây là sự lựa chọn của bạn .

Bằng cách tạo ra một publicphương thức, bạn đang cung cấp cho công chúng cơ hội để gọi nó. Điều này luôn đi kèm với một hợp đồng ngầm về cách gọi phương thức đó và những gì mong đợi khi làm như vậy. Hợp đồng này có thể (hoặc có thể không) bao gồm " yeah, uhhm, nếu bạn vượt qua nullnhư một giá trị tham số, nó sẽ phát nổ ngay trên khuôn mặt của bạn. " Các phần khác của hợp đồng đó có thể là " oh, BTW: mỗi khi hai luồng thực thi phương thức này cùng một lúc, một con mèo con chết ".

Loại hợp đồng bạn muốn thiết kế là tùy thuộc vào bạn. Điều quan trọng là

  • tạo một API hữu ích và nhất quán và

  • để tài liệu tốt, đặc biệt là cho các trường hợp cạnh.

Các trường hợp có khả năng làm thế nào phương pháp của bạn đang được gọi là?


Vâng, an toàn luồng là ngoại lệ, không phải là quy tắc, khi có quyền truy cập đồng thời. Do đó, việc sử dụng trạng thái ẩn / toàn cầu được ghi lại, cũng như mọi trường hợp đồng thời đều an toàn.
Ded repeatator

14

Các câu trả lời khác là tốt; Tôi muốn mở rộng chúng bằng cách đưa ra một số nhận xét về công cụ sửa đổi khả năng truy cập. Đầu tiên, phải làm gì nếu nullkhông bao giờ hợp lệ:

  • phương thức công khaiđược bảo vệ là những phương thức mà bạn không kiểm soát người gọi. Họ nên ném khi thông qua null. Bằng cách đó, bạn huấn luyện người gọi của mình để không bao giờ vượt qua bạn, vì họ muốn chương trình của họ không bị sập.

  • nội bộtin phương pháp là những nơi bạn làm kiểm soát người gọi. Họ nên khẳng định khi thông qua null; sau đó họ có thể sẽ gặp sự cố sau đó, nhưng ít nhất bạn đã có được sự khẳng định đầu tiên và một cơ hội để đột nhập vào trình gỡ lỗi. Một lần nữa, bạn muốn huấn luyện người gọi của bạn gọi cho bạn một cách chính xác bằng cách làm cho nó bị tổn thương khi họ không.

Bây giờ, bạn sẽ làm gì nếu nó thể hợp lệ cho một null được truyền vào? Tôi sẽ tránh tình huống này với mẫu đối tượng null . Đó là: tạo một thể hiện đặc biệt của loại và yêu cầu nó được sử dụng bất cứ khi nào cần một đối tượng không hợp lệ . Bây giờ bạn đã trở lại trong thế giới thú vị của việc ném / khẳng định mỗi khi sử dụng null.

Ví dụ: bạn không muốn ở trong tình huống này:

class Person { ... }
...
class ExpenseReport { 
  public void SubmitReport(Person approver) { 
    if (approver == null) ... do something ...

và tại trang web cuộc gọi:

// I guess this works but I don't know what it means
expenses.SubmitReport(null); 

Bởi vì (1) bạn đang đào tạo người gọi của mình rằng null là một giá trị tốt và (2) không rõ ngữ nghĩa của giá trị đó là gì. Thay vào đó, làm điều này:

class ExpenseReport { 
  public static readonly Person ApproverNotRequired = new Person();
  public static readonly Person ApproverUnknown = new Person();
  ...
  public void SubmitReport(Person approver) { 
    if (approver == null) throw ...
    if (approver == ApproverNotRequired) ... do something ...
    if (approver == ApproverUnknown) ... do something ...

Và bây giờ trang web cuộc gọi trông như thế này:

expenses.SubmitReport(ExpenseReport.ApproverNotRequired);

và bây giờ người đọc mã biết ý nghĩa của nó.


Tốt hơn hết, đừng có một chuỗi ifs cho các đối tượng null mà nên sử dụng đa hình để làm cho chúng hoạt động chính xác.
Konrad Rudolph

@KonradRudolph: Thật vậy, đó thường là một kỹ thuật tốt. Tôi thích sử dụng nó cho các cấu trúc dữ liệu, nơi tôi tạo ra một EmptyQueuekiểu ném khi bị mất hiệu lực, v.v.
Eric Lippert

@EricLippert - Hãy tha thứ cho tôi vì tôi vẫn đang học ở đây, nhưng giả sử nếu Personlớp là bất biến, và nó không chứa một constructor không cãi nhau, làm thế nào bạn sẽ xây dựng của bạn ApproverNotRequiredhoặc ApproverUnknowncác trường hợp? Nói cách khác, những giá trị nào bạn sẽ vượt qua các nhà xây dựng?

2
Tôi thích va vào câu trả lời / nhận xét của bạn trên SE, họ luôn thấu hiểu. Ngay khi tôi thấy khẳng định cho các phương pháp nội bộ / riêng tư, tôi đã cuộn xuống và hy vọng đó là câu trả lời của Eric Lippert, đã không thất vọng :)
bob Esponja

4
Các bạn đang suy nghĩ quá mức về ví dụ cụ thể mà tôi đã đưa ra. Nó được dự định để có được ý tưởng về mẫu đối tượng null nhanh chóng. Nó không có ý định trở thành một ví dụ về thiết kế tốt trong hệ thống tự động hóa giấy tờ và tiền lương. Trong một hệ thống thực, làm cho "người phê duyệt" có loại "người" có thể là một ý tưởng tồi vì nó không phù hợp với quy trình kinh doanh; có thể không có người phê duyệt, bạn có thể cần một vài người, v.v. Như tôi thường lưu ý khi trả lời các câu hỏi trong miền đó, điều chúng tôi thực sự muốn là một đối tượng chính sách . Đừng treo lên ví dụ.
Eric Lippert

8

Các câu trả lời khác chỉ ra rằng mã của bạn có thể được dọn sạch để không cần kiểm tra null ở nơi bạn có mã, tuy nhiên, để có câu trả lời chung về việc kiểm tra null hữu ích có thể được xem xét mã mẫu sau:

public static class Foo {
    public static void Frob(Bar a, Bar b) {
        if (a == null) throw new ArgumentNullException(nameof(a));
        if (b == null) throw new ArgumentNullException(nameof(b));

        a.Something = b.Something;
    }
}

Điều này cung cấp cho bạn một lợi thế để bảo trì. Nếu có lỗi xảy ra trong mã của bạn, trong đó một cái gì đó chuyển một đối tượng null sang phương thức, bạn sẽ nhận được thông tin hữu ích từ phương thức như tham số nào là null. Nếu không có kiểm tra, bạn sẽ nhận được một NullReferenceException trên dòng:

a.Something = b.Something;

Và (cho rằng thuộc tính Something là một loại giá trị) hoặc a hoặc b có thể là null và bạn sẽ không có cách nào biết được từ dấu vết ngăn xếp. Với các kiểm tra, bạn sẽ biết chính xác đối tượng nào là null, điều này có thể rất hữu ích khi cố gắng giải nén một khiếm khuyết.

Tôi sẽ lưu ý rằng mẫu trong mã gốc của bạn:

if (x == null) return;

Có thể sử dụng nó trong một số trường hợp nhất định, nó cũng có thể (có thể thường xuyên hơn) cung cấp cho bạn một cách rất tốt để che giấu các vấn đề tiềm ẩn trong cơ sở mã của bạn.


4

Không không phải tất cả, nhưng nó phụ thuộc.

Bạn chỉ thực hiện kiểm tra tham chiếu null nếu bạn muốn phản ứng với nó.

Nếu bạn thực hiện kiểm tra tham chiếu nullném ngoại lệ được kiểm tra , bạn buộc người dùng phương thức phản ứng với nó và cho phép anh ta khôi phục. Chương trình vẫn hoạt động như mong đợi.

Nếu bạn không thực hiện kiểm tra tham chiếu null (hoặc ném ngoại lệ không được kiểm tra), nó có thể ném một kiểm tra không được kiểm tra NullReferenceException, thường không được xử lý bởi người dùng phương thức và thậm chí có thể tắt ứng dụng. Điều này có nghĩa là chức năng rõ ràng là hoàn toàn bị hiểu lầm và do đó ứng dụng bị lỗi.


4

Một cái gì đó mở rộng cho câu trả lời của @ null, nhưng theo kinh nghiệm của tôi, hãy kiểm tra null bất kể là gì.

Lập trình phòng thủ tốt là thái độ mà bạn mã hóa thói quen hiện tại một cách cô lập, với giả định rằng toàn bộ phần còn lại của chương trình đang cố gắng phá vỡ nó. Khi bạn đang viết mã nhiệm vụ quan trọng trong đó thất bại không phải là một lựa chọn, đây là cách duy nhất để đi.

Bây giờ, làm thế nào bạn thực sự đối phó với một nullgiá trị, điều đó phụ thuộc rất nhiều vào trường hợp sử dụng của bạn.

Nếu nulllà một giá trị có thể chấp nhận, ví dụ: bất kỳ tham số thứ hai đến thứ năm nào đối với thường trình MSVC _splitpath (), thì âm thầm xử lý nó là hành vi đúng.

Nếu nullkhông nên xảy ra khi gọi thủ tục đang được đề cập, tức là trong trường hợp của OP, hợp đồng API yêu cầu AssignEntity()phải được gọi với một hợp lệ Entityrồi ném ngoại lệ hoặc nếu không thì sẽ đảm bảo bạn gây ra nhiều tiếng ồn trong quá trình.

Không bao giờ lo lắng về chi phí của kiểm tra null, chúng sẽ rất rẻ nếu chúng xảy ra và với các trình biên dịch hiện đại, phân tích mã tĩnh có thể và sẽ loại bỏ chúng hoàn toàn nếu trình biên dịch xác định rằng điều đó sẽ không xảy ra.


Hoặc tránh hoàn toàn (48 phút 29 giây: "Không bao giờ sử dụng hoặc cho phép bất cứ nơi nào gần chương trình của bạn một con trỏ null" ).
Peter Mortensen
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.