Bạn Phải Sửa Lại Các Assert Của Mình: Bài Học Từ Ngôn Ngữ Zig

Công nghệ01 tháng 6, 2026·8 phút đọc

Bài viết này phản đối việc tắt các câu lệnh khẳng định (assert) trong môi trường sản xuất, sử dụng ngôn ngữ lập trình Zig làm ví dụ điển hình. Tác giả lập luận rằng việc giữ nguyên các assertion giúp tối ưu hóa hiệu năng và phát hiện lỗi sớm, trong khi việc tắt chúng có thể dẫn đến những lỗ hổng bảo mật và hành vi không mong muốn nguy hiểm hơn.

Bạn Phải Sửa Lại Các Assert Của Mình: Bài Học Từ Ngôn Ngữ Zig

Tại sao bạn không nên tắt 'assert' trong môi trường sản xuất: Bài học từ Zig

Tôi nghĩ việc "tắt assert trong môi trường sản xuất (production)" là một kỹ thuật khá phổ biến, đúng không? Theo hiểu biết của tôi, đây có thể là một nhận định đúng, nhưng tôi tin rằng đó là một thói quen xấu không thể tha thứ. Hãy bắt đầu với một số bối cảnh trước, vì cuộc thảo luận này bắt nguồn từ cách std.debug.assert hoạt động trong ngôn ngữ Zig.

Một câu lệnh assert (khẳng định) là một dòng mã giới thiệu một sự thật mới cho chương trình, chẳng hạn như "tham số này không bao giờ là null", hoặc "số nguyên này không bao giờ là số chẵn". Trong Zig, các assert được sử dụng để nêu rõ các điều kiện tiền đề/hậu tố và các bất biến (invariants) trong mã của bạn. Điều này rất hữu ích vì nếu bạn chọn các assertion tốt, chúng sẽ bảo vệ bạn khỏi các lỗi lập trình tốt hơn nhiều so với bài kiểm tra đơn vị (unit test), đặc biệt nếu bạn thực hiện fuzzing cho mã của mình.

Cơ chế Unreachable và Tối ưu hóa trong Zig

Trong Zig, các assert dựa trên tính năng unreachable — một tính năng ngôn ngữ đánh dấu các đường dẫn mã không hợp lệ. Ví dụ, nếu một biến luôn bị thay đổi từ trạng thái .a sang .b trước khi đến một câu lệnh switch, thì trường hợp .a trong switch đó là không thể xảy ra.

Một tính năng thú vị khác của unreachable là nó có thể được sử dụng như một câu lệnh, nhưng cũng hợp lệ ở bất kỳ đâu mà một biểu thức (bất kỳ kiểu nào) được mong đợi. Nhờ unreachable, chúng ta không cần phải đưa ra một giá trị giữ chỗ (placeholder) khó xử cho một trường hợp không bao giờ xảy ra.

Hàm assert trong thư viện chuẩn của Zig cũng tận dụng unreachable. Điều này cho phép trình biên dịch thực hiện các tối ưu hóa mạnh mẽ. Ví dụ, mã máy cần thiết để triển khai nhánh đầu tiên của câu lệnh switch có thể bị loại bỏ hoàn toàn khỏi tệp thực thi cuối cùng. Đây là loại tối ưu hóa mà các trò chơi điện tử và các ứng dụng đa phương thức thời gian thực khác dựa vào rất nhiều.

Các chế độ biên dịch và Rủi ro của việc Tắt Assert

Zig có các chế độ xây dựng khác nhau. Các chế độ được kiểm tra (Debug, ReleaseSafe, @setRuntimeSafety(true)) đảm bảo chương trình sẽ bị crash (panic) khi một assert bị kích hoạt. Ngược lại, các chế độ không được kiểm tra (ReleaseFast, ReleaseSmall, @setRuntimeSafety(false)) dẫn đến "hành vi bất hợp pháp không được kiểm tra" (unchecked illegal behavior).

Nói ngắn gọn, hành vi bất hợp pháp không được kiểm tra có nghĩa là chương trình sẽ hoạt động sai. Trong ví dụ về câu lệnh switch, khi tối ưu hóa, mã máy có thể "rơi qua" sang các trường hợp khác, dẫn đến kết quả không mong muốn. Đây là một công cụ sắc bén, nhưng nó là thứ thúc đẩy nhiều tối ưu hóa mạnh mẽ.

Sự khác biệt so với C/C++

Khi tiếp cận Zig, một điều khiến các nhà phát triển C/C++ đặc biệt ngạc nhiên là std.debug.assert không phải là một macro (và xin lưu ý rằng Zig không có macro). Trong C/C++, việc tắt assertion thường hoạt động giống như việc chú thích mọi lời gọi hàm assert, bao gồm cả biểu thức được truyền vào macro. Điều này có nghĩa là bạn không bao giờ nên đặt biểu thức có tác động phụ vào lời gọi assert trong các ngôn ngữ đó.

Trong Zig, điều này không xảy ra vì std.debug.assert là một hàm bình thường. Điều này có nghĩa là các đối số của nó luôn được đánh giá trước khi gọi hàm bất kể logic bên trong hàm là gì. Kết quả là bạn có thể đặt các biểu thức có tác động phụ vào các lời gọi assert mà không cần lo lắng. Tuy nhiên, điều này cũng có nghĩa là nếu bạn có một assert dựa trên các phép tính phức tạp, chúng có thể không bị loại bỏ khi xây dựng ở các chế độ không kiểm tra. Trong trường hợp đó, bạn cần cẩn thận bảo vệ mã bằng một câu lệnh if tại thời điểm biên dịch (comptime if).

Tại sao việc tắt Assert là một ý tưởng tồi

Có ba điều bạn có thể làm với asserts:

  1. Giữ chúng hoạt động để kiểm tra tính đúng đắn.
  2. Biến chúng thành cơ hội tối ưu hóa hiệu năng.
  3. Tắt chúng đi để tránh crash.

Tôi tin rằng lựa chọn (3) là một lựa chọn tồi tệ không thể cứu vãn. Tắt assert có nghĩa là khi một trong những điều kiện được giả định là không thể xảy ra thực sự xảy ra, chương trình sẽ tiếp tục chạy thay vì bị crash. Bây giờ bạn có một chương trình tiếp tục chạy dưới các giả định sai lệch. Đây vẫn là một dạng hành vi sai lệch, ngay cả khi nó không được gây ra bởi hành vi bất hợp pháp không được kiểm tra.

Những người ủng hộ an toàn bộ nhớ một cách ngây thơ có thể lập luận rằng hành vi không xác định (UB) tồi tệ hơn nhiều, nhưng tôi không đồng ý. Điều làm cho UB trở nên nguy hiểm là nó là một con đường biến chương trình thành một "cỗ máy kỳ quái" (weird machine). Tuy nhiên, trong phần mềm đủ phức tạp, bạn không nhất thiết cần UB để vặn xoắn chương trình thành một cỗ máy như vậy. Việc làm sai một assertion tại thời gian chạy theo định nghĩa là sự lệch khỏi thông số kỹ thuật, và nó có thể dễ dàng đủ để khiến chương trình thực hiện các hoạt động mà nó không bao giờ định làm.

SQL Injection là một ví dụ cụ thể và phổ biến về hành vi sai lệch cấp độ "cỗ máy kỳ quái" không yêu cầu UB. Nếu chi phí của hành vi sai lệch chương trình cao đến mức bạn không muốn mạo hiểm, thì bạn nên giữ assert bật. Nếu hiệu năng quan trọng đến mức bạn sẵn sàng mạo hiểm hành vi sai lệch, thì bạn đang để lãng phí hiệu năng trên bàn, trong khi nghĩ rằng mình an toàn hơn thực tế.

Hiệu ứng "Gaslighting" của các Assert sai

Có một lý do còn lớn hơn tại sao việc tắt có hệ thống tất cả các assert trong prod là phản tác dụng. Vấn đề cốt lõi nằm ở khả năng các assert đó bị sai, và hậu quả của việc đó. Nếu chúng ta có thể đảm bảo rằng tất cả các assert của mình luôn đúng, thì việc luôn sử dụng chúng để tối ưu hóa hiệu năng sẽ không gây tranh cãi.

Tuy nhiên, chúng ta biết rằng mình có thể viết một assert sai và không có gì đảm bảo rằng các bài kiểm tra sẽ bắt được nó. Nếu bạn đang trong tình huống đó, việc phát hiện ra tất cả các assert sai trong mã của mình càng sớm càng tốt là lợi ích tốt nhất của bạn. Nếu không, bạn sẽ tiếp tục viết thêm mã dựa sai lầm vào những assertion sai đó, làm trầm trọng thêm vấn đề.

Hãy tưởng tượng một cơ sở mã lớn có một assert sai ở đâu đó. Thời gian trôi qua, assert dường như giữ đúng vì nó không bao giờ kích hoạt trong thử nghiệm, và bạn không bao giờ phát hiện ra rằng assert này thực sự có thể bị bác bỏ vì bạn đã tắt assert trong prod. Sau một thời gian, người khác thêm mã bên dưới nó. Vì trong thử nghiệm assert đầu tiên không bao giờ kích hoạt, assert thứ hai cũng không bao giờ kích hoạt. Một cách mỉa mai, đây có thể là lúc một lỗ hổng có thể khai thác được được đưa vào cơ sở mã và bạn không nhận ra, vì bạn đã tắt assert trong prod.

Viết mã đúng đã khó, nhưng việc làm điều đó trở nên vô lý khó khăn khi mã có những assert thực sự "gaslight" (làm hoài nghi) bạn.

Kết luận: Hãy sửa chữa Assert của bạn

Tùy thuộc vào bối cảnh, các chương trình khác nhau sẽ có các ưu tiên khác nhau. Đối với một số chương trình, việc ưu tiên hiệu suất hơn là giảm thiểu rủi ro hành vi sai lệch là lựa chọn đúng đắn. Trong trường hợp đó, việc biến assert thành cơ hội tối ưu hóa hoàn toàn hợp lý.

Tuy nhiên, việc thường xuyên tắt assert trong prod là tối ưu kém hơn cả việc giữ assert bật và tối ưu hóa cho hiệu suất. Tôi thấy thật vô lý khi mọi người thực hành thói quen này một cách không phê phán, trong khi lại cực kỳ chỉ trích chế độ ReleaseFast.

Thực tế là không có cách nào quanh co: bạn phải sửa những assert chằng chịt của mình và phấn đấu cho tính đúng đắn của chương trình, không chỉ là một tập con của nó. Việc tắt assert trong các bản build release đối với tôi dường như là điều ngớ ngẩn nhất một người có thể làm, chỉ đứng sau việc xóa assert khỏi cơ sở mã hoặc đơn giản là không bao giờ viết chúng.

Chia sẻ:FacebookX
Nội dung tổng hợp bằng AI, mang tính tham khảo. Xem bài gốc ↗