VietTechiesHIỂU CÔNG NGHỆ. MỞ GÓC NHÌN.
☰ Chuyên mục

Tony Hoare gọi null là sai lầm tỷ đô của mình. Năm 1965, ông đã từ chối cách khắc phục.

11/10/2026 · VietTechies · Biên tập

Tony Hoare năm 2011, mỉm cười, đeo kính gọng kim loại mảnh, bên cạnh một mảng nền tối có tên ông và họa tiết code về tham chiếu null.


Năm 1965, Tony Hoare đưa tham chiếu null vào ALGOL W, một ngôn ngữ lập trình thuộc họ ALGOL (viết tắt của “algorithmic language”, tức “ngôn ngữ thuật toán”). Lời giải thích nổi tiếng nhất của ông gói gọn trong một câu: ông làm vậy “đơn giản chỉ vì nó quá dễ thực hiện”.

Câu nói ấy ẩn chứa một mâu thuẫn. Khi đó, Hoare đang thiết kế thứ mà về sau ông gọi là “hệ thống kiểu toàn diện đầu tiên dành cho tham chiếu trong một ngôn ngữ hướng đối tượng”. Theo chính lời ông, mục tiêu là mọi lần dùng tham chiếu “đều phải tuyệt đối an toàn, và việc kiểm tra do trình biên dịch (compiler) tự động đảm nhận”.

Null là trường hợp ngoại lệ. Từ đây, một tham chiếu có thể chẳng trỏ vào đâu cả, và cũng chẳng có gì trong ngôn ngữ buộc chương trình phải kiểm tra trước khi lần theo tham chiếu đó.

Bốn mươi bốn năm sau, vào tháng 3 năm 2009, Hoare lên phát biểu tại QCon London, một hội nghị về phần mềm, trong chuyên đề mang tên “Historically bad ideas” (Những ý tưởng tồi trong lịch sử). Trong phần tóm tắt bài nói, ông viết: “Tôi gọi nó là sai lầm tỷ đô của mình.”

Phần tóm tắt cũng nêu ra cách khắc phục. Những ngôn ngữ mới hơn như Spec#, một bản mở rộng của C# do Microsoft Research phát triển, cho phép lập trình viên khai báo những tham chiếu không bao giờ có thể là null. Hoare viết: “Đây là giải pháp mà tôi đã gạt bỏ vào năm 1965.”

Hoare qua đời vào tháng 3 năm 2026, hưởng thọ 92 tuổi. Người ta biết đến ông nhiều hơn qua thuật toán sắp xếp Quicksort và logic Hoare, một hệ thống hình thức dùng để chứng minh tính đúng đắn của chương trình. Ông nhận Giải thưởng Turing năm 1980. Vậy mà ở một chuyên đề về những ý tưởng tồi trong lịch sử, ý tưởng tồi mà ông đem ra nói lại là của chính ông.

Kiểm tra từng chỉ số, không sót lần nào

Ngoại lệ ấy đáng chú ý chính vì những gì Hoare từng xây dựng trước đó. Năm 1960, ông vào làm lập trình viên tại Elliott Brothers, một hãng sản xuất máy tính của Anh. Tại đây, ông dẫn dắt việc phát triển một compiler đời đầu cho ALGOL 60. Trong bài giảng nhận Giải thưởng Turing, ông nhớ lại rằng ở compiler đó, “mọi chỉ số của mọi biến có chỉ số, ở mọi chỗ xuất hiện, đều được kiểm tra lúc chương trình chạy, không trừ lần nào”. Nói nôm na, lần truy cập mảng nào cũng được kiểm tra xem có vượt ra ngoài giới hạn của mảng hay không.

Nhiều năm sau, công ty hỏi khách hàng có muốn một tùy chọn tắt các bước kiểm tra ấy hay không, để chương trình chạy nhanh hơn khi đưa vào sử dụng thực tế. Hoare kể lại: “Tất cả đều nhất trí khuyên chúng tôi đừng làm thế.” Khách hàng vốn đã biết lỗi chỉ số xuất hiện thường xuyên đến mức nào trong những lần chạy thực tế. Ông nói thêm rằng ở bất kỳ ngành kỹ thuật đàng hoàng nào, việc không tuân thủ “những biện pháp phòng ngừa sơ đẳng như vậy” hẳn đã bị luật pháp cấm từ lâu.

Vậy tại sao lại để null làm ngoại lệ? Trả lời phỏng vấn sau đó, cũng trong năm 2009, Hoare cho biết phương án an toàn có cái giá riêng của nó.

Giải pháp ông đã từ chối

Trong ALGOL W, lập trình viên vốn đã có thể khai báo một lớp chứa các “bản ghi biến thể” (variant record), trong đó mỗi biến thể có một bộ thuộc tính (tức các trường dữ liệu) riêng. Hoare nói với trang Simple Talk của Redgate rằng tham chiếu null lẽ ra có thể chỉ đơn giản là một biến thể không có thuộc tính nào cả.

Cái khó nằm ở khâu kiểm tra. Với thiết kế đó, compiler sẽ bắt mọi lần truy cập thuộc tính phải kèm một phép kiểm tra, cộng thêm đoạn code xử lý trường hợp rỗng. Hoare nói: “Vấn đề đã được giải quyết! Nhưng chỉ bằng cách đẩy nó sang cho người dùng.”

Ông còn có những lý do thực tế để phản đối. Các phép kiểm tra nhiều khi không cần thiết, và điều đó làm giảm hiệu năng. Biến có thể đổi giá trị giữa lúc kiểm tra và lúc truy cập. Ngoài ra còn phải tính đến bộ thu gom rác (garbage collector), tức bộ phận trong hệ thống đang chạy có nhiệm vụ tìm và giải phóng vùng nhớ không còn dùng đến. Bộ thu gom rác kiểu “scan-mark” cần mọi tham chiếu đều có giá trị khởi tạo, và theo ông, null là lựa chọn đúng. Ông bổ sung rằng với các cấu trúc dữ liệu dạng vòng, không có cách nào dễ dàng để thay thế null.

Đó là những lập luận kỹ thuật thực thụ. Theo lời Hoare, thứ ông không nhìn ra là con số tổng cộng: “Tôi đã không thể lường trước rằng các lỗi tham chiếu null sẽ cộng dồn thành một cái giá khổng lồ trong những thập kỷ tiếp theo.” Ông còn đưa ra điều mà ông gọi là lời bào chữa duy nhất còn lại: “nếu tôi không phát minh ra tham chiếu null thì cũng sẽ có người khác làm việc đó.”

Con số một tỷ đô la là ước tính phỏng chừng

Con số nổi tiếng ấy nghe thì chắc nịch, nhưng thực ra không vững đến thế. Phần tóm tắt của Hoare viết rằng những lỗi, lỗ hổng và sự cố sập hệ thống mà null kéo theo “có lẽ đã gây ra phiền toái và thiệt hại trị giá một tỷ đô la trong 40 năm qua”. Đó là ước tính phỏng chừng của chính ông, có rào đón bằng chữ “có lẽ”, và phần tóm tắt không dẫn ra nghiên cứu nào làm căn cứ.

Qua những lần kể lại, con số này có thể bị thổi phồng. Một bài báo năm 2025 trên heise online, một trang tin công nghệ, viết rằng theo Hoare, sai lầm này “đã gây ra thiệt hại hàng tỷ đô la”. Còn Hoare thì nói một tỷ, kèm chữ “có lẽ”, trong vòng 40 năm.

Giải pháp ấy ngày nay ra sao

Hãy nhìn kỹ cách sửa mà Hoare ủng hộ. Nó không xóa bỏ giá trị rỗng, mà để kiểu dữ liệu chỉ rõ chỗ nào được phép có giá trị rỗng.

Ngày nay, giải pháp ấy đã nằm sẵn trong những ngôn ngữ phổ biến. Trong Kotlin, một biến kiểu String không thể chứa null. Muốn cho phép null, bạn khai báo biến, chẳng hạn b, với kiểu String?, và từ đó trở đi, compiler sẽ báo lỗi nếu bạn viết b.length mà không có phép kiểm tra hay lời gọi an toàn (safe call) nào. Tài liệu của Kotlin mô tả an toàn null (null safety) là một tính năng được thiết kế để “giảm đáng kể rủi ro do tham chiếu null, vốn còn được gọi là Sai lầm tỷ đô”.

Rust còn đi xa hơn: ngôn ngữ này không có tham chiếu null. Giá trị nào có thể không có thì mang kiểu Option<T>, tức là hoặc None, hoặc Some(T), và bạn phải chuyển nó thành T thì mới dùng được như một giá trị thuộc kiểu đó. Cách làm này gần với thiết kế bản ghi biến thể mà Hoare từng mô tả: một biến thể không chứa gì, và một biến thể mang theo giá trị.

C# chọn cách nhẹ tay hơn. Phiên bản 8.0, phát hành tháng 9 năm 2019, đã bổ sung kiểu tham chiếu có thể null (nullable reference type). Có điều, đây là những chú thích kiểu dùng để compiler đưa ra cảnh báo; khi chương trình chạy, tham chiếu có thể null và tham chiếu không thể null hoạt động như nhau.

Ai trả giá cho chữ “dễ thực hiện”

Cả hai phương án của Hoare đều dồn việc lên vai lập trình viên. Với bản ghi biến thể, lẽ ra lần truy cập nào cũng phải kèm một phép kiểm tra, và compiler sẽ bắt buộc điều đó. Null khiến việc kiểm tra trở thành tùy chọn, để rồi cái giá phải trả quay về dưới dạng mà phần tóm tắt của ông gọi là “vô số lỗi, lỗ hổng và sự cố sập hệ thống”.

Các ngôn ngữ hiện đại giữ lại phép kiểm tra, nhưng khiến việc viết nó đỡ tốn công hơn. Kiểu dữ liệu cho biết chỗ nào được phép có null, còn toán tử gọi an toàn ?. của Kotlin trả về null thay vì ném ra ngoại lệ khi đối tượng là null.

Cái giá của một lựa chọn mặc định dễ làm hiếm khi do người chọn nó trả. Người trả là tất cả những ai xây dựng tiếp trên nền đó, chừng nào lựa chọn ấy còn tồn tại.

Vậy nên, lần tới khi một lựa chọn thiết kế có vẻ “quá dễ thực hiện”, hãy tự hỏi câu mà Hoare nói ông đã không trả lời được vào năm 1965: thiết kế ấy sẽ khiến tất cả những người dùng nó tốn bao nhiêu, nếu cộng dồn qua nhiều thập kỷ?

Bài liên quan

Đọc bằng ngôn ngữ khác: English

Nguồn