Là một trường Boolean mới tốt hơn một tham chiếu null khi một giá trị có thể vắng mặt một cách có ý nghĩa?


39

Ví dụ: giả sử tôi có một lớp Member, trong đó có lastChangePasswordTime:

class Member{
  .
  .
  .
  constructor(){
    this.lastChangePasswordTime=null,
  }
}

mà LastChangePasswordTime có thể vắng mặt có ý nghĩa, bởi vì một số thành viên có thể không bao giờ thay đổi mật khẩu của họ.

Nhưng theo Nếu null là xấu, thì nên dùng gì khi một giá trị có thể vắng mặt một cách có ý nghĩa? https://softwareengineering.stackexchange.com/a/12836/248528 , tôi không nên sử dụng null để thể hiện một giá trị vắng mặt có ý nghĩa. Vì vậy, tôi cố gắng thêm một cờ Boolean:

class Member{
  .
  .
  .
  constructor(){
    this.isPasswordChanged=false,
    this.lastChangePasswordTime=null,
  }
}

Nhưng tôi nghĩ nó khá lỗi thời vì:

  1. Khi isPasswordChanged là false, lastChangePasswordTime phải là null và kiểm tra lastChangePasswordTime == null gần như giống hệt với việc kiểm tra isPasswordChanged là sai, vì vậy tôi thích kiểm tra trực tiếp LastChangePasswordTime == null

  2. Khi thay đổi logic ở đây, tôi có thể quên cập nhật cả hai trường.

Lưu ý: khi người dùng thay đổi mật khẩu, tôi sẽ ghi lại thời gian như thế này:

this.lastChangePasswordTime=Date.now();

Trường Boolean bổ sung có tốt hơn tham chiếu null ở đây không?


51
Câu hỏi này khá cụ thể về ngôn ngữ, bởi vì những gì tạo nên một giải pháp tốt phần lớn phụ thuộc vào những gì ngôn ngữ lập trình của bạn cung cấp. Ví dụ: trong C ++ 17 hoặc Scala, bạn sẽ sử dụng std::optionalhoặc Option. Trong các ngôn ngữ khác, bạn có thể phải tự xây dựng một cơ chế phù hợp hoặc bạn thực sự có thể dùng đến nullhoặc một cái gì đó tương tự vì nó thành ngữ hơn.
Christian Hackl

16
Có một lý do nào khiến bạn không muốn LastChangePasswordTime được đặt thành thời gian tạo mật khẩu (rốt cuộc, đó là một bản mod)?
Kristian H

@ChristianHackl hmm, đồng ý rằng có các giải pháp "hoàn hảo" khác nhau, nhưng tôi không thấy bất kỳ ngôn ngữ (chính) nào mặc dù sử dụng một boolean riêng biệt sẽ là một ý tưởng tốt hơn nói chung so với thực hiện kiểm tra null / nil. Dù vậy, không hoàn toàn chắc chắn về C / C ++ vì tôi đã không hoạt động ở đó khá lâu.
Frank Hopkins

@FrankHopkins: Một ví dụ sẽ là các ngôn ngữ mà các biến có thể không được khởi tạo, ví dụ C hoặc C ++. lastChangePasswordTimecó thể là một con trỏ chưa được khởi tạo ở đó và so sánh nó với bất cứ điều gì sẽ là hành vi không xác định. Không phải là một lý do thực sự thuyết phục để không khởi tạo con trỏ thành NULL/ nullptrthay vào đó, đặc biệt là không có trong C ++ hiện đại (nơi bạn hoàn toàn không sử dụng một con trỏ), nhưng ai biết được? Một ví dụ khác có thể là các ngôn ngữ không có con trỏ hoặc có thể hỗ trợ xấu cho con trỏ. (FORTRAN 77 xuất hiện trong tâm trí ...)
Christian Hackl

Tôi sẽ sử dụng một enum với 3 trường hợp cho việc này :)
J. Doe

Câu trả lời:


72

Tôi không thấy lý do tại sao, nếu bạn có một giá trị vắng mặt có ý nghĩa, nullkhông nên được sử dụng nếu bạn cố tình và cẩn thận về nó.

Nếu mục tiêu của bạn là bao quanh giá trị nullable để tránh vô tình tham chiếu nó, tôi sẽ đề xuất tạo isPasswordChangedgiá trị dưới dạng hàm hoặc thuộc tính trả về kết quả của kiểm tra null, ví dụ:

class Member {
    DateTime lastChangePasswordTime = null;
    bool isPasswordChanged() { return lastChangePasswordTime != null; }

}

Theo tôi, làm theo cách này:

  • Cung cấp khả năng đọc mã tốt hơn so với kiểm tra null, điều này có thể làm mất ngữ cảnh.
  • Loại bỏ sự cần thiết cho bạn phải thực sự lo lắng về việc duy trì isPasswordChangedgiá trị mà bạn đề cập.

Cách bạn duy trì dữ liệu (có lẽ là trong cơ sở dữ liệu) sẽ chịu trách nhiệm đảm bảo các null được bảo tồn.


29
+1 cho lần mở. Việc sử dụng null của null thường không được chấp thuận chỉ trong trường hợp null là kết quả không mong muốn, nghĩa là nó sẽ không có ý nghĩa (đối với người tiêu dùng). Nếu null là có ý nghĩa, thì đó không phải là một kết quả bất ngờ.
Flater

28
Tôi sẽ đặt nó mạnh hơn một chút vì không bao giờ có hai biến duy trì trạng thái giống nhau. Nó sẽ thất bại. Ẩn một giá trị null, giả sử giá trị null có ý nghĩa bên trong thể hiện, từ bên ngoài là một điều rất tốt. Trong trường hợp này, sau đó bạn có thể quyết định rằng 1970-01-01 biểu thị rằng mật khẩu chưa bao giờ bị thay đổi. Sau đó, logic của phần còn lại của chương trình không phải quan tâm.
Bent

3
Bạn có thể quên gọi isPasswordChangedgiống như bạn có thể quên kiểm tra nếu nó không. Tôi thấy không có gì đạt được ở đây.
Gherman

9
@Gherman Nếu người tiêu dùng không hiểu khái niệm null là có ý nghĩa hoặc anh ta cần kiểm tra giá trị hiện tại, anh ta sẽ bị mất một trong hai cách, nhưng nếu phương thức được sử dụng, mọi người đều đọc mã tại sao rõ ràng là một kiểm tra và có một cơ hội hợp lý rằng giá trị là null / không có mặt. Mặt khác, không rõ liệu đó chỉ là một nhà phát triển thêm kiểm tra null chỉ "tại sao không" hoặc liệu đó có phải là một phần của khái niệm này hay không. Chắc chắn, người ta có thể tìm ra, nhưng một cách bạn có thông tin trực tiếp khác bạn cần xem xét việc thực hiện.
Frank Hopkins

2
@FrankHopkins: Điểm tốt, nhưng nếu sử dụng đồng thời, ngay cả việc kiểm tra một biến duy nhất cũng có thể yêu cầu khóa.
Christian Hackl

63

nullskhông xấu xa. Sử dụng chúng mà không cần suy nghĩ là. Đây là một trường hợp nullchính xác là câu trả lời - không có ngày nào.

Lưu ý rằng giải pháp của bạn tạo ra nhiều vấn đề hơn. Ý nghĩa của ngày được đặt thành một cái gì đó, nhưng isPasswordChangedlà sai? Bạn vừa tạo một trường hợp thông tin mâu thuẫn mà bạn cần nắm bắt và xử lý đặc biệt, trong khi một nullgiá trị có ý nghĩa rõ ràng, rõ ràng và không thể xung đột với thông tin khác.

Vì vậy, không, giải pháp của bạn không tốt hơn. Cho phép một nullgiá trị là cách tiếp cận đúng ở đây. Những người tuyên bố rằng nullluôn luôn xấu xa cho dù bối cảnh không hiểu tại sao nulltồn tại.


17
Trong lịch sử, nulltồn tại bởi vì Tony Hoare đã mắc sai lầm hàng tỷ đô la vào năm 1965. Những người cho rằng đó nulllà "xấu xa", đơn giản hóa quá mức chủ đề, tất nhiên, nhưng điều tốt là các ngôn ngữ hiện đại hoặc phong cách lập trình đang tránh xa null, thay thế nó với các loại tùy chọn chuyên dụng.
Christian Hackl

9
@ChristianHackl: chỉ vì lợi ích lịch sử - chết Hoare có bao giờ nói cái gì thay thế thực tế tốt hơn sẽ có (mà không chuyển sang một mô hình hoàn toàn mới)?
AnoE

5
@AnoE: Câu hỏi thú vị. Không biết Hoare đã nói gì về điều đó, nhưng lập trình không có giá trị thường là thực tế ngày nay trong các ngôn ngữ lập trình được thiết kế tốt, hiện đại.
Christian Hackl

10
@Tom wat? Trong các ngôn ngữ như C # và Java, DateTimetrường là một con trỏ. Toàn bộ khái niệm nullchỉ có ý nghĩa cho con trỏ!
Todd Sewell

16
@Tom: Con trỏ Null chỉ nguy hiểm đối với các ngôn ngữ và cách triển khai cho phép chúng được lập chỉ mục và bỏ qua một cách mù quáng, với bất kỳ điều gì có thể gây ra sự vui nhộn. Trong một ngôn ngữ bẫy cố gắng lập chỉ mục hoặc hủy đăng ký một con trỏ null, nó sẽ ít nguy hiểm hơn một con trỏ tới một đối tượng giả. Có nhiều tình huống khi chương trình chấm dứt bất thường có thể tốt hơn là để nó tạo ra những con số vô nghĩa, và trên các hệ thống bẫy chúng, con trỏ null là công thức phù hợp cho điều đó.
supercat

29

Tùy thuộc vào ngôn ngữ lập trình của bạn, có thể có các lựa chọn thay thế tốt, chẳng hạn như optionalloại dữ liệu. Trong C ++ (17), đây sẽ là std :: tùy chọn . Nó có thể vắng mặt hoặc có một giá trị tùy ý của kiểu dữ liệu cơ bản.


8
Có cả Optionaljava nữa; ý nghĩa của một null Optionallà tùy thuộc vào bạn ...
Matthieu M.

1
Đến đây để kết nối với Tùy chọn. Tôi đã mã hóa nó như một loại ngôn ngữ có chúng.
Zipp

2
@FrankHopkins Thật xấu hổ vì không có gì trong Java ngăn cản bạn viết Optional<Date> lastPasswordChangeTime = null;, nhưng tôi sẽ không từ bỏ chỉ vì điều đó. Thay vào đó, tôi sẽ thấm nhuần một quy tắc nhóm được tổ chức rất chắc chắn, rằng không có giá trị tùy chọn nào được phép gán null. Tùy chọn mua cho bạn quá nhiều tính năng hay để từ bỏ dễ dàng. Bạn có thể dễ dàng gán các giá trị mặc định ( orElsehoặc với đánh giá lười biếng orElseGet:), ném lỗi ( orElseThrow), chuyển đổi giá trị theo cách an toàn null ( map, flatmap), v.v.
Alexander

5
@FrankHopkins Việc kiểm tra isPresenthầu như luôn luôn là mùi mã, theo kinh nghiệm của tôi và là một dấu hiệu khá đáng tin cậy cho thấy các nhà phát triển đang sử dụng các tùy chọn này "thiếu điểm". Thật vậy, về cơ bản nó không mang lại lợi ích gì ref == null, nhưng nó cũng không phải là thứ bạn nên sử dụng thường xuyên. Ngay cả khi nhóm không ổn định, một công cụ phân tích tĩnh có thể đặt ra luật, như kẻ nói dối hoặc FindBugs , có thể bắt được hầu hết các trường hợp.
Alexander

5
@FrankHopkins: Có thể cộng đồng Java chỉ cần một chút thay đổi mô hình, có thể mất nhiều năm. Ví dụ, nếu bạn nhìn vào Scala, nó hỗ trợ nullvì ngôn ngữ tương thích 100% với Java, nhưng không ai sử dụng nó, và ở Option[T]khắp mọi nơi, với sự hỗ trợ lập trình chức năng tuyệt vời như map, flatMapkhớp mẫu, v.v. xa, xa ngoài == nullkiểm tra. Bạn đúng rằng về mặt lý thuyết, một loại tùy chọn nghe có vẻ ít hơn một con trỏ được tôn vinh, nhưng thực tế chứng minh rằng lập luận đó sai.
Christian Hackl

11

Sử dụng đúng

Có nhiều cách sử dụng khác nhau null. Cách phổ biến nhất và đúng về mặt ngữ nghĩa là sử dụng nó khi bạn có thể có hoặc không có một giá trị duy nhất . Trong trường hợp này, một giá trị bằng nullhoặc là một cái gì đó có ý nghĩa như một bản ghi từ cơ sở dữ liệu hoặc một cái gì đó.

Trong những tình huống này, sau đó bạn chủ yếu sử dụng nó như thế này (bằng mã giả):

if (value is null) {
  doSomethingAboutIt();
  return;
}

doSomethingUseful(value);

Vấn đề

Và nó có một vấn đề rất lớn. Vấn đề là vào thời điểm bạn gọi doSomethingUsefulgiá trị có thể chưa được kiểm tra null! Nếu không thì chương trình có thể sẽ bị sập. Và người dùng thậm chí có thể không nhìn thấy bất kỳ thông báo lỗi tốt nào, bị bỏ lại với một cái gì đó như "lỗi khủng khiếp: giá trị mong muốn nhưng không có giá trị!" (sau khi cập nhật: mặc dù có thể có ít thông tin lỗi hơn như Segmentation fault. Core dumped., hoặc tệ hơn nữa, không có lỗi và thao tác không chính xác trên null trong một số trường hợp)

Quên viết séc nullvà xử lý các nulltình huống là một lỗi cực kỳ phổ biến . Đây là lý do tại sao Tony Hoare, người đã phát minh ra nulltại một hội nghị phần mềm có tên QCon London năm 2009 rằng ông đã mắc sai lầm hàng tỷ đô la vào năm 1965: https://www.infoq.com/presentations/Null-References-The-Billion-Dollar- Sai lầm-Tony-Hoare

Tránh vấn đề

Một số công nghệ và ngôn ngữ giúp kiểm tra nullkhông thể quên theo các cách khác nhau, giảm số lượng lỗi.

Ví dụ, Haskell có Maybeđơn nguyên thay vì null. Giả sử đó DatabaseRecordlà một loại do người dùng định nghĩa. Trong Haskell, một giá trị của loại Maybe DatabaseRecordcó thể bằng Just <somevalue>hoặc nó có thể bằng Nothing. Sau đó, bạn có thể sử dụng nó theo nhiều cách khác nhau nhưng cho dù bạn sử dụng nó như thế nào, bạn không thể áp dụng một số thao tác trên Nothingmà không biết.

Ví dụ, hàm này được gọi là zeroAsDefaulttrả về xcho Just x0cho Nothing:

zeroAsDefault :: Maybe Int -> Int
zeroAsDefault mx = case mx of
    Nothing -> 0
    Just x -> x

Christian Hackl nói C ++ 17 và Scala có cách riêng của họ. Vì vậy, bạn có thể muốn thử tìm hiểu xem ngôn ngữ của bạn có bất cứ thứ gì như vậy không và sử dụng nó.

Nulls vẫn được sử dụng rộng rãi

Nếu bạn không có gì tốt hơn thì sử dụng nulllà tốt. Chỉ cần tiếp tục xem ra cho nó. Gõ khai báo trong các hàm sẽ giúp bạn phần nào.

Ngoài ra điều đó nghe có vẻ không tiến bộ lắm nhưng bạn nên kiểm tra xem đồng nghiệp của mình có muốn sử dụng nullhay không. Họ có thể bảo thủ và có thể không muốn sử dụng cấu trúc dữ liệu mới vì một số lý do. Ví dụ, hỗ trợ các phiên bản cũ hơn của một ngôn ngữ. Những điều như vậy nên được khai báo trong các tiêu chuẩn mã hóa của dự án và thảo luận đúng đắn với nhóm.

Theo đề nghị của bạn

Bạn đề nghị sử dụng một trường boolean riêng. Nhưng dù sao bạn cũng phải kiểm tra nó và vẫn có thể quên kiểm tra nó. Vì vậy, không có gì chiến thắng ở đây. Nếu bạn thậm chí có thể quên một cái gì đó khác, như cập nhật cả hai giá trị mỗi lần, thì điều đó còn tồi tệ hơn. Nếu vấn đề quên kiểm tra nullkhông được giải quyết thì không có điểm nào. Tránh nulllà khó khăn và bạn không nên làm theo cách làm cho nó tồi tệ hơn.

Làm thế nào để không sử dụng null

Cuối cùng, có những cách phổ biến để sử dụng nullkhông chính xác. Một cách như vậy là sử dụng nó thay cho các cấu trúc dữ liệu trống như mảng và chuỗi. Một mảng trống là một mảng thích hợp như bất kỳ khác! Nó hầu như luôn luôn quan trọng và hữu ích cho các cấu trúc dữ liệu, có thể phù hợp với nhiều giá trị, có thể để trống, tức là có 0 độ dài.

Từ quan điểm đại số, một chuỗi rỗng cho các chuỗi giống như 0 cho các số, tức là nhận dạng:

a+0=a
concat(str, '')=str

Chuỗi rỗng cho phép các chuỗi nói chung trở thành một chuỗi đơn: https://en.wikipedia.org/wiki/Monoid Nếu bạn không hiểu thì điều đó không quan trọng đối với bạn.

Bây giờ hãy xem tại sao nó quan trọng để lập trình với ví dụ này:

for (element in array) {
  doSomething(element);
}

Nếu chúng ta vượt qua một mảng trống ở đây, mã sẽ hoạt động tốt. Nó sẽ không làm gì cả. Tuy nhiên, nếu chúng tôi vượt qua nullở đây thì có thể chúng tôi sẽ gặp sự cố với một lỗi như "không thể lặp qua null, xin lỗi". Chúng tôi có thể bọc nó iflại nhưng ít sạch hơn và một lần nữa, bạn có thể quên kiểm tra nó

Cách xử lý null

Những gì doSomethingAboutIt()nên làm và đặc biệt là liệu nó có nên ném một ngoại lệ hay không là một vấn đề phức tạp khác. Nói tóm lại, nó phụ thuộc vào việc nullgiá trị đầu vào có thể chấp nhận được đối với một tác vụ nhất định hay không và điều gì được mong đợi trong phản hồi. Các ngoại lệ dành cho các sự kiện không được mong đợi. Tôi sẽ không đi sâu hơn vào chủ đề đó. Câu trả lời này rất lâu rồi.


5
Trên thực tế, horrible error: wanted value but got null!tốt hơn nhiều so với điển hình hơn Segmentation fault. Core dumped....
Toby Speight

2
@TobySpeight Đúng, nhưng tôi thích cái gì đó nằm trong các điều khoản của người dùng như thế nào Error: the product you want to buy is out of stock.
Gherman

Chắc chắn - tôi chỉ quan sát rằng nó có thể (và thường là) thậm chí còn tồi tệ hơn . Tôi hoàn toàn đồng ý về những gì sẽ tốt hơn! Và câu trả lời của bạn là khá nhiều những gì tôi đã nói với câu hỏi này: +1.
Toby Speight

2
@Gherman: Vượt qua nullnơi nullkhông được phép là lỗi lập trình, tức là lỗi trong mã. Một sản phẩm hết hàng là một phần của logic kinh doanh thông thường và không phải là một lỗi. Bạn không thể và không nên cố gắng dịch phát hiện lỗi sang một thông báo bình thường trong giao diện người dùng. Xâm nhập ngay lập tức và không thực thi nhiều mã hơn là cách hành động được ưa thích, đặc biệt nếu đó là một ứng dụng quan trọng (ví dụ: một ứng dụng thực hiện các giao dịch tài chính thực sự).
Christian Hackl

1
@EricDuminil Chúng tôi muốn loại bỏ (hoặc giảm) khả năng mắc lỗi, không xóa nullhoàn toàn, ngay cả khỏi nền.
Gherman

4

Ngoài tất cả các câu trả lời rất hay được đưa ra trước đây, tôi nói thêm rằng mỗi khi bạn muốn để một trường không có giá trị, hãy suy nghĩ cẩn thận nếu đó có thể là một loại danh sách. Một loại nullable tương đương là một danh sách gồm 0 hoặc 1 phần tử và thường thì phần này có thể được khái quát thành một danh sách các phần tử N. Cụ thể trong trường hợp này, bạn có thể muốn xem xét lastChangePasswordTimelà một danh sách passwordChangeTimes.


Đó là một điểm tuyệt vời. Điều tương tự thường đúng với các loại tùy chọn; một tùy chọn có thể được xem như trường hợp đặc biệt của chuỗi.
Christian Hackl

2

Hãy tự hỏi mình điều này: hành vi nào yêu cầu trường lastChangePasswordTime?

Nếu bạn cần trường đó cho phương thức IsPasswordExpired () để xác định xem Thành viên có nên được nhắc thay đổi mật khẩu thường xuyên không, tôi sẽ đặt trường theo thời gian Thành viên được tạo ban đầu. Việc triển khai IsPasswordExpired () là giống nhau đối với các thành viên mới và thành viên hiện có.

class Member{
   private DateTime lastChangePasswordTime;

   public Member(DateTime lastChangePasswordTime) {
      // set value, maybe check for null
   }

   public bool IsPasswordExpired() {
      DateTime limit = DateTime.Now.AddMonths(-3);
      return lastChangePasswordTime < limit;
   }
}

Nếu bạn có một yêu cầu riêng mà các thành viên mới tạo phải cập nhật mật khẩu của họ, tôi sẽ thêm một trường boolean riêng biệt được gọi là passwordShouldBeChanged và đặt nó thành đúng khi tạo. Sau đó, tôi sẽ thay đổi chức năng của phương thức IsPasswordExpired () để bao gồm kiểm tra cho trường đó (và đổi tên phương thức thành ShouldChangePassword).

class Member{
   private DateTime lastChangePasswordTime;
   private bool passwordShouldBeChanged;

   public Member(DateTime lastChangePasswordTime, bool passwordShouldBeChanged) {
      // set values, maybe check for nulls
   }

   public bool ShouldChangePassword() {
      return PasswordExpired(lastChangePasswordTime) || passwordShouldBeChanged;
   }

   private static bool PasswordExpired(DateTime lastChangePasswordTime) {
      DateTime limit = DateTime.Now.AddMonths(-3);
      return lastChangePasswordTime < limit;
   }
}

Làm cho ý định của bạn rõ ràng trong mã.


2

Đầu tiên, nulls là xấu xa là giáo điều và như thường lệ với giáo điều, nó hoạt động tốt nhất như một hướng dẫn và không phải là vượt qua / không vượt qua bài kiểm tra.

Thứ hai, bạn có thể xác định lại tình huống của mình theo cách có ý nghĩa rằng giá trị không bao giờ có thể là null. InititialPasswordChanged là Boolean ban đầu được đặt thành false, PasswordSetTime là ngày và giờ khi mật khẩu hiện tại được đặt.

Lưu ý rằng mặc dù điều này có chi phí nhỏ, nhưng bây giờ bạn có thể LUÔN tính toán thời gian tồn tại kể từ khi mật khẩu được đặt lần cuối.


1

Cả hai đều 'an toàn / lành mạnh / chính xác' nếu người gọi kiểm tra trước khi sử dụng. Vấn đề là những gì xảy ra nếu người gọi không kiểm tra. Cái nào tốt hơn, một số hương vị của lỗi null hoặc sử dụng một giá trị không hợp lệ?

Không có câu trả lời đúng duy nhất. Nó phụ thuộc vào những gì bạn đang lo lắng.

Nếu sự cố thực sự xấu nhưng câu trả lời không quan trọng hoặc có giá trị mặc định được chấp nhận, thì có lẽ sử dụng boolean làm cờ sẽ tốt hơn. Nếu sử dụng câu trả lời sai là một vấn đề tồi tệ hơn so với sự cố, thì sử dụng null là tốt hơn.

Đối với phần lớn các trường hợp 'điển hình', thất bại nhanh và buộc người gọi phải kiểm tra là cách nhanh nhất để kiểm tra mã, và do đó tôi nghĩ null nên là lựa chọn mặc định. Mặc dù vậy, tôi sẽ không đặt quá nhiều niềm tin vào "X là gốc rễ của mọi nhà truyền giáo xấu xa"; họ thường không lường trước được tất cả các trường hợp sử dụng.

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.