Nếu null là xấu, tại sao các ngôn ngữ hiện đại thực hiện nó? [đóng cửa]


82

Tôi chắc chắn rằng các nhà thiết kế ngôn ngữ như Java hoặc C # đã biết các vấn đề liên quan đến sự tồn tại của các tham chiếu null (xem Tài liệu tham khảo null có thực sự là một điều xấu không? ). Ngoài ra, việc thực hiện một loại tùy chọn không thực sự phức tạp hơn nhiều so với tham chiếu null.

Tại sao họ quyết định bao gồm nó? Tôi chắc chắn thiếu tài liệu tham khảo null sẽ khuyến khích (hoặc thậm chí bắt buộc) mã chất lượng tốt hơn (đặc biệt là thiết kế thư viện tốt hơn) cả từ người tạo ngôn ngữ và người dùng.

Có phải đơn giản là vì chủ nghĩa bảo thủ - "các ngôn ngữ khác có nó, chúng ta cũng phải có nó ..."?


99
null là tuyệt vời Tôi yêu nó và sử dụng nó mỗi ngày.
Pieter B

17
@PieterB Nhưng bạn có sử dụng nó cho phần lớn các tài liệu tham khảo hay bạn muốn hầu hết các tài liệu tham khảo không phải là null? Đối số không phải là không nên có dữ liệu không có giá trị, chỉ có điều nó phải rõ ràng và chọn tham gia.

11
@PieterB Nhưng khi đa số không nên là null, sẽ không có ý nghĩa gì khi biến khả năng null thành ngoại lệ thay vì mặc định? Lưu ý rằng mặc dù thiết kế thông thường của các loại tùy chọn là buộc kiểm tra sự vắng mặt và giải nén rõ ràng, người ta cũng có thể có ngữ nghĩa Java / C # / ... nổi tiếng để tham khảo không tham gia (sử dụng như thể không thể null, thổi lên nếu không). Nó ít nhất sẽ ngăn chặn một số lỗi và làm cho một phân tích tĩnh phàn nàn về việc thiếu kiểm tra null thực tế hơn nhiều.

20
WTF là lên với các bạn? Trong tất cả những điều có thể và làm sai với phần mềm, cố gắng để vô hiệu hóa một null là không có vấn đề gì cả. Nó LUÔN tạo ra AV / segfault và do đó được sửa. Có quá nhiều lỗi thiếu mà bạn phải lo lắng về điều này? Nếu vậy, tôi có rất nhiều phụ tùng, và không ai trong số họ phát sinh vấn đề với các tham chiếu / con trỏ null.
Martin James

13
@MartinJames "Nó LUÔN tạo ra AV / segfault và do đó được sửa chữa" - không, không, không.
gièm pha

Câu trả lời:


97

Tuyên bố miễn trừ trách nhiệm: Vì tôi không biết cá nhân nhà thiết kế ngôn ngữ nào, nên bất kỳ câu trả lời nào tôi đưa ra cho bạn sẽ được suy đoán.

Từ chính Tony Hoare :

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.

Nhấn mạnh mỏ.

Đương nhiên, nó dường như không phải là một ý tưởng tồi đối với anh ta vào thời điểm đó. Có vẻ như nó đã được duy trì một phần vì lý do tương tự - nếu nó có vẻ là một ý tưởng tốt cho nhà phát minh quicksort giành giải thưởng Turing, không có gì đáng ngạc nhiên khi nhiều người vẫn không hiểu tại sao nó lại xấu xa. Nó cũng có khả năng một phần vì nó thuận tiện cho các ngôn ngữ mới tương tự như các ngôn ngữ cũ, cả vì lý do đường cong tiếp thị và học tập. Trường hợp tại điểm:

"Chúng tôi theo đuổi các lập trình viên C ++. Chúng tôi đã cố gắng kéo rất nhiều người trong số họ đi được một nửa đến Lisp." -Guy Steele, đồng tác giả của thông số Java

(Nguồn: http://www.paulgraham.com/icad.html )

Và, tất nhiên, C ++ không có giá trị vì C không có giá trị và không cần phải đi vào tác động lịch sử của C. Loại C ++ của J ++ thay thế, là triển khai Java của Microsoft và nó cũng thay thế C ++ là ngôn ngữ được lựa chọn để phát triển Windows, do đó, nó có thể lấy từ một trong hai.

EDIT Đây là một trích dẫn khác từ Hoare đáng xem xét:

Nhìn chung, các ngôn ngữ lập trình phức tạp hơn nhiều so với trước đây: định hướng đối tượng, kế thừa và các tính năng khác vẫn chưa thực sự được suy nghĩ thông qua quan điểm của một ngành học dựa trên cơ sở mạch lạc và khoa học hoặc một lý thuyết chính xác . Định đề ban đầu của tôi, mà tôi đã theo đuổi như một nhà khoa học suốt đời, là người ta sử dụng các tiêu chí chính xác như một phương tiện để hội tụ một thiết kế ngôn ngữ lập trình đàng hoàng, một trong đó không đặt bẫy cho người dùng và những người trong đó mà các thành phần khác nhau của chương trình tương ứng rõ ràng với các thành phần khác nhau của đặc điểm kỹ thuật của nó, vì vậy bạn có thể suy luận thành phần về nó. [...] Các công cụ, bao gồm trình biên dịch, phải dựa trên một số lý thuyết về ý nghĩa của việc viết một chương trình chính xác. - Cuộc phỏng vấn lịch sử chung của Philip L. Frana, 17 tháng 7 năm 2002, Cambridge, Anh; Viện Charles Babbage, Đại học Minnesota. [ Http://www.cbi.umn.edu/oh/display.phtml?id=343]

Một lần nữa, nhấn mạnh của tôi. Sun / Oracle và Microsoft là các công ty và điểm mấu chốt của bất kỳ công ty nào là tiền. Những lợi ích đối với họ có nullthể đã vượt xa những nhược điểm, hoặc đơn giản là họ có thể có thời hạn quá chặt chẽ để xem xét đầy đủ vấn đề. Như một ví dụ về một sai lầm ngôn ngữ khác có thể xảy ra do thời hạn:

Thật xấu hổ khi Clonizable bị hỏng, nhưng nó đã xảy ra. Các API Java ban đầu được thực hiện rất nhanh theo thời hạn chặt chẽ để đáp ứng một cửa sổ thị trường đóng cửa. Nhóm Java ban đầu đã làm một công việc đáng kinh ngạc, nhưng không phải tất cả các API đều hoàn hảo. Clonizable là một điểm yếu, và tôi nghĩ mọi người nên nhận thức được những hạn chế của nó. -Josh Bloch

(Nguồn: http://www.artima.com/intv/bloch13.html )


32
Kính gửi downvoter: làm thế nào tôi có thể cải thiện câu trả lời của mình?
Doval

6
Bạn đã không thực sự trả lời câu hỏi; bạn chỉ cung cấp một số trích dẫn về một số ý kiến ​​sau thực tế và một số vẫy tay thêm về "chi phí". (Nếu null là một sai lầm tỷ đô la, thì có nên tiết kiệm đô la bởi MS và Java bằng cách thực hiện nó để giảm khoản nợ đó không?)
DougM

29
@DougM Bạn mong đợi tôi làm gì, đánh bật mọi nhà thiết kế ngôn ngữ từ 50 năm qua và hỏi anh ta tại sao anh ta thực hiện nullbằng ngôn ngữ của mình? Bất kỳ câu trả lời cho câu hỏi này sẽ được suy đoán trừ khi nó đến từ một nhà thiết kế ngôn ngữ. Tôi không biết bất kỳ trang web nào thường xuyên này ngoài Eric Lippert. Phần cuối cùng là cá trích đỏ vì nhiều lý do. Số lượng mã bên thứ 3 được viết trên API của MS và Java rõ ràng vượt xa số lượng mã trong chính API. Vì vậy, nếu khách hàng của bạn muốn null, bạn cung cấp cho họ null. Bạn cũng cho rằng họ đã chấp nhận nullđang khiến họ mất tiền.
Doval

3
Nếu câu trả lời duy nhất bạn có thể đưa ra là suy đoán, hãy nêu rõ trong đoạn mở đầu của bạn. (Bạn đã hỏi làm thế nào bạn có thể cải thiện câu trả lời của mình và tôi đã trả lời. Bất kỳ phụ huynh nào chỉ là bình luận mà bạn có thể thoải mái bỏ qua; đó là những gì dấu ngoặc đơn dành cho tiếng Anh, sau tất cả.)
DougM

7
Câu trả lời này là hợp lý; Tôi đã thêm một số cân nhắc trong tôi. Tôi lưu ý rằng ICloneablenó bị hỏng tương tự trong .NET; Thật không may, đây là một nơi mà những thiếu sót của Java không được học hỏi từ thời gian.
Eric Lippert

121

Tôi chắc chắn các nhà thiết kế ngôn ngữ như Java hoặc C # đã biết các vấn đề liên quan đến sự tồn tại của các tham chiếu null

Tất nhiên.

Ngoài ra, việc thực hiện một loại tùy chọn không thực sự phức tạp hơn nhiều so với tham chiếu null.

Tôi có ý kiến ​​khác! Các cân nhắc thiết kế đã đi vào các loại giá trị vô giá trị trong C # 2 là phức tạp, gây tranh cãi và khó khăn. Họ đã đưa các nhóm thiết kế của cả hai ngôn ngữ và thời gian chạy nhiều tháng tranh luận, thực hiện các nguyên mẫu, v.v. và trên thực tế, ngữ nghĩa của quyền anh vô hiệu đã được thay đổi rất gần với vận chuyển C # 2.0, điều này rất gây tranh cãi.

Tại sao họ quyết định bao gồm nó?

Tất cả các thiết kế là một quá trình lựa chọn giữa nhiều mục tiêu không tương thích một cách tinh tế và thô thiển; Tôi chỉ có thể đưa ra một bản phác thảo ngắn gọn về một vài yếu tố sẽ được xem xét:

  • Tính trực giao của các tính năng ngôn ngữ thường được coi là một điều tốt. C # có các loại giá trị nullable, các loại giá trị không thể null và các loại tham chiếu nullable. Các kiểu tham chiếu không thể rỗng không tồn tại, điều này làm cho hệ thống kiểu không trực giao.

  • Sự quen thuộc với người dùng hiện tại của C, C ++ và Java rất quan trọng.

  • Khả năng tương tác dễ dàng với COM rất quan trọng.

  • Khả năng tương tác dễ dàng với tất cả các ngôn ngữ .NET khác là rất quan trọng.

  • Khả năng tương tác dễ dàng với cơ sở dữ liệu là rất quan trọng.

  • Tính nhất quán của ngữ nghĩa là quan trọng; nếu chúng ta có tham chiếu TheKingOfFrance bằng null thì điều đó luôn có nghĩa là "không có Vua Pháp ngay bây giờ", hoặc cũng có thể có nghĩa là "Chắc chắn có một vị vua của Pháp; tôi chỉ không biết bây giờ là ai"? hoặc nó có thể có nghĩa là "ý niệm về việc có một vị vua ở Pháp là vô nghĩa, vì vậy đừng đặt câu hỏi!"? Null có thể có nghĩa là tất cả những điều này và hơn thế nữa trong C #, và tất cả các khái niệm này đều hữu ích.

  • Chi phí thực hiện là quan trọng.

  • Có thể tuân theo phân tích tĩnh là quan trọng.

  • 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.

  • Và những gì về tính nhất quán của ngữ nghĩa? Giá trị Null lan truyền khi được sử dụng, nhưng tham chiếu null ném ngoại lệ khi được sử dụng. Điều đó không nhất quán; sự không nhất quán đó được biện minh bởi một số lợi ích?

  • Chúng ta có thể thực hiện các tính năng mà không phá vỡ các tính năng khác? Những tính năng có thể khác trong tương lai có tính năng ngăn chặn?

  • Bạn tham chiến với đội quân bạn có chứ không phải người bạn thích. Hãy nhớ rằng, C # 1.0 không có khái quát, vì vậy nói về Maybe<T>một giải pháp thay thế là một người không bắt đầu hoàn toàn. .NET có nên trượt trong hai năm trong khi nhóm thời gian chạy thêm tổng quát, chỉ để loại bỏ các tham chiếu null?

  • Điều gì về tính nhất quán của hệ thống loại? Bạn có thể nói Nullable<T>với bất kỳ loại giá trị nào - không, chờ đã, đó là một lời nói dối. Bạn không thể nói Nullable<Nullable<T>>. Bạn có thể? Nếu vậy, ngữ nghĩa mong muốn của nó là gì? Có đáng để làm cho toàn bộ hệ thống loại có một trường hợp đặc biệt trong đó chỉ cho tính năng này?

Và như vậy. Những quyết định này rất phức tạp.


12
+1 cho tất cả mọi thứ nhưng đặc biệt là đưa ra thuốc generic. Thật dễ dàng để quên có những khoảng thời gian trong cả lịch sử của Java và C # nơi mà các thế hệ không tồn tại.
Doval

2
Có thể là một câu hỏi ngớ ngẩn (tôi chỉ là một sinh viên CNTT) - nhưng loại tùy chọn không thể được triển khai theo cấp độ cú pháp (với CLR không biết gì về nó) như một tài liệu tham khảo không thể thông thường yêu cầu kiểm tra "có giá trị" trước khi sử dụng mã? Tôi tin rằng các loại tùy chọn không cần bất kỳ kiểm tra trong thời gian chạy.
mrpyo

2
@mrpyo: Chắc chắn, đó là một lựa chọn thực hiện có thể. Không có lựa chọn thiết kế nào khác biến mất, và lựa chọn thực hiện đó có nhiều ưu và nhược điểm riêng.
Eric Lippert

1
@mrpyo Tôi nghĩ rằng việc kiểm tra "có giá trị" không phải là ý kiến ​​hay. Về mặt lý thuyết, đó là một ý tưởng rất tốt, nhưng trong thực tế, IMO nó sẽ mang lại tất cả các loại kiểm tra trống, chỉ để thỏa mãn trình biên dịch - như các ngoại lệ được kiểm tra trong Java và mọi người đánh lừa nó catchesmà không làm gì cả. Tôi nghĩ tốt hơn là để hệ thống nổ tung thay vì tiếp tục hoạt động ở trạng thái có thể không hợp lệ.
NothingsImpossible

2
@voo: Mảng thuộc loại tham chiếu không đáng kể rất khó vì nhiều lý do. Có nhiều giải pháp khả thi và tất cả chúng đều áp đặt chi phí cho các hoạt động khác nhau. Đề xuất của Supercat là theo dõi xem một yếu tố có thể được đọc một cách hợp pháp trước khi nó được chỉ định hay không, điều này áp đặt chi phí. Của bạn là để đảm bảo rằng một trình khởi tạo chạy trên mỗi phần tử trước khi mảng được hiển thị, trong đó áp đặt một tập hợp chi phí khác nhau. Vì vậy, đây là vấn đề: bất kể kỹ thuật nào trong số những kỹ thuật này được chọn, ai đó sẽ phàn nàn rằng nó không hiệu quả đối với kịch bản thú cưng của họ . Đây là điểm nghiêm trọng so với tính năng.
Eric Lippert

28

Null phục vụ một mục đích rất hợp lệ đại diện cho việc thiếu giá trị.

Tôi sẽ nói tôi là người có tiếng nói nhất mà tôi biết về việc lạm dụng null và tất cả những cơn đau đầu và đau khổ mà họ có thể gây ra đặc biệt là khi được sử dụng một cách tự do.

Quan điểm cá nhân của tôi là mọi người có thể sử dụng null chỉ khi họ có thể biện minh cho việc đó là cần thiết và thích hợp.

Ví dụ minh chứng null:

Ngày chết thường là một lĩnh vực vô giá trị. Có ba tình huống có thể xảy ra với ngày chết. Người đó đã chết và ngày được biết, người đó đã chết và ngày không xác định, hoặc người đó không chết và do đó ngày chết không tồn tại.

Ngày chết cũng là trường DateTime và không có giá trị "không xác định" hoặc "trống". Nó có ngày mặc định xuất hiện khi bạn tạo một mốc thời gian mới thay đổi dựa trên ngôn ngữ được sử dụng, nhưng về mặt kỹ thuật, người đó thực tế đã chết vào thời điểm đó và sẽ gắn cờ là "giá trị trống" của bạn nếu bạn sử dụng ngày mặc định

Các dữ liệu sẽ cần phải đại diện cho tình huống đúng.

Người chết là ngày chết được biết đến (3/9/1984)

Đơn giản, '3/9/1984'

Người chết không rõ ngày chết

Vậy điều gì là tốt nhất? Không , '0/0/0000' hoặc '01 / 01/1869 '(hoặc giá trị mặc định của bạn là gì?)

Người không chết ngày không được áp dụng

Vậy điều gì là tốt nhất? Không , '0/0/0000' hoặc '01 / 01/1869 '(hoặc giá trị mặc định của bạn là gì?)

Vì vậy, hãy nghĩ từng giá trị ...

  • Không , nó có ý nghĩa và mối quan tâm bạn cần phải cảnh giác, vô tình cố gắng thao túng nó mà không xác nhận nó không phải là ví dụ đầu tiên sẽ ném một ngoại lệ, nhưng nó cũng đại diện tốt nhất cho tình huống thực tế ... Nếu người đó không chết ngày chết không tồn tại ... không có gì ... không có gì ...
  • 0/0/0000 , Điều này có thể ổn trong một số ngôn ngữ và thậm chí có thể là một đại diện thích hợp không có ngày. Thật không may, một số ngôn ngữ và xác nhận sẽ từ chối đây là một datetime không hợp lệ, điều này khiến nó không hoạt động trong nhiều trường hợp.
  • 1/1/1869 (hoặc bất kể giá trị thời gian mặc định của bạn là gì) , vấn đề ở đây là nó rất khó xử lý. Bạn có thể sử dụng điều đó khi bạn thiếu giá trị giá trị, ngoại trừ điều gì xảy ra nếu tôi muốn lọc tất cả các hồ sơ của mình, tôi không có ngày chết? Tôi có thể dễ dàng lọc ra những người thực sự đã chết vào ngày đó có thể gây ra sự cố toàn vẹn dữ liệu.

Thực tế là đôi khi bạn Đừng cần phải thể hiện không có gì và chắc chắn đôi khi một loại biến hoạt động tốt vì điều đó, nhưng loại thường biến cần để có thể đại diện cho gì cả.

Nếu tôi không có táo tôi có 0 quả táo, nhưng nếu tôi không biết tôi có bao nhiêu quả táo thì sao?

Bằng mọi cách, null bị lạm dụng và có khả năng gây nguy hiểm, nhưng đôi khi nó cần thiết. Nó chỉ là mặc định trong nhiều trường hợp bởi vì cho đến khi tôi cung cấp một giá trị thì thiếu một giá trị và một cái gì đó cần phải thể hiện nó. (Vô giá trị)


37
Null serves a very valid purpose of representing a lack of value.Một Optionhoặc Maybeloại phục vụ mục đích rất hợp lệ này mà không bỏ qua hệ thống loại.
Doval

34
Không ai tranh luận rằng không nên có giá trị thiếu giá trị, họ cho rằng các giá trị có thể bị thiếu nên được đánh dấu rõ ràng như vậy, thay vì mọi giá trị đều bị thiếu.

2
Tôi đoán RualStorge đã nói về mối quan hệ với SQL, bởi vì có những trại nói rằng mỗi cột phải được đánh dấu là NOT NULL. Câu hỏi của tôi không liên quan đến RDBMS mặc dù ...
mrpyo

5
+1 để phân biệt giữa "không có giá trị" và "giá trị không xác định"
David

2
Nó sẽ không có ý nghĩa hơn để tách ra trạng thái của một người? Tức là một Personloại có một statelĩnh vực loại State, đó là một liên minh phân biệt đối xử AliveDead(dateOfDeath : Date).
jon-hanson

10

Tôi sẽ không đi xa đến mức "các ngôn ngữ khác có nó, chúng ta cũng phải có nó ..." giống như đó là một loại theo kịp với Jones. Một tính năng chính của bất kỳ ngôn ngữ mới nào là khả năng tương tác với các thư viện hiện có trong các ngôn ngữ khác (đọc: C). Vì C có con trỏ null, lớp khả năng tương tác nhất thiết cần khái niệm null (hoặc một số tương đương "không tồn tại" khác sẽ nổ tung khi bạn sử dụng nó).

Nhà thiết kế ngôn ngữ có thể đã chọn sử dụng Loại tùy chọn và buộc bạn xử lý đường dẫn null ở mọi nơi mà mọi thứ có thể là null. Và điều đó gần như chắc chắn sẽ dẫn đến ít lỗi hơn.

Nhưng (đặc biệt đối với Java và C # do thời gian giới thiệu và đối tượng mục tiêu của họ) sử dụng các loại tùy chọn cho lớp khả năng tương tác này có thể đã bị tổn hại nếu không sử dụng ngư lôi. Loại tùy chọn được truyền hết, gây khó chịu cho các lập trình viên C ++ từ giữa đến cuối thập niên 90 - hoặc lớp khả năng tương tác sẽ ném ngoại lệ khi gặp null, làm phiền các lập trình viên C ++ từ giữa đến cuối thập niên 90. ..


3
Đoạn đầu tiên không có ý nghĩa với tôi. Java không có chữ C xen kẽ trong hình dạng mà bạn đề xuất (có JNI nhưng nó đã nhảy qua hàng tá vòng cho mọi thứ liên quan đến tài liệu tham khảo; cộng với nó hiếm khi được sử dụng trong thực tế), tương tự cho các ngôn ngữ "hiện đại" khác.

@delnan - xin lỗi, tôi quen thuộc hơn với C #, nơi có loại interop này. Tôi khá giả định rằng nhiều thư viện Java nền tảng cũng sử dụng JNI ở phía dưới.
Telastyn

6
Bạn đưa ra một lập luận tốt để cho phép null, nhưng bạn vẫn có thể cho phép null mà không khuyến khích nó. Scala là một ví dụ tốt về điều này. Nó có thể tương tác liền mạch với các apis Java sử dụng null, nhưng bạn được khuyến khích bọc nó Optionđể sử dụng trong Scala, điều này cũng dễ như val x = Option(possiblyNullReference). Trong thực tế, mọi người không mất nhiều thời gian để thấy được lợi ích của một Option.
Karl Bielefeldt

1
Các loại tùy chọn đi đôi với khớp mẫu (được xác minh tĩnh), điều mà C # không may không có. F # mặc dù, và nó thật tuyệt vời.
Steven Evers

1
@SteveEvers Có thể giả mạo nó bằng cách sử dụng một lớp cơ sở trừu tượng với hàm tạo riêng, các lớp bên trong được niêm phong và một Matchphương thức lấy các đại biểu làm đối số. Sau đó, bạn chuyển các biểu thức lambda đến Match(điểm thưởng cho việc sử dụng các đối số được đặt tên) và Matchgọi đúng.
Doval

7

Trước hết, tôi nghĩ tất cả chúng ta có thể đồng ý rằng một khái niệm về sự vô hiệu là cần thiết. Có một số tình huống mà chúng ta cần phải đại diện cho sự vắng mặt của thông tin.

Cho phép các nulltham chiếu (và con trỏ) chỉ là một triển khai của khái niệm này và có thể là phổ biến nhất mặc dù nó được biết là có vấn đề: C, Java, Python, Ruby, PHP, JavaScript, ... đều sử dụng tương tự null.

Tại sao ? Chà, cái gì thay thế?

Trong các ngôn ngữ chức năng như Haskell, bạn có Optionhoặc Maybeloại; tuy nhiên những thứ đó được xây dựng dựa trên:

  • các loại tham số
  • các loại dữ liệu đại số

Bây giờ, các C, Java, Python, Ruby hoặc PHP ban đầu có hỗ trợ một trong hai tính năng đó không? Các thế hệ hoàn hảo của Java là gần đây trong lịch sử của ngôn ngữ và bằng cách nào đó tôi nghi ngờ những người khác thậm chí còn thực hiện chúng.

Có bạn có nó. nulllà dễ dàng, các loại dữ liệu đại số tham số là khó hơn. Mọi người đã đi cho sự thay thế đơn giản nhất.


+1 cho "null là dễ dàng, các loại dữ liệu đại số tham số khó hơn." Nhưng tôi nghĩ đó không phải là vấn đề đánh máy tham số và ADT khó hơn; chỉ là họ không nhận thấy cần thiết. Nếu Java đã vận chuyển mà không có một hệ thống đối tượng, mặt khác, nó sẽ thất bại; OOP là một tính năng "showstopping", trong đó nếu bạn không có nó, không ai quan tâm.
Doval

@Doval: tốt, OOP có thể cần thiết cho Java, nhưng nó không dành cho C :) Nhưng đúng là Java nhắm đến việc đơn giản. Thật không may, mọi người dường như cho rằng một ngôn ngữ đơn giản dẫn đến các chương trình đơn giản, điều này hơi lạ (Brainfuck là một ngôn ngữ rất đơn giản ...), nhưng chúng tôi chắc chắn đồng ý rằng các ngôn ngữ phức tạp (C ++ ...) mặc dù không phải là thuốc chữa bách bệnh chúng có thể cực kỳ hữu ích.
Matthieu M.

1
@MatthieuM.: Hệ thống thực sự phức tạp. Một ngôn ngữ được thiết kế tốt có độ phức tạp phù hợp với hệ thống trong thế giới thực được mô hình hóa có thể cho phép hệ thống phức tạp được mô hình hóa bằng mã đơn giản. Nỗ lực đơn giản hóa một ngôn ngữ chỉ đơn giản là đẩy sự phức tạp lên người lập trình viên đang sử dụng nó.
supercat

@supercat: Tôi không thể đồng ý nhiều hơn. Hay như Einstein được diễn giải: "Hãy làm mọi thứ đơn giản nhất có thể, nhưng không đơn giản hơn".
Matthieu M.

@MatthieuM.: Einstein đã khôn ngoan theo nhiều cách. Các ngôn ngữ cố gắng giả định "mọi thứ đều là một đối tượng, một tham chiếu có thể được lưu trữ trong Object" không thể nhận ra rằng các ứng dụng thực tế cần các đối tượng có thể thay đổi không chia sẻ và các đối tượng bất biến có thể chia sẻ (cả hai đều hoạt động giống như các giá trị), cũng như có thể chia sẻ và không thể thay đổi thực thể. Sử dụng một Objectloại duy nhất cho mọi thứ không loại bỏ sự cần thiết phải phân biệt như vậy; nó chỉ làm cho việc sử dụng chúng một cách chính xác khó khăn hơn.
supercat

5

Null / nil / none chính nó không phải là xấu xa.

Nếu bạn xem bài giảng nổi tiếng sai lầm "Sai lầm tỷ đô" của mình, Tony Hoare nói về việc cho phép bất kỳ biến nào có thể giữ null là một sai lầm lớn. Giải pháp thay thế - sử dụng Options - không không trên thực tế thoát khỏi tài liệu tham khảo null. Thay vào đó, nó cho phép bạn chỉ định biến nào được phép giữ null và biến nào không.

Như một vấn đề thực tế, với các ngôn ngữ hiện đại thực hiện xử lý ngoại lệ phù hợp, lỗi vô hiệu hóa không có gì khác biệt so với bất kỳ ngoại lệ nào khác - bạn tìm thấy nó, bạn sửa nó. Một số lựa chọn thay thế cho các tham chiếu null (ví dụ mẫu Null Object) ẩn các lỗi, khiến mọi thứ âm thầm thất bại cho đến sau này. Theo tôi, tốt hơn nhiều là thất bại nhanh chóng .

Vì vậy, câu hỏi là sau đó, tại sao các ngôn ngữ không thực hiện Tùy chọn? Như một vấn đề thực tế, ngôn ngữ phổ biến nhất mọi thời đại C ++ có khả năng xác định các biến đối tượng không thể gán NULL. Đây là một giải pháp cho "vấn đề vô giá trị" Tony Hoare đã đề cập trong bài phát biểu của mình. Tại sao ngôn ngữ gõ phổ biến nhất tiếp theo, Java, không có ngôn ngữ này? Người ta có thể hỏi tại sao nó có quá nhiều sai sót nói chung, đặc biệt là trong hệ thống loại của nó. Tôi không nghĩ rằng bạn thực sự có thể nói rằng các ngôn ngữ có hệ thống mắc lỗi này. Một số làm, một số thì không.


1
Một trong những điểm mạnh lớn nhất của Java từ góc độ triển khai, nhưng điểm yếu từ góc độ ngôn ngữ, là chỉ có một loại không nguyên thủy: Tham chiếu đối tượng lăng nhăng. Điều này cực kỳ đơn giản hóa thời gian chạy, tạo ra một số triển khai JVM cực kỳ nhẹ. Tuy nhiên, thiết kế đó có nghĩa là mọi loại phải có giá trị mặc định và đối với Tham chiếu đối tượng lăng nhăng, mặc định duy nhất có thể là null.
supercat

Vâng, một loại gốc không nguyên thủy ở bất kỳ tỷ lệ nào. Tại sao đây là một điểm yếu từ góc độ ngôn ngữ? Tôi không hiểu tại sao thực tế này yêu cầu mọi loại đều có giá trị mặc định (hoặc ngược lại tại sao nhiều loại gốc sẽ cho phép các loại không có giá trị mặc định), cũng như tại sao đó là điểm yếu.
BT

Những loại không nguyên thủy nào khác mà một phần tử trường hoặc mảng có thể giữ? Điểm yếu là một số tài liệu tham khảo được sử dụng để đóng gói danh tính và một số để đóng gói các giá trị có trong các đối tượng được xác định qua đó. Đối với các biến loại tham chiếu được sử dụng để đóng gói danh tính, nulllà mặc định hợp lý duy nhất. Tuy nhiên, các tham chiếu được sử dụng để đóng gói giá trị có thể có hành vi mặc định hợp lý trong trường hợp một loại sẽ có hoặc có thể xây dựng một thể hiện mặc định hợp lý. Nhiều khía cạnh về cách các tài liệu tham khảo nên hành xử tùy thuộc vào việc chúng đóng gói giá trị như thế nào, nhưng ...
supercat

... Hệ thống kiểu Java không có cách nào thể hiện điều đó. Nếu foogiữ tham chiếu duy nhất cho một int[]chứa {1,2,3}và mã muốn foogiữ một tham chiếu đến một int[]chứa {2,2,3}, cách nhanh nhất để đạt được điều đó sẽ là tăng foo[0]. Nếu mã muốn cho một phương thức biết foogiữ {1,2,3}, thì phương thức kia sẽ không sửa đổi mảng hoặc không duy trì tham chiếu ngoài điểm foomuốn sửa đổi nó, cách nhanh nhất để đạt được điều đó là chuyển tham chiếu đến mảng. Nếu Java có loại "tham chiếu chỉ đọc phù du", thì ...
supercat

... Mảng có thể được truyền một cách an toàn dưới dạng tham chiếu phù du và một phương thức muốn duy trì giá trị của nó sẽ biết rằng nó cần phải sao chép nó. Trong trường hợp không có kiểu như vậy, cách duy nhất để phơi bày nội dung của mảng một cách an toàn là tạo một bản sao của nó hoặc gói nó trong một đối tượng được tạo ra chỉ với mục đích đó.
supercat

4

Bởi vì ngôn ngữ lập trình thường được thiết kế để thực tế hữu ích hơn là đúng về mặt kỹ thuật. Thực tế là nullcác trạng thái là một sự xuất hiện phổ biến do dữ liệu xấu hoặc bị thiếu hoặc một trạng thái chưa được quyết định. Các giải pháp vượt trội về mặt kỹ thuật đều khó sử dụng hơn là chỉ cho phép các trạng thái null và hút lấy thực tế là các lập trình viên mắc lỗi.

Ví dụ: nếu tôi muốn viết một tập lệnh đơn giản hoạt động với một tệp, tôi có thể viết mã giả như sau:

file = openfile("joebloggs.txt")

for line in file
{
  print(line)
}

và nó sẽ đơn giản thất bại nếu joebloggs.txt không tồn tại. Vấn đề là, đối với các tập lệnh đơn giản có thể ổn và trong nhiều tình huống trong mã phức tạp hơn, tôi biết nó tồn tại và sự thất bại sẽ không xảy ra nên buộc tôi phải kiểm tra lãng phí thời gian của mình. Các lựa chọn thay thế an toàn hơn đạt được sự an toàn của họ bằng cách buộc tôi xử lý chính xác trạng thái thất bại tiềm tàng nhưng thường thì tôi không muốn làm điều đó, tôi chỉ muốn tiếp tục.


13
Và ở đây bạn đã đưa ra một ví dụ về những gì chính xác là sai với null. Hàm "openfile" được triển khai đúng cách sẽ đưa ra một ngoại lệ (đối với tệp bị thiếu) sẽ dừng thực thi ngay tại đó với lời giải thích chính xác về những gì đã xảy ra. Thay vào đó, nếu nó trả về null, nó sẽ truyền thêm (đến for line in file) và ném ngoại lệ tham chiếu null vô nghĩa, điều này ổn đối với một chương trình đơn giản như vậy nhưng gây ra các vấn đề gỡ lỗi thực sự trong các hệ thống phức tạp hơn nhiều. Nếu null không tồn tại, nhà thiết kế của "openfile" sẽ không thể mắc lỗi này.
mrpyo

2
+1 cho "Bởi vì ngôn ngữ lập trình thường được thiết kế để thực sự hữu ích thay vì đúng về mặt kỹ thuật"
Martin Ba

2
Mọi loại tùy chọn mà tôi biết đều cho phép bạn thực hiện fail-on-null với một lệnh gọi phương thức bổ sung ngắn (ví dụ Rust let file = something(...).unwrap():). Tùy thuộc vào POV của bạn, đó là một cách dễ dàng để không xử lý lỗi hoặc xác nhận cô đọng rằng null có thể xảy ra. Thời gian lãng phí là tối thiểu và bạn tiết kiệm thời gian ở những nơi khác vì bạn không phải tìm hiểu liệu thứ gì đó có thể là null hay không. Một lợi thế khác (có thể tự nó có giá trị cuộc gọi thêm) là bạn rõ ràng bỏ qua trường hợp lỗi; Khi nó thất bại, có rất ít nghi ngờ những gì đã sai và nơi cần khắc phục.

4
@mrpyo Không phải tất cả các ngôn ngữ đều hỗ trợ các ngoại lệ và / hoặc xử lý ngoại lệ (a la try / Catch). Và các trường hợp ngoại lệ cũng có thể bị lạm dụng - "ngoại lệ như kiểm soát dòng chảy" là một kiểu chống phổ biến. Kịch bản này - một tệp không tồn tại - là AFAIK là ví dụ được trích dẫn thường xuyên nhất về mô hình chống đó. Có vẻ như bạn đang thay thế một thực hành xấu bằng một thực hành khác.
David

8
@mrpyo if file exists { open file }bị tình trạng chủng tộc. Cách đáng tin cậy duy nhất để biết nếu mở tệp sẽ thành công là thử mở tệp.

4

Có rất rõ ràng, sử dụng thực tế của các NULL(hoặc nil, hay Nil, hoặc null, hoặc Nothinghoặc bất cứ điều gì nó được gọi trong ngôn ngữ ưa thích của bạn) con trỏ.

Đối với những ngôn ngữ không có hệ thống ngoại lệ (ví dụ C), con trỏ null có thể được sử dụng làm dấu hiệu lỗi khi trả về một con trỏ. Ví dụ:

char *buf = malloc(20);
if (!buf)
{
    perror("memory allocation failed");
    exit(1);
}

Ở đây một NULLtrả về từ malloc(3)được sử dụng như một dấu hiệu của sự thất bại.

Khi được sử dụng trong các đối số phương thức / hàm, nó có thể chỉ ra sử dụng mặc định cho đối số hoặc bỏ qua đối số đầu ra. Ví dụ dưới đây.

Ngay cả đối với những ngôn ngữ có cơ chế ngoại lệ, con trỏ null có thể được sử dụng làm dấu hiệu cho lỗi mềm (nghĩa là lỗi có thể phục hồi được) đặc biệt khi việc xử lý ngoại lệ đắt tiền (ví dụ: Objective-C):

NSError *err = nil;
NSString *content = [NSString stringWithContentsOfURL:sourceFile
                                         usedEncoding:NULL // This output is ignored
                                                error:&err];
if (!content) // If the object is null, we have a soft error to recover from
{
    fprintf(stderr, "error: %s\n", [[err localizedDescription] UTF8String]);
    if (!error) // Check if the parent method ignored the error argument
        *error = err;
    return nil; // Go back to parent layer, with another soft error.
}

Ở đây, lỗi mềm không khiến chương trình bị sập nếu không bắt được. Điều này giúp loại bỏ sự cố bắt điên như Java có và kiểm soát tốt hơn trong luồng chương trình vì các lỗi mềm không bị gián đoạn (và một số ngoại lệ cứng còn lại thường không thể phục hồi và không bị bỏ lại)


5
Vấn đề là không có cách nào để phân biệt các biến không bao giờ chứa nullvới các biến nên. Ví dụ: nếu tôi muốn một loại mới chứa 5 giá trị trong Java, tôi có thể sử dụng một enum, nhưng cái tôi nhận được là một loại có thể chứa 6 giá trị (5 tôi muốn + null). Đó là một lỗ hổng trong hệ thống loại.
Doval

@Doval Nếu đó là tình huống chỉ cần gán cho NULL một nghĩa (hoặc nếu bạn có mặc định, hãy coi nó là từ đồng nghĩa của giá trị mặc định) hoặc sử dụng NULL (không bao giờ xuất hiện ở vị trí đầu tiên) làm dấu hiệu của lỗi mềm (tức là lỗi nhưng ít nhất là chưa bị sập)
Maxthon Chan

1
@MaxtonChan Nullchỉ có thể được gán nghĩa khi các giá trị của loại không mang dữ liệu (ví dụ: giá trị enum). Ngay khi các giá trị của bạn là bất cứ điều gì phức tạp hơn (ví dụ như một cấu trúc), nullkhông thể được gán một ý nghĩa có ý nghĩa cho loại đó. Không có cách nào để sử dụng nullmột cấu trúc hoặc một danh sách. Và, một lần nữa, vấn đề với việc sử dụng nulllàm tín hiệu lỗi là chúng ta không thể biết điều gì có thể trả về null hoặc chấp nhận null. Bất kỳ biến nào trong chương trình của bạn đều có thể nulltrừ khi bạn cực kỳ tỉ mỉ kiểm tra từng cái một nulltrước mỗi lần sử dụng, điều mà không ai làm.
Doval

1
@Doval: Sẽ không có khó khăn cố hữu đặc biệt nào khi có loại tham chiếu bất biến được coi nulllà giá trị mặc định có thể sử dụng (ví dụ: có giá trị mặc định là stringhành xử như một chuỗi rỗng, như cách nó đã làm trong Mô hình đối tượng chung trước đó). Tất cả những gì cần thiết sẽ là cho các ngôn ngữ sử dụng callthay vì callvirtkhi gọi các thành viên không ảo.
supercat

@supercat Đó là một điểm tốt, nhưng bây giờ bạn không cần thêm hỗ trợ để phân biệt giữa các loại bất biến và không bất biến? Tôi không chắc nó tầm thường đến mức nào khi thêm vào một ngôn ngữ.
Doval

4

Có hai vấn đề liên quan, nhưng hơi khác nhau:

  1. Có nên nulltồn tại? Hoặc bạn nên luôn luôn sử dụng Maybe<T>nơi null là hữu ích?
  2. Tất cả các tài liệu tham khảo nên nullable? Nếu không, nên là mặc định?

    Việc phải khai báo rõ ràng các loại tham chiếu nullable string?hoặc tương tự sẽ tránh được hầu hết (nhưng không phải tất cả) các vấn đề nullgây ra, mà không quá khác biệt với những gì lập trình viên đã sử dụng.

Tôi ít nhất đồng ý với bạn rằng không phải tất cả các tài liệu tham khảo nên là nullable. Nhưng tránh null không phải là không có sự phức tạp của nó:

.NET khởi tạo tất cả các trường default<T>trước khi chúng có thể được truy cập trước bằng mã được quản lý. Điều này có nghĩa là đối với các loại tham chiếu bạn cần nullhoặc một cái gì đó tương đương và các loại giá trị đó có thể được khởi tạo thành một loại số không mà không cần chạy mã. Trong khi cả hai đều có nhược điểm nghiêm trọng, sự đơn giản của việc defaultkhởi tạo có thể đã vượt xa những nhược điểm đó.

  • Đối với các trường ví dụ, bạn có thể giải quyết vấn đề này bằng cách yêu cầu khởi tạo các trường trước khi hiển thị thiscon trỏ tới mã được quản lý. Spec # đã đi tuyến đường này, sử dụng cú pháp khác nhau từ chuỗi xây dựng so với C #.

  • Đối với các trường tĩnh, việc đảm bảo điều này khó hơn, trừ khi bạn đặt ra các hạn chế mạnh về loại mã nào có thể chạy trong trình khởi tạo trường vì bạn không thể ẩn thiscon trỏ.

  • Làm thế nào để khởi tạo mảng của các loại tham chiếu? Hãy xem xét một List<T>cái được hỗ trợ bởi một mảng có dung lượng lớn hơn chiều dài. Các yếu tố còn lại cần phải có một số giá trị.

Một vấn đề khác là nó không cho phép các phương thức bool TryGetValue<T>(key, out T value)trở lại default(T)như valuethể chúng không tìm thấy gì. Mặc dù trong trường hợp này, thật dễ dàng để tranh luận rằng tham số out là thiết kế xấu ngay từ đầu và phương thức này sẽ trả về một liên minh phân biệt hoặc có thể thay vào đó.

Tất cả những vấn đề này có thể được giải quyết, nhưng nó không dễ như "cấm null và tất cả đều ổn".


Các List<T>IMHO là ví dụ tốt nhất, bởi vì nó sẽ đòi hỏi một trong hai mà mỗi Tcó một giá trị mặc định, rằng tất cả các mục trong cửa hàng ủng hộ là một Maybe<T>với thêm một "isValid" lĩnh vực, ngay cả khi Tlà một Maybe<U>, hoặc là mã cho List<T>cư xử khác nhau tùy thuộc cho dù đó Tlà một loại nullable. Tôi sẽ coi việc khởi tạo các T[]phần tử thành một giá trị mặc định là ít tệ nhất trong các lựa chọn đó, nhưng tất nhiên điều đó có nghĩa là các phần tử cần phải giá trị mặc định.
supercat

Rust theo điểm 1 - không có giá trị nào cả. Ceylon theo điểm 2 - không null theo mặc định. Các tham chiếu có thể là null được khai báo rõ ràng với loại kết hợp bao gồm tham chiếu hoặc null, nhưng null không bao giờ có thể là giá trị của tham chiếu đơn giản. Do đó, ngôn ngữ này hoàn toàn an toàn và không có NullPulumException vì nó không thể thực hiện được về mặt ngữ nghĩa.
Jim Balter

2

Hầu hết các ngôn ngữ lập trình hữu ích cho phép các mục dữ liệu được viết và đọc theo trình tự tùy ý, do đó thường sẽ không thể xác định tĩnh thứ tự đọc và ghi sẽ xảy ra trước khi chương trình được chạy. Có nhiều trường hợp mã trên thực tế sẽ lưu trữ dữ liệu hữu ích vào mọi vị trí trước khi đọc nó, nhưng việc chứng minh điều đó sẽ khó khăn. Vì vậy, thường sẽ cần phải chạy các chương trình trong đó ít nhất về mặt lý thuyết là mã có thể cố gắng đọc thứ gì đó chưa được viết với giá trị hữu ích. Cho dù mã đó có hợp pháp để làm như vậy hay không, không có cách chung nào để ngăn chặn mã thực hiện. Câu hỏi duy nhất là những gì sẽ xảy ra khi điều đó xảy ra.

Các ngôn ngữ và hệ thống khác nhau có cách tiếp cận khác nhau.

  • Một cách tiếp cận sẽ là nói rằng bất kỳ nỗ lực nào để đọc thứ gì đó chưa được viết sẽ gây ra lỗi ngay lập tức.

  • Cách tiếp cận thứ hai là yêu cầu mã cung cấp một số giá trị ở mọi vị trí trước khi có thể đọc được nó, ngay cả khi không có cách nào để giá trị được lưu trữ có ích về mặt ngữ nghĩa.

  • Cách tiếp cận thứ ba là chỉ cần bỏ qua vấn đề và để bất cứ điều gì sẽ xảy ra "một cách tự nhiên" chỉ xảy ra.

  • Cách tiếp cận thứ tư là nói rằng mọi loại phải có giá trị mặc định và bất kỳ vị trí nào chưa được ghi với bất kỳ thứ gì khác sẽ mặc định cho giá trị đó.

Cách tiếp cận số 4 an toàn hơn nhiều so với phương pháp số 3 và nói chung rẻ hơn so với phương pháp số 1 và số 2. Điều đó sau đó để lại câu hỏi về giá trị mặc định nên là gì cho một loại tham chiếu. Đối với các loại tham chiếu không thay đổi, trong nhiều trường hợp, sẽ có ý nghĩa khi xác định một thể hiện mặc định và nói rằng mặc định cho bất kỳ biến nào của loại đó sẽ là một tham chiếu đến thể hiện đó. Tuy nhiên, đối với các loại tham chiếu có thể thay đổi, điều đó sẽ không hữu ích. Nếu một nỗ lực được thực hiện để sử dụng một loại tham chiếu có thể thay đổi trước khi nó được viết, thì thường không có bất kỳ hành động an toàn nào ngoại trừ bẫy tại điểm sử dụng đã cố gắng.

Về mặt ngữ nghĩa, nếu một loại có một customerskiểu Customer[20]và một lần thử Customer[4].GiveMoney(23)mà không lưu trữ bất cứ thứ gì Customer[4], thì việc thực thi sẽ phải bị mắc kẹt. Người ta có thể lập luận rằng một nỗ lực đọc Customer[4]nên bẫy ngay lập tức, thay vì chờ đợi cho đến khi mã cố gắng GiveMoney, nhưng có đủ trường hợp để đọc một vị trí, tìm ra rằng nó không giữ giá trị, và sau đó sử dụng giá trị đó thông tin, có nỗ lực đọc chính nó thất bại thường sẽ là một phiền toái lớn.

Một số ngôn ngữ cho phép một người chỉ định rằng các biến nhất định sẽ không bao giờ chứa null và mọi nỗ lực lưu trữ null sẽ gây ra bẫy ngay lập tức. Đó là một tính năng hữu ích. Tuy nhiên, nói chung, bất kỳ ngôn ngữ nào cho phép lập trình viên tạo ra các mảng tham chiếu đều sẽ phải cho phép khả năng của các phần tử mảng null, hoặc nếu không thì bắt buộc khởi tạo các phần tử mảng thành dữ liệu không thể có ý nghĩa.


Một sẽ không Maybe/ Optionloại giải quyết vấn đề với # 2, vì nếu bạn không có một giá trị để bạn tham khảo chưa nhưng sẽ có một trong tương lai, bạn chỉ có thể lưu trữ Nothingtrong một Maybe <Ref type>?
Doval

@Doval: Không, nó sẽ không giải quyết được vấn đề - ít nhất, không phải không giới thiệu các tài liệu tham khảo null nữa. Một "không có gì" hành động như một thành viên của loại? Nếu vậy thì cái nào? Hay nó nên ném một ngoại lệ? Trong trường hợp nào, làm thế nào bạn tốt hơn là chỉ sử dụng nullđúng / hợp lý?
cHao

@Doval: Loại ủng hộ của a List<T>là a T[]hay a Maybe<T>? Những gì về loại ủng hộ của một List<Maybe<T>>?
supercat

@supercat Tôi không chắc chắn làm thế nào một loại sao lưu Maybecó ý nghĩa ListMaybegiữ một giá trị duy nhất. Ý bạn là Maybe<T>[]sao
Doval

@cHao Nothingchỉ có thể được gán cho các giá trị loại Maybe, vì vậy nó không hoàn toàn giống như gán null. Maybe<T>Tlà hai loại khác nhau.
Doval
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.