Giải thích tốt nhất cho các ngôn ngữ không có null


225

Mỗi khi lập trình viên phàn nàn về lỗi null / ngoại lệ, có người hỏi chúng tôi làm gì mà không có null.

Tôi có một số ý tưởng cơ bản về sự mát mẻ của các loại tùy chọn, nhưng tôi không có kỹ năng về kiến ​​thức hoặc ngôn ngữ để thể hiện tốt nhất. Một lời giải thích tuyệt vời về những điều sau đây được viết theo cách tiếp cận với lập trình viên trung bình mà chúng ta có thể hướng người đó đến là gì?

  • Theo mặc định, tính không mong muốn của việc có các tham chiếu / con trỏ là nullable
  • Cách các loại tùy chọn hoạt động bao gồm các chiến lược để dễ dàng kiểm tra các trường hợp null như
    • khớp mẫu và
    • hiểu đơn điệu
  • Giải pháp thay thế như tin nhắn ăn nil
  • (các khía cạnh khác tôi đã bỏ lỡ)

11
Nếu bạn thêm thẻ vào câu hỏi này để lập trình chức năng hoặc F #, bạn nhất định sẽ nhận được một số câu trả lời tuyệt vời.
Stephen Swensen

Tôi đã thêm thẻ lập trình chức năng vì loại tùy chọn đã đến từ thế giới ml. Tôi thà không đánh dấu nó F # (quá cụ thể). BTW ai đó có quyền phân loại cần thêm thẻ có thể loại hoặc tùy chọn.
Roman A. Taycher

4
Có rất ít nhu cầu cho các thẻ cụ thể như vậy, tôi nghi ngờ. Các thẻ chủ yếu để cho phép mọi người tìm thấy các câu hỏi có liên quan (ví dụ: "những câu hỏi tôi biết nhiều và sẽ có thể trả lời", và "lập trình chức năng" rất hữu ích ở đó. Nhưng một cái gì đó như "null" hoặc " loại tùy chọn "ít hữu ích hơn. Rất ít người có khả năng theo dõi thẻ" loại tùy chọn "để tìm câu hỏi mà họ có thể trả lời.)
jalf

Chúng ta đừng quên rằng một trong những lý do chính cho null là máy tính phát triển mạnh mẽ gắn liền với lý thuyết. Null là một trong những tập hợp quan trọng nhất trong tất cả các lý thuyết tập hợp. Nếu không có nó, toàn bộ thuật toán sẽ bị hỏng. Ví dụ - thực hiện sắp xếp hợp nhất. Điều này liên quan đến việc phá vỡ một danh sách trong một nửa nhiều lần. Nếu danh sách dài 7 mục thì sao? Đầu tiên bạn chia nó thành 4 và 3. Sau đó 2, 2, 2 và 1. Sau đó 1, 1, 1, 1, 1, 1, 1 và .... null! Null có một mục đích, chỉ là một mục đích mà bạn không thấy thực tế. Nó tồn tại nhiều hơn cho lĩnh vực lý thuyết.
stevendesu

6
@steven_desu - Tôi không đồng ý. Trong các ngôn ngữ 'nullable', bạn có thể có một tham chiếu đến danh sách trống [] và cả tham chiếu danh sách null. Câu hỏi này liên quan đến sự nhầm lẫn giữa hai.
stusmith

Câu trả lời:


433

Tôi nghĩ rằng tóm tắt ngắn gọn về lý do tại sao null là không mong muốn là các trạng thái vô nghĩa không nên được đại diện .

Giả sử tôi đang làm mẫu một cánh cửa. Nó có thể ở một trong ba trạng thái: mở, đóng nhưng mở khóa và đóng và khóa. Bây giờ tôi có thể mô hình hóa nó dọc theo dòng

class Door
    private bool isShut
    private bool isLocked

và rõ ràng làm thế nào để ánh xạ ba trạng thái của tôi vào hai biến boolean này. Nhưng điều này để lại một trạng thái thứ tư, không mong muốn có sẵn : isShut==false && isLocked==true. Bởi vì các loại tôi đã chọn làm đại diện của mình thừa nhận trạng thái này, tôi phải dành nỗ lực tinh thần để đảm bảo rằng lớp không bao giờ rơi vào trạng thái này (có lẽ bằng cách mã hóa rõ ràng một bất biến). Ngược lại, nếu tôi đang sử dụng ngôn ngữ có kiểu dữ liệu đại số hoặc bảng liệt kê được kiểm tra cho phép tôi xác định

type DoorState =
    | Open | ShutAndUnlocked | ShutAndLocked

sau đó tôi có thể định nghĩa

class Door
    private DoorState state

và không còn lo lắng nữa. Hệ thống loại sẽ đảm bảo rằng chỉ có ba trạng thái có thể xảy ra đối với một trường hợp class Door. Đây là loại hệ thống nào tốt - loại trừ rõ ràng cả một loại lỗi tại thời điểm biên dịch.

Vấn đề với nulllà mọi loại tham chiếu đều có trạng thái bổ sung này trong không gian của nó thường không mong muốn. Một stringbiến có thể là bất kỳ chuỗi ký tự nào, hoặc nó có thể là nullgiá trị bổ sung điên rồ này không ánh xạ vào miền vấn đề của tôi. Một Triangleđối tượng có ba Points, bản thân chúng có XYgiá trị, nhưng thật không may Point, Trianglechính nó hoặc có thể là giá trị null điên rồ này vô nghĩa đối với miền đồ thị mà tôi đang làm việc. V.v.

Khi bạn có ý định mô hình hóa một giá trị có thể không tồn tại, thì bạn nên chọn tham gia một cách rõ ràng. Nếu cách tôi định làm người mẫu là mọi người đều Personcó một FirstNamevà một LastName, nhưng chỉ một số người có MiddleNames, thì tôi muốn nói điều gì đó như

class Person
    private string FirstName
    private Option<string> MiddleName
    private string LastName

trong đó stringở đây được coi là một loại không nullable. Sau đó, không có bất biến khó khăn nào để thiết lập và không có bất ngờ NullReferenceExceptionnào khi cố gắng tính độ dài của tên người. Hệ thống loại đảm bảo rằng bất kỳ mã nào xử lý các MiddleNametài khoản cho khả năng của nó None, trong khi bất kỳ mã nào xử lý FirstNamemột cách an toàn đều có thể có giá trị ở đó.

Vì vậy, ví dụ, bằng cách sử dụng loại ở trên, chúng ta có thể tạo ra hàm ngớ ngẩn này:

let TotalNumCharsInPersonsName(p:Person) =
    let middleLen = match p.MiddleName with
                    | None -> 0
                    | Some(s) -> s.Length
    p.FirstName.Length + middleLen + p.LastName.Length

không phải lo lắng Ngược lại, trong một ngôn ngữ có tham chiếu nullable cho các loại như chuỗi, sau đó giả sử

class Person
    private string FirstName
    private string MiddleName
    private string LastName

bạn kết thúc tác giả như

let TotalNumCharsInPersonsName(p:Person) =
    p.FirstName.Length + p.MiddleName.Length + p.LastName.Length

sẽ nổ tung nếu đối tượng Person đến không có bất biến mọi thứ đều không rỗng, hoặc

let TotalNumCharsInPersonsName(p:Person) =
    (if p.FirstName=null then 0 else p.FirstName.Length)
    + (if p.MiddleName=null then 0 else p.MiddleName.Length)
    + (if p.LastName=null then 0 else p.LastName.Length)

hoặc có thể

let TotalNumCharsInPersonsName(p:Person) =
    p.FirstName.Length
    + (if p.MiddleName=null then 0 else p.MiddleName.Length)
    + p.LastName.Length

giả sử rằng pđảm bảo đầu tiên / cuối cùng là có nhưng giữa có thể là null hoặc có thể bạn thực hiện kiểm tra đưa ra các loại ngoại lệ khác nhau, hoặc ai biết cái gì. Tất cả những lựa chọn thực hiện điên rồ này và những điều cần suy nghĩ về việc trồng trọt bởi vì có giá trị đại diện ngu ngốc này mà bạn không muốn hoặc không cần.

Null thường thêm phức tạp không cần thiết. Sự phức tạp là kẻ thù của tất cả các phần mềm, và bạn nên cố gắng giảm độ phức tạp bất cứ khi nào hợp lý.

(Lưu ý rằng có nhiều sự phức tạp hơn ngay cả với những ví dụ đơn giản này. Ngay cả khi FirstNamekhông thể null, một chuỗi stringcó thể đại diện ""(chuỗi trống), có lẽ cũng không phải là tên người mà chúng tôi dự định mô hình. Như vậy, ngay cả với không phải Các chuỗi không thể, vẫn có thể là trường hợp chúng ta "đại diện cho các giá trị vô nghĩa". Một lần nữa, bạn có thể chọn chiến đấu với điều này thông qua bất biến và mã điều kiện trong thời gian chạy hoặc bằng cách sử dụng hệ thống loại (ví dụ: có một NonEmptyStringloại). sau này có lẽ không được khuyến khích (các loại "tốt" thường bị "đóng" trong một tập hợp các hoạt động chung và ví dụ: NonEmptyStringkhông được đóng lại.SubString(0,0)), nhưng nó thể hiện nhiều điểm hơn trong không gian thiết kế. Vào cuối ngày, trong bất kỳ hệ thống loại nhất định nào, có một số phức tạp sẽ rất tốt trong việc loại bỏ, và sự phức tạp khác thực sự khó loại bỏ hơn. Chìa khóa cho chủ đề này là trong gần như mọi hệ thống loại, sự thay đổi từ "tham chiếu không thể theo mặc định" thành "tham chiếu không thể rỗng theo mặc định" gần như luôn là một thay đổi đơn giản giúp hệ thống loại tốt hơn rất nhiều trong việc chống lại sự phức tạp và loại trừ một số loại lỗi và trạng thái vô nghĩa. Vì vậy, thật điên rồ khi nhiều ngôn ngữ cứ lặp đi lặp lại lỗi này.)


31
Re: tên - Thật vậy. Và có thể bạn quan tâm đến việc mô hình hóa một cánh cửa đang mở nhưng với chốt khóa dính ra, ngăn chặn cánh cửa đóng lại. Có rất nhiều điều phức tạp trên thế giới. Điều quan trọng là không để thêm nhiều phức tạp khi thực hiện ánh xạ giữa "các quốc gia trên thế giới" và "quốc gia chương trình" trong phần mềm của bạn.
Brian

59
Gì, bạn chưa bao giờ khóa cửa mở?
Joshua

58
Tôi không hiểu tại sao mọi người lại làm việc về ngữ nghĩa của một tên miền cụ thể. Brian đại diện cho các lỗ hổng bằng null một cách ngắn gọn và đơn giản, vâng, anh ta đã đơn giản hóa miền vấn đề trong ví dụ của mình bằng cách nói rằng mọi người đều có tên và họ. Câu hỏi đã được trả lời cho 'T', Brian - nếu bạn từng ở boston tôi nợ bạn một cốc bia cho tất cả các bài đăng bạn làm ở đây!
akaphenom

67
@akaphenom: cảm ơn, nhưng lưu ý rằng không phải tất cả mọi người đều uống bia (tôi là người không uống rượu). Nhưng tôi đánh giá cao rằng bạn chỉ đang sử dụng một mô hình đơn giản hóa của thế giới để truyền đạt lòng biết ơn, vì vậy tôi sẽ không ngụy biện nhiều hơn về các giả định thiếu sót của mô hình thế giới của bạn. : P (Quá nhiều phức tạp trong thế giới thực! :))
Brian

4
Kỳ lạ thay, có 3 cửa nhà nước trên thế giới này! Chúng được sử dụng trong một số khách sạn như cửa nhà vệ sinh. Một nút ấn hoạt động như một chìa khóa từ bên trong, khóa cửa từ bên ngoài. Nó được tự động mở khóa, ngay khi chốt chốt di chuyển.
comonad

65

Điều hay ho về các loại tùy chọn không phải là chúng tùy chọn. Đó là tất cả các loại khác không .

Đôi khi , chúng ta cần có khả năng đại diện cho một loại trạng thái "null". Đôi khi chúng ta phải biểu diễn một tùy chọn "không có giá trị" cũng như các giá trị có thể khác mà một biến có thể mất. Vì vậy, một ngôn ngữ thẳng thừng không cho phép điều này sẽ bị tê liệt một chút.

Nhưng thông thường , chúng ta không cần nó và cho phép trạng thái "null" như vậy chỉ dẫn đến sự mơ hồ và nhầm lẫn: mỗi khi tôi truy cập một biến loại tham chiếu trong .NET, tôi phải xem xét rằng nó có thể là null .

Thông thường, nó sẽ không bao giờ thực sự là null, bởi vì lập trình viên cấu trúc mã để nó không bao giờ có thể xảy ra. Nhưng trình biên dịch không thể xác minh điều đó, và mỗi lần bạn nhìn thấy nó, bạn phải tự hỏi mình "điều này có thể là null không? Tôi có cần kiểm tra null ở đây không?"

Lý tưởng nhất, trong nhiều trường hợp null không có ý nghĩa, nó không được phép .

Đó là khó khăn để đạt được trong .NET, nơi gần như mọi thứ đều có thể là null. Bạn phải dựa vào tác giả của mã mà bạn gọi là 100% có kỷ luật và nhất quán và đã ghi lại rõ ràng những gì có thể và không thể là không, hoặc bạn phải hoang tưởng và kiểm tra mọi thứ .

Tuy nhiên, nếu các loại không có giá trị theo mặc định , thì bạn không cần kiểm tra xem chúng có rỗng hay không. Bạn biết rằng chúng không bao giờ có thể là null, bởi vì trình biên dịch / trình kiểm tra loại thực thi điều đó cho bạn.

Và sau đó chúng ta chỉ cần một cánh cửa trở lại cho các trường hợp hiếm hoi mà chúng ta làm cần thiết để xử lý tình trạng null. Sau đó, một loại "tùy chọn" có thể được sử dụng. Sau đó, chúng tôi cho phép null trong trường hợp chúng tôi đưa ra quyết định có ý thức rằng chúng tôi cần có thể đại diện cho trường hợp "không có giá trị" và trong mọi trường hợp khác, chúng tôi biết rằng giá trị sẽ không bao giờ là null.

Như những người khác đã đề cập, trong C # hoặc Java chẳng hạn, null có thể có nghĩa là một trong hai điều sau:

  1. các biến là chưa được khởi tạo. Điều này nên, lý tưởng, không bao giờ xảy ra. Một biến không nên tồn tại trừ khi nó được khởi tạo.
  2. biến chứa một số dữ liệu "tùy chọn": nó cần có khả năng đại diện cho trường hợp không có dữ liệu . Điều này đôi khi là cần thiết. Có lẽ bạn đang cố gắng tìm một đối tượng trong danh sách và bạn không biết trước liệu nó có ở đó hay không. Sau đó, chúng ta cần có khả năng đại diện rằng "không tìm thấy đối tượng".

Ý nghĩa thứ hai phải được bảo tồn, nhưng nghĩa thứ nhất cần được loại bỏ hoàn toàn. Và thậm chí ý nghĩa thứ hai không nên là mặc định. Đó là một cái gì đó chúng ta có thể chọn tham gia nếu và khi chúng ta cần nó . Nhưng khi chúng tôi không cần một cái gì đó là tùy chọn, chúng tôi muốn trình kiểm tra loại đảm bảo rằng nó sẽ không bao giờ là null.


Và trong ý nghĩa thứ hai, chúng tôi muốn trình biên dịch cảnh báo (dừng?) Cho chúng tôi nếu chúng tôi cố gắng truy cập các biến như vậy mà không kiểm tra tính vô hiệu trước. Đây là một bài viết tuyệt vời về tính năng C # null / non-null sắp tới (cuối cùng!) Blog.msdn.microsoft.com/dotnet/2017/11/15/ Kẻ
Ohad Schneider

44

Tất cả các câu trả lời cho đến nay tập trung vào lý do tại sao nulllà một điều xấu, và thật hữu ích nếu một ngôn ngữ có thể đảm bảo rằng các giá trị nhất định sẽ không bao giờkhông có giá trị.

Sau đó, họ tiếp tục đề xuất rằng đó sẽ là một ý tưởng khá gọn gàng nếu bạn thực thi không có giá trị cho tất cả các giá trị, điều này có thể được thực hiện nếu bạn thêm một khái niệm như Optionhoặc Maybeđại diện cho các loại có thể không phải lúc nào cũng có giá trị được xác định. Đây là cách tiếp cận được thực hiện bởi Haskell.

Đó là tất cả những thứ tốt! Nhưng nó không loại trừ việc sử dụng các loại nullable / non-null rõ ràng để đạt được hiệu quả tương tự. Tại sao, sau đó, Tùy chọn vẫn là một điều tốt? Xét cho cùng, Scala hỗ trợ các giá trị nullable ( phải có , vì vậy nó có thể hoạt động với các thư viện Java) nhưng cũng hỗ trợ Options.

H: Vậy những lợi ích ngoài việc có thể loại bỏ null khỏi ngôn ngữ hoàn toàn là gì?

A. Thành phần

Nếu bạn thực hiện một bản dịch ngây thơ từ mã nhận biết null

def fullNameLength(p:Person) = {
  val middleLen =
    if (null == p.middleName)
      p.middleName.length
    else
      0
  p.firstName.length + middleLen + p.lastName.length
}

để mã nhận biết tùy chọn

def fullNameLength(p:Person) = {
  val middleLen = p.middleName match {
    case Some(x) => x.length
    case _ => 0
  }
  p.firstName.length + middleLen + p.lastName.length
}

không có nhiều khác biệt! Nhưng đó cũng là một cách khủng khiếp để sử dụng Tùy chọn ... Cách tiếp cận này gọn gàng hơn nhiều:

def fullNameLength(p:Person) = {
  val middleLen = p.middleName map {_.length} getOrElse 0
  p.firstName.length + middleLen + p.lastName.length
}

Hoặc thậm chí:

def fullNameLength(p:Person) =       
  p.firstName.length +
  p.middleName.map{length}.getOrElse(0) +
  p.lastName.length

Khi bạn bắt đầu giao dịch với Danh sách Tùy chọn, nó thậm chí còn tốt hơn. Hãy tưởng tượng rằng Danh sách peoplelà tùy chọn:

people flatMap(_ find (_.firstName == "joe")) map (fullNameLength)

Cái này hoạt động ra sao?

//convert an Option[List[Person]] to an Option[S]
//where the function f takes a List[Person] and returns an S
people map f

//find a person named "Joe" in a List[Person].
//returns Some[Person], or None if "Joe" isn't in the list
validPeopleList find (_.firstName == "joe")

//returns None if people is None
//Some(None) if people is valid but doesn't contain Joe
//Some[Some[Person]] if Joe is found
people map (_ find (_.firstName == "joe")) 

//flatten it to return None if people is None or Joe isn't found
//Some[Person] if Joe is found
people flatMap (_ find (_.firstName == "joe")) 

//return Some(length) if the list isn't None and Joe is found
//otherwise return None
people flatMap (_ find (_.firstName == "joe")) map (fullNameLength)

Mã tương ứng với kiểm tra null (hoặc thậm chí elvis ?: Toán tử) sẽ rất dài. Thủ thuật thực sự ở đây là thao tác FlatMap, cho phép hiểu được các tùy chọn và bộ sưu tập lồng nhau theo cách mà các giá trị nullable không bao giờ có thể đạt được.


8
+1, đây là một điểm tốt để nhấn mạnh. Một phụ lục: trên vùng đất Haskell, flatMapsẽ được gọi (>>=), đó là toán tử "liên kết" cho các đơn nguyên. Đúng vậy, Haskeller thích flatMapnhững thứ ping đến mức chúng tôi đặt nó vào logo của ngôn ngữ của chúng tôi.
CA McCann

1
+1 Hy vọng rằng một biểu thức Option<T>sẽ không bao giờ, không bao giờ là null. Đáng buồn thay, Scala là uhh, vẫn được liên kết với Java :-) (Mặt khác, nếu Scala không chơi tốt với Java, ai sẽ sử dụng nó? Oo)

Đủ dễ để làm: 'Danh sách (null) .headOption'. Lưu ý rằng điều này có nghĩa là một điều rất khác so với giá trị trả về của 'Không'
Kevin Wright

4
Tôi đã cho bạn tiền thưởng vì tôi thực sự thích những gì bạn nói về sáng tác, mà những người khác dường như không đề cập đến.
Roman A. Taycher

Câu trả lời tuyệt vời với các ví dụ tuyệt vời!
thSoft

38

Vì mọi người dường như đang thiếu nó: nulllà mơ hồ.

Ngày sinh của Alice là null. Nó có nghĩa là gì?

Ngày mất của Bob là null. Điều đó nghĩa là gì?

Một cách giải thích "hợp lý" có thể là ngày sinh của Alice tồn tại nhưng không rõ, trong khi ngày chết của Bob không tồn tại (Bob vẫn còn sống). Nhưng tại sao chúng ta lại nhận được những câu trả lời khác nhau?


Một vấn đề khác: nulllà một trường hợp cạnh.

  • null = null?
  • nan = nan?
  • inf = inf?
  • +0 = -0?
  • +0/0 = -0/0?

Các câu trả lời thường là "có", "không", "có", "có", "không", "có" tương ứng. Những "nhà toán học" điên rồ gọi NaN là "nullity" và nói rằng nó so sánh với chính nó. SQL coi null là không bằng bất cứ thứ gì (vì vậy chúng hoạt động như NaN). Người ta tự hỏi điều gì sẽ xảy ra khi bạn cố lưu trữ ±, ± 0 và NaN vào cùng một cột cơ sở dữ liệu (có 2 53 NaN, một nửa trong số đó là "âm").

Để làm cho vấn đề tồi tệ hơn, cơ sở dữ liệu khác nhau về cách họ đối xử với NULL và hầu hết trong số họ không nhất quán (xem Xử lý NULL trong SQLite để biết tổng quan). Nó thật kinh khủng.


Và bây giờ cho câu chuyện bắt buộc:

Gần đây tôi đã thiết kế một bảng cơ sở dữ liệu (sqlite3) với năm cột a NOT NULL, b, id_a, id_b NOT NULL, timestamp. Bởi vì đó là một lược đồ chung được thiết kế để giải quyết vấn đề chung cho các ứng dụng khá độc đoán, có hai ràng buộc duy nhất:

UNIQUE(a, b, id_a)
UNIQUE(a, b, id_b)

id_achỉ tồn tại để tương thích với thiết kế ứng dụng hiện có (một phần vì tôi chưa đưa ra giải pháp tốt hơn) và không được sử dụng trong ứng dụng mới. Do cách NULL làm việc trong SQL, tôi có thể chèn (1, 2, NULL, 3, t)(1, 2, NULL, 4, t)và không vi phạm các hạn chế tính độc đáo đầu tiên (vì (1, 2, NULL) != (1, 2, NULL)).

Điều này đặc biệt hiệu quả vì cách NULL hoạt động trong một ràng buộc duy nhất trên hầu hết các cơ sở dữ liệu (có lẽ để mô hình hóa các tình huống "thế giới thực" dễ dàng hơn, ví dụ như không có hai người có thể có Số An sinh Xã hội giống nhau, nhưng không phải tất cả mọi người đều có một).


FWIW, mà không gọi hành vi đầu tiên không xác định, các tham chiếu C ++ không thể "trỏ tới" null và không thể xây dựng một lớp với các biến thành viên tham chiếu chưa được khởi tạo (nếu ném ngoại lệ, việc xây dựng không thành công).

Sidenote: Đôi khi bạn có thể muốn con trỏ loại trừ lẫn nhau (nghĩa là chỉ một trong số chúng có thể không phải là NULL), ví dụ như trong iOS giả định type DialogState = NotShown | ShowingActionSheet UIActionSheet | ShowingAlertView UIAlertView | Dismissed. Thay vào đó, tôi buộc phải làm những thứ như assert((bool)actionSheet + (bool)alertView == 1).


Các nhà toán học thực tế không sử dụng khái niệm "NaN", yên tâm.
Noldorin

@Noldorin: Họ làm, nhưng họ sử dụng thuật ngữ "hình thức không xác định".
IJ Kennedy

@IJKennedy: Đó là một trường đại học khác, mà tôi biết khá rõ cảm ơn bạn. Một số 'NaN có thể biểu thị dạng không xác định, nhưng vì FPA không thực hiện lý luận tượng trưng, ​​việc đánh đồng nó với dạng không xác định là khá sai lệch!
Noldorin

Có chuyện gì với bạn assert(actionSheet ^ alertView)vậy? Hoặc không thể ngôn ngữ của bạn XOR bools?
mèo

16

Theo mặc định, việc không có tham chiếu / con trỏ là không thể thực hiện được.

Tôi không nghĩ đây là vấn đề chính với null, vấn đề chính với null là chúng có thể có hai ý nghĩa:

  1. Tham chiếu / con trỏ không được khởi tạo: vấn đề ở đây giống như tính biến đổi nói chung. Đối với một, nó làm cho việc phân tích mã của bạn khó khăn hơn.
  2. Biến là null thực sự có nghĩa là gì đó: đây là trường hợp các loại Tùy chọn thực sự chính thức hóa.

Các ngôn ngữ hỗ trợ Các loại tùy chọn cũng thường cấm hoặc không khuyến khích sử dụng các biến chưa được khởi tạo.

Làm thế nào các loại tùy chọn hoạt động bao gồm các chiến lược để dễ dàng kiểm tra các trường hợp null như khớp mẫu.

Để có hiệu lực, các loại Tùy chọn cần được hỗ trợ trực tiếp bằng ngôn ngữ. Nếu không, phải mất rất nhiều mã nồi hơi để mô phỏng chúng. Khớp mẫu và suy luận kiểu là hai tính năng ngôn ngữ chính làm cho các loại Tùy chọn dễ làm việc. Ví dụ:

Trong F #:

//first we create the option list, and then filter out all None Option types and 
//map all Some Option types to their values.  See how type-inference shines.
let optionList = [Some(1); Some(2); None; Some(3); None]
optionList |> List.choose id //evaluates to [1;2;3]

//here is a simple pattern-matching example
//which prints "1;2;None;3;None;".
//notice how value is extracted from op during the match
optionList 
|> List.iter (function Some(value) -> printf "%i;" value | None -> printf "None;")

Tuy nhiên, trong một ngôn ngữ như Java mà không có hỗ trợ trực tiếp cho các loại Tùy chọn, chúng tôi sẽ có một cái gì đó như:

//here we perform the same filter/map operation as in the F# example.
List<Option<Integer>> optionList = Arrays.asList(new Some<Integer>(1),new Some<Integer>(2),new None<Integer>(),new Some<Integer>(3),new None<Integer>());
List<Integer> filteredList = new ArrayList<Integer>();
for(Option<Integer> op : list)
    if(op instanceof Some)
        filteredList.add(((Some<Integer>)op).getValue());

Giải pháp thay thế như tin nhắn ăn nil

"Tin nhắn ăn nil" của Objective-C không phải là một giải pháp nhiều như một nỗ lực làm giảm bớt sự đau đầu của việc kiểm tra null. Về cơ bản, thay vì ném một ngoại lệ thời gian chạy khi cố gắng gọi một phương thức trên một đối tượng null, thì biểu thức thay vào đó tự đánh giá thành null. Tạm dừng sự hoài nghi, như thể mỗi phương thức bắt đầu bằng if (this == null) return null;. Nhưng sau đó, mất thông tin: bạn không biết liệu phương thức trả về null vì nó là giá trị trả về hợp lệ hay vì đối tượng thực sự là null. Nó rất giống như nuốt ngoại lệ, và không thực hiện bất kỳ tiến trình nào giải quyết các vấn đề với null được nêu ra trước đó.


Đây là một peeve vật nuôi nhưng c # hầu như không phải là một ngôn ngữ giống như c.
Roman A. Taycher

4
Tôi đã đi Java ở đây, vì C # có thể sẽ có một giải pháp đẹp hơn ... nhưng tôi đánh giá cao sự hiểu biết của bạn, điều mọi người thực sự muốn nói là "một ngôn ngữ với cú pháp lấy cảm hứng từ c". Tôi đã đi trước và thay thế tuyên bố "giống như c".
Stephen Swensen

Với linq, phải. Tôi đã nghĩ về c # và không nhận thấy điều đó.
Roman A. Taycher

1
Có với cú pháp lấy cảm hứng từ c, nhưng tôi nghĩ rằng tôi cũng đã nghe nói về các ngôn ngữ lập trình bắt buộc như python / ruby ​​với rất ít cách gọi cú pháp c giống như các lập trình viên chức năng gọi là c-like.
Roman A. Taycher

11

Hội mang đến cho chúng tôi địa chỉ còn được gọi là con trỏ không được đánh dấu. C đã ánh xạ chúng trực tiếp dưới dạng các con trỏ được gõ nhưng giới thiệu null của Algol là một giá trị con trỏ duy nhất, tương thích với tất cả các con trỏ được gõ. Vấn đề lớn với null trong C là vì mọi con trỏ đều có thể là null, nên người ta không bao giờ có thể sử dụng một con trỏ một cách an toàn mà không cần kiểm tra thủ công.

Trong các ngôn ngữ cấp cao hơn, việc có null là khó xử vì nó thực sự truyền tải hai khái niệm riêng biệt:

  • Nói rằng một cái gì đó là không xác định .
  • Nói rằng một cái gì đó là tùy chọn .

Có các biến không xác định là khá nhiều vô dụng, và dẫn đến hành vi không xác định bất cứ khi nào chúng xảy ra. Tôi cho rằng mọi người sẽ đồng ý rằng nên tránh những thứ không xác định bằng mọi giá.

Trường hợp thứ hai là tùy chọn và được cung cấp rõ ràng nhất, ví dụ với một loại tùy chọn .


Giả sử chúng ta đang ở trong một công ty vận tải và chúng ta cần tạo một ứng dụng để giúp tạo lịch trình cho các tài xế của mình. Đối với mỗi tài xế, chúng tôi lưu trữ một vài thông tin như: giấy phép lái xe họ có và số điện thoại để gọi trong trường hợp khẩn cấp.

Trong C chúng ta có thể có:

struct PhoneNumber { ... };
struct MotorbikeLicence { ... };
struct CarLicence { ... };
struct TruckLicence { ... };

struct Driver {
  char name[32]; /* Null terminated */
  struct PhoneNumber * emergency_phone_number;
  struct MotorbikeLicence * motorbike_licence;
  struct CarLicence * car_licence;
  struct TruckLicence * truck_licence;
};

Khi bạn quan sát, trong bất kỳ quá trình xử lý nào trong danh sách các trình điều khiển của chúng tôi, chúng tôi sẽ phải kiểm tra các con trỏ null. Trình biên dịch sẽ không giúp bạn, sự an toàn của chương trình dựa vào vai bạn.

Trong OCaml, cùng một mã sẽ như thế này:

type phone_number = { ... }
type motorbike_licence = { ... }
type car_licence = { ... }
type truck_licence = { ... }

type driver = {
  name: string;
  emergency_phone_number: phone_number option;
  motorbike_licence: motorbike_licence option;
  car_licence: car_licence option;
  truck_licence: truck_licence option;
}

Bây giờ chúng ta hãy nói rằng chúng tôi muốn in tên của tất cả các trình điều khiển cùng với số giấy phép xe tải của họ.

Trong C:

#include <stdio.h>

void print_driver_with_truck_licence_number(struct Driver * driver) {
  /* Check may be redundant but better be safe than sorry */
  if (driver != NULL) {
    printf("driver %s has ", driver->name);
    if (driver->truck_licence != NULL) {
      printf("truck licence %04d-%04d-%08d\n",
        driver->truck_licence->area_code
        driver->truck_licence->year
        driver->truck_licence->num_in_year);
    } else {
      printf("no truck licence\n");
    }
  }
}

void print_drivers_with_truck_licence_numbers(struct Driver ** drivers, int nb) {
  if (drivers != NULL && nb >= 0) {
    int i;
    for (i = 0; i < nb; ++i) {
      struct Driver * driver = drivers[i];
      if (driver) {
        print_driver_with_truck_licence_number(driver);
      } else {
        /* Huh ? We got a null inside the array, meaning it probably got
           corrupt somehow, what do we do ? Ignore ? Assert ? */
      }
    }
  } else {
    /* Caller provided us with erroneous input, what do we do ?
       Ignore ? Assert ? */
  }
}

Trong OCaml đó sẽ là:

open Printf

(* Here we are guaranteed to have a driver instance *)
let print_driver_with_truck_licence_number driver =
  printf "driver %s has " driver.name;
  match driver.truck_licence with
    | None ->
        printf "no truck licence\n"
    | Some licence ->
        (* Here we are guaranteed to have a licence *)
        printf "truck licence %04d-%04d-%08d\n"
          licence.area_code
          licence.year
          licence.num_in_year

(* Here we are guaranteed to have a valid list of drivers *)
let print_drivers_with_truck_licence_numbers drivers =
  List.iter print_driver_with_truck_licence_number drivers

Như bạn có thể thấy trong ví dụ tầm thường này, không có gì phức tạp trong phiên bản an toàn:

  • Nó khó chịu hơn.
  • Bạn nhận được đảm bảo tốt hơn nhiều và không cần kiểm tra null.
  • Trình biên dịch đảm bảo rằng bạn xử lý chính xác tùy chọn

Trong khi ở C, bạn có thể đã quên một kiểm tra null và bùng nổ ...

Lưu ý: các mẫu mã này không được biên dịch, nhưng tôi hy vọng bạn có ý tưởng.


Tôi chưa bao giờ thử nó nhưng en.wikipedia.org/wiki/Cyclone_%28programming_lingu%29 tuyên bố cho phép con trỏ không null cho c.
Roman A. Taycher

1
Tôi không đồng ý với tuyên bố của bạn rằng không ai quan tâm đến trường hợp đầu tiên. Nhiều người, đặc biệt là những người trong cộng đồng ngôn ngữ chức năng, cực kỳ quan tâm đến điều này và không khuyến khích hoặc hoàn toàn cấm sử dụng các biến chưa được khởi tạo.
Stephen Swensen

Tôi tin rằng NULLtrong "tài liệu tham khảo có thể không chỉ ra bất cứ điều gì" đã được phát minh cho một số ngôn ngữ Algol (Wikipedia đồng ý, xem en.wikipedia.org/wiki/Null_pulum#Null_pulum ). Nhưng tất nhiên, có khả năng các lập trình viên lắp ráp đã khởi tạo con trỏ của họ thành một địa chỉ không hợp lệ (đọc: Null = 0).

1
@Stephen: Có lẽ chúng tôi cũng có ý nghĩa tương tự. Đối với tôi, họ không khuyến khích hoặc cấm sử dụng những thứ chưa được khởi tạo một cách chính xác bởi vì không có lý do nào để thảo luận về những điều không xác định vì chúng ta không thể làm bất cứ điều gì lành mạnh hoặc hữu ích với chúng. Nó sẽ không có hứng thú gì.
bltxd

2
như @tc. nói, null không có gì để làm với lắp ráp. Trong lắp ráp, các loại thường không thể rỗng. Một giá trị được tải vào một thanh ghi mục đích chung có thể bằng 0 hoặc nó có thể là một số nguyên khác không. Nhưng nó không bao giờ có thể là null. Ngay cả khi bạn tải một địa chỉ bộ nhớ vào một thanh ghi, trên hầu hết các kiến ​​trúc phổ biến, không có đại diện riêng cho "con trỏ null". Đó là một khái niệm được giới thiệu bằng các ngôn ngữ cấp cao hơn, như C.
jalf

5

Microsoft Research có một dự án xen kẽ được gọi là

Thông số kỹ thuật #

Đó là một tiện ích mở rộng C # với loại không null và một số cơ chế để kiểm tra các đối tượng của bạn không bị rỗng , mặc dù vậy, IMHO, áp dụng thiết kế theo nguyên tắc hợp đồng có thể phù hợp hơn và hữu ích hơn cho nhiều tình huống rắc rối do tham chiếu null gây ra.


4

Đến từ nền tảng .NET, tôi luôn nghĩ null có một điểm, nó hữu ích. Cho đến khi tôi biết được các cấu trúc và cách dễ dàng làm việc với chúng tránh được rất nhiều mã soạn sẵn. Tony Hoare phát biểu tại QCon London năm 2009, đã xin lỗi vì đã phát minh ra tài liệu tham khảo null . Để báo cho anh ta:

Tôi gọi đó là sai lầm tỷ đô của tôi. Đó là phát minh của tài liệu tham khảo null vào năm 1965. Vào thời điểm đó, tôi đang thiết kế hệ thống loại toàn diện đầu tiên cho các tài liệu tham khảo bằng ngôn ngữ hướng đối tượng (ALGOL W). Mục tiêu của tôi là đảm bảo rằng tất cả việc sử dụng tài liệu tham khảo phải tuyệt đối an toàn, với việc kiểm tra được thực hiện tự động bởi trình biên dịch. Nhưng tôi không thể cưỡng lại sự cám dỗ để đưa vào một tài liệu tham khảo null, đơn giản vì nó rất dễ thực hiện. Điều này đã dẫn đến vô số lỗi, lỗ hổng và sự cố hệ thống, có thể gây ra hàng tỷ đô la đau đớn và thiệt hại trong bốn mươi năm qua. Trong những năm gần đây, một số máy phân tích chương trình như PREfix và PREfast trong Microsoft đã được sử dụng để kiểm tra các tài liệu tham khảo và đưa ra cảnh báo nếu có rủi ro chúng có thể không có giá trị. Các ngôn ngữ lập trình gần đây hơn như Spec # đã đưa ra các khai báo cho các tham chiếu không null. Đây là giải pháp, mà tôi đã từ chối vào năm 1965.

Xem câu hỏi này quá tại các lập trình viên



1

Tôi đã luôn xem Null (hoặc nil) là sự vắng mặt của một giá trị .

Đôi khi bạn muốn điều này, đôi khi bạn không. Nó phụ thuộc vào tên miền bạn đang làm việc. Nếu sự vắng mặt là có ý nghĩa: không có tên đệm, thì ứng dụng của bạn có thể hành động tương ứng. Mặt khác, nếu không có giá trị null: Tên đầu tiên là null, thì nhà phát triển nhận được cuộc gọi điện thoại 2 giờ sáng.

Tôi cũng đã thấy mã bị quá tải và quá phức tạp với việc kiểm tra null. Đối với tôi điều này có nghĩa là một trong hai điều:
a) một lỗi cao hơn trong cây ứng dụng
b) thiết kế xấu / không hoàn chỉnh

Về mặt tích cực - Null có lẽ là một trong những khái niệm hữu ích hơn để kiểm tra xem có thứ gì đó vắng mặt hay không, và các ngôn ngữ không có khái niệm null sẽ kết thúc những thứ quá phức tạp khi đến lúc phải xác thực dữ liệu. Trong trường hợp này, nếu một biến mới không được khởi tạo, các languagues đã nói thường sẽ đặt các biến thành một chuỗi rỗng, 0 hoặc một bộ sưu tập trống. Tuy nhiên, nếu một chuỗi rỗng hoặc 0 hoặc bộ sưu tập trống là các giá trị hợp lệ cho ứng dụng của bạn - thì bạn có vấn đề.

Đôi khi điều này phá vỡ bằng cách phát minh ra các giá trị đặc biệt / kỳ lạ cho các trường để thể hiện trạng thái chưa được khởi tạo. Nhưng sau đó, điều gì xảy ra khi giá trị đặc biệt được nhập bởi người dùng có thiện chí? Và chúng ta đừng đi vào mớ hỗn độn này sẽ tạo thành thói quen xác thực dữ liệu. Nếu ngôn ngữ hỗ trợ khái niệm null, tất cả các mối quan tâm sẽ biến mất.


Xin chào @Jon, hơi khó khăn khi theo dõi bạn ở đây. Cuối cùng tôi đã nhận ra rằng bằng các giá trị "đặc biệt / kỳ lạ", bạn có thể có nghĩa gì đó như 'không xác định' của Javascript hoặc 'NaN' của Javascript. Nhưng bên cạnh đó, bạn không thực sự giải quyết bất kỳ câu hỏi nào mà OP đã hỏi. Và tuyên bố rằng "Null có lẽ là khái niệm hữu ích nhất để kiểm tra nếu có thứ gì đó vắng mặt" gần như chắc chắn là sai. Các loại tùy chọn là một lựa chọn được đánh giá tốt, an toàn cho loại không.
Stephen Swensen

@Stephen - Thực sự nhìn lại tin nhắn của tôi, tôi nghĩ rằng toàn bộ nửa thứ hai nên được chuyển sang một câu hỏi chưa được hỏi. Nhưng tôi vẫn nói null rất hữu ích cho việc kiểm tra xem có thứ gì vắng mặt không.
Jon

0

Ngôn ngữ vectơ đôi khi có thể thoát khỏi mà không có null.

Các vector trống phục vụ như là một null gõ trong trường hợp này.


Tôi nghĩ rằng tôi hiểu những gì bạn đang nói về nhưng bạn có thể liệt kê một số ví dụ? Đặc biệt là áp dụng nhiều chức năng cho một giá trị có thể null?
Roman A. Taycher

Áp dụng tốt một biến đổi vectơ cho một vectơ trống dẫn đến một vectơ trống khác. FYI, SQL chủ yếu là một ngôn ngữ vector.
Joshua

1
OK tôi làm rõ hơn rằng. SQL là ngôn ngữ vectơ cho các hàng và ngôn ngữ giá trị cho các cột.
Joshua
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.