2009-03-13 30 views
7

Tôi tiếp tục nghe mọi người nói về cách các loại tham chiếu không thể vô hiệu hóa sẽ giải quyết rất nhiều lỗi và lập trình dễ dàng hơn nhiều. Ngay cả những người sáng tạo của null gọi nó là billion dollar mistake của mình, và Spec# đã giới thiệu các loại không nullable để chống lại vấn đề này.Giới thiệu về các cuộc tranh luận loại không thể vô hiệu hóa

EDIT: Bỏ qua nhận xét của tôi về SpeC#. Tôi hiểu lầm cách nó hoạt động.

EDIT 2: Tôi phải nói chuyện với những người sai, tôi đã thực sự hy vọng cho ai đó để tranh luận với :-)


Vì vậy, tôi sẽ đoán, đang trong thiểu số, rằng tôi sai, nhưng tôi không thể hiểu tại sao cuộc tranh luận này có bất kỳ công đức nào. Tôi thấy null là một công cụ tìm lỗi. Hãy xem xét những điều sau:

class Class { ... } 

void main() { 
    Class c = nullptr; 
    // ... ... ... code ... 
    for(int i = 0; i < c.count; ++i) { ... } 
} 

BAM! Vi phạm quyền truy cập. Ai đó đã quên khởi chạy c.


Bây giờ xem xét việc này:

class Class { ... } 

void main() { 
    Class c = new Class(); // set to new Class() by default 
    // ... ... ... code ... 
    for(int i = 0; i < c.count; ++i) { ... } 
} 

Whoops. Các vòng lặp được âm thầm bỏ qua. Có thể mất một lúc để theo dõi vấn đề.


Nếu lớp học của bạn trống, mã sẽ không thành công. Tại sao không có hệ thống cho bạn biết (mặc dù hơi thô lỗ) thay vì phải tự mình tìm ra?

+0

Tốt để xem những người khác được hưởng không, tôi vẫn còn ở trường nên tôi chỉ cho rằng có cái gì đó tôi đang thiếu. –

+1

Có nhiều cách xử lý nguyên tắc hơn "không có giá trị". NULL loại trừ các kiểu nguyên thủy, chẳng hạn như int. Nó tốt hơn cho một hệ thống kiểu đại diện cho việc thiếu giá trị nhất quán trên tất cả các loại, thay vì chỉ ngầm cho các tham chiếu. Xem các tùy chọn "Có thể" và ML/OCaml/F # của Haskell "" để xem nó nên được thực hiện như thế nào. – MichaelGG

+0

có thể trùng lặp [Giải thích tốt nhất cho các ngôn ngữ không có giá trị rỗng] (http://stackoverflow.com/questions/3989264/best-explanation-for-languages-without-null) – nawfal

Trả lời

-2

Tôi thích sử dụng NULL. Nó đã được một thời gian kể từ khi tôi đã làm việc trong C + +, nhưng nó đã làm cho nó rất dễ dàng để tìm thấy vấn đề của tôi liên quan đến giá trị trả lại và tài liệu tham khảo và sửa chữa chúng.

+0

Bỏ lỡ điểm. –

2

Tôi thừa nhận rằng tôi chưa thực sự đọc nhiều về SpeC#, nhưng tôi đã hiểu rằng NonNullable cơ bản là thuộc tính mà bạn đặt vào tham số, không nhất thiết phải khai báo biến; Biến ví dụ của bạn thành một cái gì đó như:

class Class { ... } 

void DoSomething(Class c) 
{ 
    if (c == null) return; 
    for(int i = 0; i < c.count; ++i) { ... } 
} 

void main() { 
    Class c = nullptr; 
    // ... ... ... code ... 
    DoSomething(c); 
} 

Với SpeC#, bạn đang đánh dấu doSomething để nói "tham số c không thể rỗng". Điều đó có vẻ như một tính năng tốt để có với tôi, vì nó có nghĩa là tôi không cần dòng đầu tiên trong phương thức DoSomething() (mà là một dòng dễ quên, và hoàn toàn vô nghĩa với bối cảnh của DoSomething()).

6

Tôi không hiểu ví dụ của bạn. Nếu "= new Class()" của bạn chỉ là một trình giữ chỗ thay cho không có null, thì đó là (đối với mắt của tôi) rõ ràng là một lỗi. Nếu không, thì lỗi thực sự là "..." đã không đặt chính xác nội dung của nó, điều này hoàn toàn giống nhau trong cả hai trường hợp.

Trường hợp ngoại lệ cho bạn thấy rằng bạn đã quên khởi chạy c sẽ cho bạn biết tại thời điểm nó không được khởi tạo, nhưng không phải nơi khởi tạo. Tương tự như vậy, một vòng lặp bỏ lỡ sẽ (ngầm) cho bạn biết nơi cần thiết để có một số không. 0, nhưng không phải những gì cần phải được thực hiện hoặc ở đâu. Tôi không thấy một trong hai cách dễ dàng hơn trên lập trình viên.

Tôi không nghĩ rằng điểm "không có giá trị" là chỉ cần thực hiện tìm kiếm và thay thế văn bản và biến chúng thành các phiên bản trống. Điều đó rõ ràng là vô ích.Vấn đề là cấu trúc mã của bạn để các biến của bạn không bao giờ ở trạng thái mà chúng trỏ đến các giá trị vô dụng/không chính xác, trong đó NULL đơn giản là phổ biến nhất.

+0

Vì vậy, nếu một ngôn ngữ thực hiện các loại không nullable, một tuyên bố như "Class c;" đặt c thành? (Tôi hoàn toàn đồng ý với ý kiến ​​của bạn bằng cách này) – zildjohn01

+2

Suy nghĩ đầu tiên của tôi: nó không nên được cho phép. Về mặt logic nó không có ý nghĩa gì khi nói "tạo một khe mà phải lưu trữ một Foo (nhưng để trống)".Điều này có thể yêu cầu tất cả mọi thứ-là-một-biểu hiện (như Lisp hoặc Ruby) - Tôi không biết làm thế nào nó sẽ làm việc nếu bạn đã phải thực hiện các câu lệnh tuần tự để làm nhiệm vụ. – Ken

+1

Nhìn lại: hãy suy nghĩ về SQL. Nếu bạn có một cột "NOT NULL" và cố gắng chèn một bản ghi mà không chỉ định một giá trị không null cho nó, nó chỉ đơn giản là làm tăng một lỗi. – Ken

0

Như tôi thấy nó có hai khu vực mà null được sử dụng.

Đầu tiên là thiếu giá trị. Ví dụ, một boolean có thể đúng hoặc sai, hoặc người dùng vẫn chưa chọn một thiết lập, do đó null. Điều này là hữu ích và là một điều tốt, nhưng có lẽ đã được thực hiện không chính xác ban đầu và bây giờ có một nỗ lực để chính thức hóa việc sử dụng đó. (Có nên có một boolean thứ hai để giữ trạng thái thiết lập/bỏ đặt, hoặc null như là một phần của một logic ba trạng thái?)

Thứ hai là theo ý nghĩa của con trỏ null. Điều này thường không phải là một tình huống lỗi chương trình, tức là. một ngoại lệ. Nó không phải là một trạng thái dự định, có một lỗi chương trình. Điều này nên được theo các trường hợp ngoại lệ chính thức, như được thực hiện trong các ngôn ngữ hiện đại. Đó là, một NullException bị bắt qua một khối try/catch.

Vì vậy, bạn quan tâm đến điều nào trong số này?

+0

# 2. – zildjohn01

2

Ý tưởng về các loại không null là để trình biên dịch thay vì ứng dụng khách tìm lỗi. Giả sử bạn thêm vào ngôn ngữ của bạn hai loại specifiers @nullable (có thể là null) và @nonnull (không bao giờ null) (Tôi đang sử dụng cú pháp chú thích Java).

Khi bạn xác định một hàm, bạn chú thích các đối số của nó. Ví dụ, đoạn mã sau sẽ biên dịch

int f(@nullable Foo foo) { 
    if (foo == null) 
    return 0; 
    return foo.size(); 
}

Mặc dù foo có thể được null tại lối vào, dòng chảy của bảo đảm kiểm soát, khi bạn gọi foo.size(), foo là nonnull.

Nhưng nếu bạn loại bỏ dấu kiểm cho giá trị rỗng, bạn sẽ nhận được lỗi biên dịch.

Sau đây sẽ biên dịch quá vì foo là nonnull tại mục:

int g(@nonnull Foo foo) { 
    return foo.size(); // OK 
}

Tuy nhiên, bạn sẽ không thể gọi g với một con trỏ nullable:

@nullable Foo foo; 
g(foo); // compiler error!

Trình biên dịch không phân tích dòng chảy cho mọi chức năng, vì vậy nó có thể phát hiện khi @nullable trở thành @nonnull (ví dụ, bên trong một câu lệnh if kiểm tra null). Nó cũng sẽ chấp nhận một định nghĩa có thể xác minh @nonnull được cung cấp ngay lập tức.

@nonnull Foo foo = new Foo();

Có nhiều hơn nữa về chủ đề này trong my blog.

0

Tôi hiện đang làm việc về chủ đề này trong C#. .NET có Nullable cho các kiểu giá trị, nhưng tính năng nghịch đảo không tồn tại đối với các kiểu tham chiếu.

Tôi đã tạo NotNullable cho các loại tham chiếu và di chuyển vấn đề từ if's (không còn kiểm tra null) vào miền datatype. Điều này làm cho ứng dụng đưa ra các ngoại lệ trong thời gian chạy và không phải trong thời gian biên dịch.

12

của nó một chút lẻ rằng phản ứng đánh dấu "câu trả lời" trong chủ đề này thực sự làm nổi bật vấn đề với null ở nơi đầu tiên, cụ thể là:

Tôi cũng phát hiện ra rằng hầu hết NULL lỗi con trỏ của tôi xoay quanh các hàm do quên để kiểm tra các hàm trả về của chuỗi.h, trong đó NULL được sử dụng làm chỉ báo.

Sẽ không tốt nếu trình biên dịch có thể bắt các loại lỗi này tại thời gian biên dịch, thay vì thời gian chạy?

Nếu bạn đã sử dụng ngôn ngữ giống như ML (SML, OCaml, SML và F # ở một mức độ nào đó) hoặc Haskell, các loại tham chiếu không thể rỗng. Thay vào đó, bạn đại diện cho một giá trị "null" bằng cách gói nó một kiểu tùy chọn. Bằng cách này, bạn thực sự thay đổi kiểu trả về của một hàm nếu nó có thể trả về null dưới dạng giá trị pháp lý. Vì vậy, chúng ta hãy nói rằng tôi muốn kéo người dùng ra khỏi cơ sở dữ liệu:

let findUser username = 
    let recordset = executeQuery("select * from users where username = @username") 
    if recordset.getCount() > 0 then 
     let user = initUser(recordset) 
     Some(user) 
    else 
     None 

Find dùng có kiểu val findUser : string -> user option, vì vậy các kiểu trả về của hàm thực sự nói với bạn rằng nó có thể trả về giá trị null. Để sử dụng mã, bạn cần xử lý cả hai trường hợp Một số và Không có:

match findUser "Juliet Thunderwitch" with 
| Some x -> print_endline "Juliet exists in database" 
| None -> print_endline "Juliet not in database" 

Nếu bạn không xử lý cả hai trường hợp, mã sẽ không biên dịch được. Vì vậy, hệ thống kiểu đảm bảo rằng bạn sẽ không bao giờ nhận được một ngoại lệ tham chiếu null và nó đảm bảo rằng bạn luôn xử lý các giá trị rỗng. Và nếu một hàm trả về user, thì hàm được bảo đảm là một thể hiện thực tế của một đối tượng. Awesomeness.

Bây giờ chúng ta thấy vấn đề trong mã mẫu của OP:

class Class { ... } 

void main() { 
    Class c = new Class(); // set to new Class() by default 
    // ... ... ... code ... 
    for(int i = 0; i < c.count; ++i) { ... } 
} 

khởi tạo và đối tượng chưa được khởi tạo có kiểu dữ liệu tương tự, bạn không thể biết sự khác biệt giữa chúng. Đôi khi, null object pattern có thể hữu ích, nhưng đoạn mã trên cho thấy trình biên dịch không có cách nào để xác định xem bạn có đang sử dụng đúng loại của mình hay không.

0

Loại không thể vô hiệu hóa có ý nghĩa hơn đối với tôi khi chúng ta đang xử lý các đối tượng miền. Khi bạn đang ánh xạ các bảng cơ sở dữ liệu cho các đối tượng và bạn có các cột không có giá trị rỗng. Giả sử bạn có một bảng gọi là User và nó có cột userid varchar (20) không nullable;

Điều đó sẽ rất tiện lợi khi có lớp Người dùng với trường chuỗi UserId không thể bỏ qua. Bạn cab giảm một số lỗi tại thời gian biên dịch.