Làm thế nào để ngôn ngữ với các loại Có thể thay vì null xử lý các điều kiện cạnh?


53

Eric Lippert đã đưa ra một điểm rất thú vị trong cuộc thảo luận về lý do tại sao C # sử dụng nullchứ không phải là một Maybe<T>loại :

Tính nhất quán của hệ thống loại là quan trọng; chúng ta có thể luôn biết rằng một tài liệu tham khảo không có giá trị không bao giờ trong bất kỳ trường hợp nào được quan sát là không hợp lệ? Điều gì về hàm tạo của một đối tượng với trường tham chiếu không thể rỗng của kiểu tham chiếu? Thế còn trong phần hoàn thiện của một đối tượng như vậy, trong đó đối tượng được hoàn thành bởi vì mã được cho là điền vào tham chiếu đã ném một ngoại lệ? Một hệ thống loại nói dối với bạn về sự đảm bảo của nó là nguy hiểm.

Đó là một chút mở mắt. Các khái niệm liên quan đến tôi và tôi đã thực hiện một số cách chơi với trình biên dịch và hệ thống loại, nhưng tôi chưa bao giờ nghĩ về kịch bản đó. Làm thế nào để các ngôn ngữ có loại Có thể thay vì null xử lý các trường hợp cạnh như khởi tạo và khôi phục lỗi, trong đó trên thực tế, một tham chiếu được cho là không được bảo đảm là không ở trạng thái hợp lệ?


Tôi đoán nếu có lẽ là một phần của ngôn ngữ thì có thể nó được triển khai nội bộ thông qua một con trỏ null và nó chỉ là đường cú pháp. Nhưng tôi không nghĩ bất kỳ ngôn ngữ nào thực sự thích nó.
panzi

1
@panzi: Ceylon sử dụng kiểu gõ nhạy cảm với dòng chảy để phân biệt giữa Type?(có thể) và Type(không phải null)
Lukas Eder

1
@RobertHarvey Không có nút "câu hỏi hay" trong Stack Exchange?
dùng253751

2
@panzi Đó là một tối ưu hóa hợp lệ và tốt đẹp, nhưng nó không giúp ích gì cho vấn đề này: Khi một cái gì đó không phải là Maybe Tnó, thì nó không phải Nonevà do đó bạn không thể khởi tạo bộ lưu trữ của nó thành con trỏ null.

@immibis: Tôi đã đẩy nó rồi. Chúng tôi nhận được một vài câu hỏi hay ở đây; Tôi nghĩ rằng điều này xứng đáng nhận xét.
Robert Harvey

Câu trả lời:


45

Trích dẫn đó chỉ ra một vấn đề xảy ra nếu việc khai báo và gán định danh (ở đây: các thành viên thể hiện) tách biệt với nhau. Như một bản phác thảo mã giả nhanh chóng:

class Broken {
    val foo: Foo  // where Foo and Bar are non-nullable reference types
    val bar: Bar

    Broken() {
        foo = new Foo()
        throw new Exception()
        // this code is never reached, so "bar" is not assigned
        bar = new Bar()
    }

    ~Broken() {
        foo.cleanup()
        bar.cleanup()
    }
}

Kịch bản bây giờ là trong quá trình xây dựng một thể hiện, một lỗi sẽ được đưa ra, do đó việc xây dựng sẽ bị hủy bỏ trước khi thể hiện được xây dựng đầy đủ. Ngôn ngữ này cung cấp một phương thức hủy bỏ sẽ chạy trước khi bộ nhớ được giải phóng, ví dụ như để giải phóng tài nguyên không phải bộ nhớ. Nó cũng phải được chạy trên các đối tượng được xây dựng một phần, bởi vì các tài nguyên được quản lý thủ công có thể đã được phân bổ trước khi việc xây dựng bị hủy bỏ.

Với null, hàm hủy có thể kiểm tra xem một biến đã được gán như thế nào chưa if (foo != null) foo.cleanup(). Không có null, đối tượng hiện ở trạng thái không xác định - giá trị của là bargì?

Tuy nhiên, vấn đề này tồn tại do sự kết hợp của ba khía cạnh:

  • Sự vắng mặt của các giá trị mặc định như nullhoặc khởi tạo được đảm bảo cho các biến thành viên.
  • Sự khác biệt giữa khai báo và chuyển nhượng. Buộc các biến được gán ngay lập tức (ví dụ với một letcâu lệnh như đã thấy trong các ngôn ngữ chức năng) là một cách dễ dàng để buộc khởi tạo được đảm bảo - nhưng hạn chế ngôn ngữ theo những cách khác.
  • Hương vị cụ thể của các hàm hủy như là một phương thức được gọi bởi thời gian chạy ngôn ngữ.

Thật dễ dàng để chọn một thiết kế khác không thể hiện những vấn đề này, ví dụ bằng cách luôn kết hợp khai báo với gán và có ngôn ngữ cung cấp nhiều khối hoàn thiện thay vì một phương thức hoàn thiện duy nhất:

// the body of the class *is* the constructor
class Working() {
    val foo: Foo = new Foo()
    FINALIZE { foo.cleanup() }  // block is registered to run when object is destroyed

    throw new Exception()

    // the below code is never reached, so
    //  1. the "bar" variable never enters the scope
    //  2. the second finalizer block is never registered.
    val bar: Bar = new Bar()
    FINALIZE { bar.cleanup() }  // block is registered to run when object is destroyed
}

Vì vậy, không có vấn đề gì với sự vắng mặt của null, nhưng với sự kết hợp một tập hợp các tính năng khác mà không có null.

Câu hỏi thú vị bây giờ là tại sao C # chọn một thiết kế mà không phải thiết kế kia. Ở đây, ngữ cảnh của trích dẫn liệt kê nhiều đối số khác cho null trong ngôn ngữ C #, có thể được tóm tắt chủ yếu là sự quen thuộc và khả năng tương thích của Hồi giáo - và đó là những lý do chính đáng.


Ngoài ra còn có một lý do khác khiến bộ hoàn thiện phải xử lý nulls: thứ tự hoàn thiện không được đảm bảo, vì khả năng của các chu kỳ tham chiếu. Nhưng tôi đoán FINALIZEthiết kế của bạn cũng giải quyết được rằng: nếu foođã được hoàn thiện, FINALIZEphần của nó sẽ không chạy.
Svick

14

Các cách tương tự mà bạn đảm bảo bất kỳ dữ liệu nào khác đều ở trạng thái hợp lệ.

Người ta có thể cấu trúc ngữ nghĩa và luồng điều khiển sao cho bạn không thể có biến / trường thuộc loại nào đó mà không tạo đầy đủ giá trị cho nó. Thay vì tạo một đối tượng và để một hàm tạo gán các giá trị "ban đầu" cho các trường của nó, bạn chỉ có thể tạo một đối tượng bằng cách chỉ định các giá trị cho tất cả các trường của nó cùng một lúc. Thay vì khai báo một biến và sau đó gán một giá trị ban đầu, bạn chỉ có thể giới thiệu một biến có khởi tạo.

Ví dụ, trong Rust, bạn tạo một đối tượng có kiểu cấu trúc thông qua Point { x: 1, y: 2 }thay vì viết một hàm tạo self.x = 1; self.y = 2;. Tất nhiên, điều này có thể xung đột với phong cách ngôn ngữ bạn có trong tâm trí.

Một cách tiếp cận khác, bổ sung là sử dụng phân tích độ bền để ngăn truy cập vào bộ lưu trữ trước khi khởi tạo. Điều này cho phép khai báo một biến mà không cần khởi tạo ngay lập tức, miễn là nó được gán trước khi đọc lần đầu tiên. Nó cũng có thể bắt một số trường hợp liên quan đến thất bại như

Object o;
try {
    call_can_throw();
    o = new Object();
} catch {}
use(o);

Về mặt kỹ thuật, bạn cũng có thể định nghĩa một khởi tạo mặc định tùy ý cho các đối tượng, ví dụ bằng không tất cả các trường số, tạo các mảng trống cho các trường mảng, v.v. nhưng điều này khá tùy tiện, kém hiệu quả hơn các tùy chọn khác và có thể che giấu các lỗi.


7

Đây là cách Haskell thực hiện: (không chính xác là đối trọng với các tuyên bố của Lippert vì Haskell không phải là ngôn ngữ hướng đối tượng).

CẢNH BÁO: câu trả lời dài dòng từ một fanboy Haskell nghiêm túc phía trước.

TL; DR

Ví dụ này minh họa chính xác Haskell khác với C # như thế nào. Thay vì ủy thác hậu cần xây dựng kết cấu cho một nhà xây dựng, nó phải được xử lý theo mã xung quanh. Không có cách nào để giá trị null (Hoặc Nothingtrong Haskell) tăng lên khi chúng ta đang mong đợi một giá trị khác null vì các giá trị null chỉ có thể xảy ra trong các loại trình bao bọc đặc biệt được gọi Maybelà không thể hoán đổi với / chuyển đổi trực tiếp thành thông thường, không các loại nullable. Để sử dụng một giá trị không thể thực hiện được bằng cách gói nó vào một Maybe, trước tiên chúng ta phải trích xuất giá trị bằng cách sử dụng khớp mẫu, điều này buộc chúng ta phải chuyển hướng điều khiển vào một nhánh nơi chúng ta biết chắc chắn rằng chúng ta có giá trị không null.

Vì thế:

chúng ta có thể luôn biết rằng một tài liệu tham khảo không có giá trị không bao giờ trong bất kỳ trường hợp nào được quan sát là không hợp lệ?

Đúng. IntMaybe Intlà hai loại hoàn toàn riêng biệt. Tìm Nothingtrong một đồng bằng Intsẽ tương đương với việc tìm chuỗi "cá" trong một Int32.

Điều gì về hàm tạo của một đối tượng với trường tham chiếu không thể rỗng của kiểu tham chiếu?

Không thành vấn đề: các nhà xây dựng giá trị trong Haskell không thể làm gì ngoài việc lấy các giá trị họ đưa ra và đặt chúng lại với nhau. Tất cả logic khởi tạo diễn ra trước khi hàm tạo được gọi.

Thế còn trong phần hoàn thiện của một đối tượng như vậy, trong đó đối tượng được hoàn thành bởi vì mã được cho là điền vào tham chiếu đã ném một ngoại lệ?

Không có người hoàn thiện trong Haskell, vì vậy tôi thực sự không thể giải quyết điều này. Phản ứng đầu tiên của tôi vẫn đứng, tuy nhiên.

Trả lời đầy đủ :

Haskell không có null và sử dụng Maybekiểu dữ liệu để biểu thị nullables. Có thể là một kiểu dữ liệu đại số được định nghĩa như thế này:

data Maybe a = Just a | Nothing

Đối với những người không quen thuộc với Haskell, hãy đọc phần này là "A Maybelà a Nothinghoặc a Just a". Đặc biệt:

  • Maybelà hàm tạo kiểu : có thể nghĩ (không chính xác) là một lớp chung (trong đó alà biến kiểu). Sự tương tự C # là class Maybe<a>{}.
  • Justlà một hàm tạo giá trị : nó là một hàm lấy một đối số của kiểu avà trả về một giá trị của kiểu Maybe achứa giá trị. Vì vậy, mã x = Just 17tương tự như int? x = 17;.
  • Nothinglà một hàm tạo giá trị khác, nhưng nó không có đối số và Maybetrả về không có giá trị nào ngoài "Không có gì". x = Nothingtương tự như int? x = null;(giả sử chúng ta hạn chế atrong Haskell Int, điều này có thể được thực hiện bằng cách viết x = Nothing :: Maybe Int).

Bây giờ, những điều cơ bản của Maybeloại này đã được đưa ra, làm thế nào để Haskell tránh được các vấn đề được thảo luận trong câu hỏi của OP?

Chà, Haskell thực sự khác biệt với hầu hết các ngôn ngữ được thảo luận cho đến nay, vì vậy tôi sẽ bắt đầu bằng cách giải thích một vài nguyên tắc ngôn ngữ cơ bản.

Trước hết, ở Haskell, mọi thứ đều bất biến . Mọi điều. Các tên đề cập đến các giá trị, không phải các vị trí bộ nhớ nơi các giá trị có thể được lưu trữ (chỉ riêng điều này là một nguồn loại bỏ lỗi rất lớn). Không giống như trong C #, nơi biến khai và phân công là hai hoạt động riêng biệt, trong các giá trị Haskell được tạo ra bằng cách định nghĩa giá trị của họ (ví dụ như x = 15, y = "quux", z = Nothing), mà không bao giờ có thể thay đổi. Do đó, mã như:

ReferenceType x;

Không thể có trong Haskell. Không có vấn đề với việc khởi tạo giá trị nullbởi vì mọi thứ phải được khởi tạo rõ ràng thành một giá trị để nó tồn tại.

Thứ hai, Haskell không phải là một ngôn ngữ hướng đối tượng : nó là một ngôn ngữ chức năng thuần túy , vì vậy không có đối tượng theo nghĩa chặt chẽ của từ này. Thay vào đó, có các hàm đơn giản (các hàm tạo giá trị) lấy các đối số của chúng và trả về một cấu trúc hợp nhất.

Tiếp theo, hoàn toàn không có mã kiểu bắt buộc. Bằng cách này, tôi có nghĩa là hầu hết các ngôn ngữ theo một mô hình như thế này:

do thing 1
add thing 2 to thing 3
do thing 4
if thing 5:
    do thing 6
return thing 7

Hành vi chương trình được thể hiện như một loạt các hướng dẫn. Trong các ngôn ngữ hướng đối tượng, khai báo lớp và hàm cũng đóng một vai trò rất lớn trong luồng chương trình, nhưng về bản chất, "thịt" của việc thực hiện chương trình có dạng một loạt các lệnh được thực thi.

Trong Haskell, điều này là không thể. Thay vào đó, luồng chương trình được quyết định hoàn toàn bởi các hàm chuỗi. Ngay cả chú thích bắt buộc docũng chỉ là đường cú pháp để truyền các hàm ẩn danh cho >>=toán tử. Tất cả các chức năng có dạng:

<optional explicit type signature>
functionName arg1 arg2 ... argn = body-expression

Nơi body-expressioncó thể là bất cứ điều gì đánh giá một giá trị. Rõ ràng là có nhiều tính năng cú pháp có sẵn nhưng điểm chính là sự vắng mặt hoàn toàn của chuỗi các câu lệnh.

Cuối cùng, và có lẽ là quan trọng nhất, hệ thống loại của Haskell cực kỳ nghiêm ngặt. Nếu tôi phải tóm tắt triết lý thiết kế trung tâm của hệ thống loại Haskell, tôi sẽ nói: "Làm cho càng nhiều thứ càng tốt sai trong thời gian biên dịch sao cho càng ít càng tốt khi chạy sai." Không có chuyển đổi ngầm định nào (muốn quảng cáo Intđến một Double? Sử dụng fromIntegralchức năng). Điều duy nhất có thể có một giá trị không hợp lệ xảy ra trong thời gian chạy là sử dụng Prelude.undefined(dường như chỉ cần có ở đó và không thể xóa ).

Với tất cả những điều này, hãy xem ví dụ "bị hỏng" của amon và cố gắng diễn đạt lại mã này trong Haskell. Đầu tiên, khai báo dữ liệu (sử dụng cú pháp bản ghi cho các trường được đặt tên):

data NotSoBroken = NotSoBroken {foo :: Foo, bar :: Bar } 

( foobarthực sự là các hàm truy cập vào các trường ẩn danh ở đây thay vì các trường thực tế, nhưng chúng ta có thể bỏ qua chi tiết này).

Hàm tạo NotSoBrokengiá trị không có khả năng thực hiện bất kỳ hành động nào ngoài việc thực hiện a Foovà a Bar(không thể rỗng) và tạo NotSoBrokenra chúng. Không có nơi nào để đặt mã mệnh lệnh hoặc thậm chí gán các trường theo cách thủ công. Tất cả logic khởi tạo phải diễn ra ở nơi khác, rất có thể là trong một chức năng nhà máy chuyên dụng.

Trong ví dụ, việc xây dựng Brokenluôn luôn thất bại. Không có cách nào để phá vỡ hàm tạo NotSoBrokengiá trị theo cách tương tự (đơn giản là không có nơi nào để viết mã), nhưng chúng ta có thể tạo một hàm xuất xưởng bị lỗi tương tự.

makeNotSoBroken :: Foo -> Bar -> Maybe NotSoBroken
makeNotSoBroken foo bar = Nothing

(dòng đầu tiên là một khai báo chữ ký loại: makeNotSoBrokenlấy a Foovà a Barlàm đối số và tạo ra a Maybe NotSoBroken).

Kiểu trả về phải Maybe NotSoBrokenvà không chỉ đơn giản NotSoBrokenvì chúng tôi đã bảo nó đánh giá Nothing, đó là một hàm tạo giá trị cho Maybe. Các loại chỉ đơn giản là không xếp hàng nếu chúng tôi viết bất cứ điều gì khác nhau.

Ngoài việc hoàn toàn vô nghĩa, chức năng này thậm chí còn không hoàn thành mục đích thực sự của nó, như chúng ta sẽ thấy khi chúng ta cố gắng sử dụng nó. Hãy tạo một hàm gọi useNotSoBrokenmà hy vọng một NotSoBrokennhư một cuộc tranh cãi:

useNotSoBroken :: NotSoBroken -> Whatever

( useNotSoBrokenchấp nhận một NotSoBrokenđối số và tạo ra một Whatever).

Và sử dụng nó như vậy:

useNotSoBroken (makeNotSoBroken)

Trong hầu hết các ngôn ngữ, loại hành vi này có thể gây ra ngoại lệ con trỏ null. Trong Haskell, các loại không khớp với nhau: makeNotSoBrokentrả về a Maybe NotSoBroken, nhưng useNotSoBrokenmong đợi a NotSoBroken. Các loại này không thể thay thế cho nhau và mã không thể biên dịch.

Để giải quyết vấn đề này, chúng ta có thể sử dụng một casecâu lệnh để phân nhánh dựa trên cấu trúc của Maybegiá trị (sử dụng một tính năng được gọi là khớp mẫu ):

case makeNotSoBroken of
    Nothing  -> --handle situation here
    (Just x) -> useNotSoBroken x

Rõ ràng đoạn trích này cần được đặt bên trong một số bối cảnh để thực sự biên dịch, nhưng nó cho thấy những điều cơ bản về cách Haskell xử lý nullables. Dưới đây là giải thích từng bước của đoạn mã trên:

  • Đầu tiên, makeNotSoBrokenđược đánh giá, được đảm bảo để tạo ra một giá trị của loại Maybe NotSoBroken.
  • Các casetuyên bố kiểm tra cấu trúc của giá trị này.
  • Nếu giá trị là Nothing, mã "xử lý tình huống ở đây" được ước tính.
  • Nếu giá trị thay vào đó khớp với một Justgiá trị, nhánh khác được thực thi. Lưu ý cách mệnh đề khớp đồng thời xác định giá trị là một Justcấu trúc và liên kết NotSoBrokentrường bên trong của nó với một tên (trong trường hợp này x). xsau đó có thể được sử dụng như NotSoBrokengiá trị bình thường .

Vì vậy, khớp mẫu cung cấp một phương tiện mạnh mẽ để thực thi an toàn kiểu, vì cấu trúc của đối tượng gắn liền với sự phân nhánh của điều khiển.

Tôi hy vọng đây là một lời giải thích dễ hiểu. Nếu nó không có ý nghĩa, hãy nhảy vào Learn You A Haskell For Great Good! , một trong những hướng dẫn ngôn ngữ trực tuyến tốt nhất mà tôi từng đọc. Hy vọng bạn sẽ thấy vẻ đẹp tương tự trong ngôn ngữ này mà tôi làm.


TL; DR phải ở trên cùng :)
andrew.fox

@ andrew.fox Điểm tốt. Tôi sẽ chỉnh sửa.
Tiếp

0

Tôi nghĩ rằng trích dẫn của bạn là một đối số người rơm.

Các ngôn ngữ hiện đại ngày nay (bao gồm C #), đảm bảo với bạn rằng hàm tạo hoàn thành đầy đủ hoặc không.

Nếu có một ngoại lệ trong hàm tạo và đối tượng bị bỏ lại một phần chưa được khởi tạo, thì việc có nullhoặc Maybe::nonecho trạng thái chưa khởi tạo sẽ không có sự khác biệt thực sự trong mã hủy.

Bạn sẽ chỉ phải đối phó với nó một trong hai cách. Khi có tài nguyên bên ngoài để quản lý, bạn phải quản lý chúng một cách rõ ràng bằng mọi cách. Ngôn ngữ và thư viện có thể giúp đỡ, nhưng bạn sẽ phải suy nghĩ một chút về vấn đề này.

Btw: Trong C #, nullgiá trị tương đối nhiều Maybe::none. Bạn chỉ có thể gán nullcho các biến và thành viên đối tượng ở mức độ được khai báo là nullable :

String? nullableString = getOptionalString();
Nullable<String> maybe = nullableString; // This is equivalent

Điều này không có gì khác so với đoạn trích sau:

Maybe<String> optionalString = getOptionalString();

Vì vậy, để kết luận, tôi không thấy sự vô hiệu trong bất kỳ cách nào đối nghịch với Maybecác loại. Tôi thậm chí sẽ đề nghị rằng C # đã lẻn vào Maybeloại của chính nó và gọi nó Nullable<T>.

Với các phương thức mở rộng, thậm chí còn dễ dàng để dọn sạch Nullable theo mô hình đơn nguyên:

Resource? resource = initializationThatMayFail();
...
resource.ifExists( Resource r -> r.cleanup() );

2
điều đó có nghĩa là gì, "constructor hoặc hoàn thành đầy đủ hoặc không"? Ví dụ, trong Java, việc khởi tạo trường (không phải là cuối cùng) trong hàm tạo không được bảo vệ khỏi cuộc đua dữ liệu - điều đó có đủ điều kiện là hoàn thành đầy đủ hay không?
gnat

@gnat: ý của bạn là gì trong "Ví dụ trong Java, việc khởi tạo trường (không phải là cuối cùng) trong hàm tạo không được bảo vệ khỏi cuộc đua dữ liệu". Trừ khi bạn làm một cái gì đó phức tạp một cách ngoạn mục liên quan đến nhiều luồng, khả năng điều kiện cuộc đua bên trong một nhà xây dựng là (hoặc nên) gần như không thể. Bạn không thể truy cập vào một trường của một đối tượng không có cấu trúc ngoại trừ từ bên trong hàm tạo đối tượng. Và nếu việc xây dựng thất bại, bạn không có một tham chiếu đến đối tượng.
Roland Tepp

Sự khác biệt lớn giữa nullvới tư cách là thành viên ngầm của mọi loại và Maybe<T>đó là Maybe<T>, bạn cũng có thể có T, mà không có bất kỳ giá trị mặc định nào.
Svick

Khi tạo mảng, thường sẽ không thể xác định các giá trị hữu ích cho tất cả các phần tử mà không cần phải đọc một số, cũng như không thể xác minh tĩnh rằng không có phần tử nào được đọc mà không có giá trị hữu ích được tính cho nó. Điều tốt nhất có thể làm là khởi tạo các phần tử mảng theo cách mà chúng có thể được nhận ra là không sử dụng được.
supercat

@svick: Trong C # (vốn là ngôn ngữ được đề cập bởi OP), nullkhông phải là thành viên ngầm của mọi loại. Để nullcó giá trị lebal, bạn cần xác định loại thành nullable một cách rõ ràng, điều này làm cho một T?(cú pháp đường cho Nullable<T>) về cơ bản tương đương với Maybe<T>.
Roland Tepp

-3

C ++ thực hiện điều đó bằng cách truy cập vào trình khởi tạo xảy ra trước phần thân của hàm tạo. C # chạy trình khởi tạo mặc định trước phần thân của hàm tạo, nó gần như gán 0 cho mọi thứ, floatstrở thành 0,0, boolstrở thành sai, tham chiếu trở thành null, v.v. Trong C ++, bạn có thể làm cho nó chạy một trình khởi tạo khác để đảm bảo loại tham chiếu không null .

class Foo { Foo(int i) { throw new Exception("Never finishes"); }
class Bar { Bar(string s) { } }

class Broken
{
    val foo: Foo  // where Foo and Bar are non-nullable reference types
    val bar: Bar

    Broken() :
        foo = new Foo(123),// roughly causes a "goto destroy_foo;"
        bar = new Bar("never executes") { }

    // This destructory-function never runs because the constructor never completed
    ~Broken() 
    // This is made-up syntax:
    // : 
    // destroy_bar:
    // bar.~Bar();
    // destroy_foo:
    // foo.~Foo();
    {
    }
}

2
câu hỏi là về các ngôn ngữ với các loại Có thể
gnat

3
Các tài liệu tham khảo của người dùng trở thành vô giá trị - toàn bộ tiền đề của câu hỏi là chúng tôi không có null, và cách duy nhất để chỉ ra sự vắng mặt của một giá trị là sử dụng một Maybeloại (còn được gọi là Option), mà AFAIK C ++ không có trong thư viện chuẩn. Việc không có null cho phép chúng tôi đảm bảo rằng một trường sẽ luôn có giá trị như một thuộc tính của hệ thống loại . Đây là một bảo đảm mạnh hơn so với việc đảm bảo thủ công rằng không có đường dẫn mã nào tồn tại trong đó một biến vẫn có thể tồn tại null.
amon

Mặc dù c ++ không thực sự có các loại Có thể rõ ràng, nhưng những thứ như std :: shared_ptr <T> đủ gần để tôi nghĩ rằng c ++ vẫn xử lý trường hợp khởi tạo biến có thể xảy ra "ngoài phạm vi" của hàm tạo và trong thực tế là bắt buộc đối với các loại tham chiếu (&), vì chúng không thể là null.
FryGuy
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.