Bạn nhất định phải sửa `assert`
(kristoff.it)- assert là cơ chế để biểu đạt tiền điều kiện, hậu điều kiện và bất biến ngay trong mã; nếu một ràng buộc có thể được hệ thống kiểu ép buộc thì nên biểu đạt bằng tính năng của ngôn ngữ
std.debug.assertcủa Zig là hàm thông thường chứ không phải macro, dùngunreachableđể đánh dấu nhánh không thể xảy ra và cũng được tận dụng cho tối ưu hóa- Trong Debug và ReleaseSafe, assert thất bại sẽ làm chương trình crash bằng panic, nhưng trong ReleaseFast và ReleaseSmall thì có thể trở thành unchecked illegal behavior và gây hoạt động sai
- Nếu tắt assert trong production, bạn sẽ đánh mất cơ hội phát hiện sớm các giả định sai, và về sau mã có thể phụ thuộc vào assert sai rồi dẫn đến lỗ hổng
- Việc chọn ReleaseSafe hay ReleaseFast phụ thuộc vào mức ưu tiên của chương trình, nhưng điểm cốt lõi là không nên che đậy bằng cách tắt assert mà phải sửa những assert sai
Vai trò của assert và hành vi mặc định của Zig
- assert là cơ chế để biểu đạt trong mã rằng những điều kiện như “tham số này không thể là null”, “số nguyên này không thể là số chẵn” luôn phải đúng
- Ví dụ:
assert(my_arg != null);,assert(my_num % 2 != 0); - Nếu có thể dùng hệ thống kiểu để ép buộc ràng buộc thì nên dùng tính năng của ngôn ngữ thay vì assert
- Trong Zig, con trỏ thường
*Fookhông thể là null, còn con trỏ tùy chọn?*Foocó thể là null nhưng buộc phải kiểm tra trước khi truy cập giá trị
- Ví dụ:
- assert phù hợp để biểu đạt tiền điều kiện, hậu điều kiện và bất biến
- Một assert tốt đôi khi còn mạnh hơn unit test trong việc bắt lỗi lập trình
- Khi dùng cùng fuzzing, hiệu quả của assert có thể còn lớn hơn
unreachable của Zig và assert
- assert của Zig dựa trên
unreachable, tính năng ngôn ngữ dùng để đánh dấu đường đi mã sai- Trong
switch, có thể đánh dấu một nhánh không thể đạt tới như.a => unreachable unreachablecó thể được dùng như một câu lệnh, hoặc ở vị trí cần một biểu thức thuộc bất kỳ kiểu nào- Không cần phải cố tạo ra một giá trị tạm chỉ để lấp vào trường hợp không thể xảy ra
- Trong
std.debug.asserttrong thư viện chuẩn Zig được triển khai như saupub fn assert(ok: bool) void { if (!ok) unreachable; // assertion failure }- Thông tin
unreachablecó thể được dùng cho tối ưu hóa- Trình biên dịch có thể loại bỏ các đường đi không thể tới, và thông tin này có thể lan truyền để cho phép tối ưu hóa phi cục bộ
- Không phải mọi assert đều mang lại cải thiện hiệu năng, nhưng cũng có những tối ưu hóa mà lập trình viên khó dự đoán trước
Chế độ build và an toàn runtime
- Zig có các chế độ build Debug, ReleaseSafe, ReleaseFast, ReleaseSmall
- Thiết lập này không nhất thiết chỉ áp dụng toàn cục cho cả chương trình
- Mỗi dependency có thể được build ở chế độ khác nhau, và với
@setRuntimeSafetythì còn có thể điều chỉnh an toàn runtime theo từng block bên trong hàm
- Assert thất bại được xem là “illegal behavior” trong Zig
- Ở các chế độ checked như Debug, ReleaseSafe,
@setRuntimeSafety(true), chương trình sẽ crash bằng panic - Ở các chế độ unchecked như ReleaseFast, ReleaseSmall,
@setRuntimeSafety(false), sẽ phát sinh “unchecked illegal behavior” khiến chương trình hoạt động sai
- Ở các chế độ checked như Debug, ReleaseSafe,
- Kết quả của unchecked illegal behavior là không được bảo đảm
- Trong ví dụ
switch, do đặc điểm của mã máy hiện được tạo ra nên có thể trông như nó nhảy sang nhánh khác - Ở phiên bản compiler khác, nó có thể tạo ra hành vi sai hoàn toàn khác
- Có thể xem hành vi liên quan trong ví dụ godbolt
- Trong ví dụ
- Cũng có thể xem một ví dụ godbolt khác để thấy assert và
switchphía sau khác nhau ra sao giữa ReleaseSafe và ReleaseFast- Trong ReleaseFast, xuất hiện dạng hàm bỏ qua mọi phép so sánh và trả về
true - Đây là kiểu hành vi mà video game và các ứng dụng media thời gian thực khác phụ thuộc rất nhiều vào
- Trong ReleaseFast, xuất hiện dạng hàm bỏ qua mọi phép so sánh và trả về
Assert của Zig không phải macro
std.debug.assertcủa Zig là hàm thông thường chứ không phải macro- Zig không có macro
- Đây là điểm khiến các lập trình viên C/C++ đặc biệt bất ngờ khi tiếp cận Zig
- Trong C/C++, khi vô hiệu hóa assert thì thường cả lời gọi assert lẫn biểu thức truyền vào sẽ hoạt động như thể bị comment out
- Vì vậy trong C/C++ không nên đưa biểu thức có side effect vào assert
- Bởi khi assert bị vô hiệu hóa thì chính phép tính đó cũng có thể biến mất
- Trong Zig, theo quy tắc gọi hàm, đối số được đánh giá trước khi gọi hàm
- Biểu thức đối số vẫn được đánh giá bất kể logic bên trong
std.debug.assert - Vì vậy có thể đưa vào assert cả biểu thức có side effect như sau
// assert that the remove operation is not a noop: assert(my_map.remove("expected-to-exist")); - Biểu thức đối số vẫn được đánh giá bất kể logic bên trong
- Ngược lại, nếu cần phép tính phức tạp để xác định điều kiện assert thì trong chế độ unchecked, phép tính đó chưa chắc sẽ bị loại bỏ
- Trong trường hợp đó cần dùng
comptime ifđể bảo vệ đoạn mã
const builtin = @import("builtin"); if (builtin.mode == .Debug) { var condition = ...; // whatever bookkeeping is necessary // to compute the condition assert(condition == .ok); } - Trong trường hợp đó cần dùng
- Nếu đã quen với ngữ nghĩa C/C++ thì điều này có thể hơi lạ, nhưng trong Zig có giả định rằng assert nói chung sẽ không bị vô hiệu hóa
Vấn đề khi tắt assert trong production
- Về cơ bản có ba lựa chọn với assert
- Giữ nó như kiểm tra runtime và để tiến trình crash bằng panic khi thất bại
- Dùng assert cho tối ưu hóa hiệu năng, đồng thời chấp nhận chương trình có thể hoạt động sai nếu assert sai
- Vô hiệu hóa hoàn toàn assert
std.debug.assertkhông hỗ trợ mặc định việc vô hiệu hóa hoàn toàn assert- Nếu tự triển khai assert kiểm tra cờ ở thời điểm build thì có thể tạo ra hành vi gần với cách của C/C++
- Lý do khiến người ta muốn tắt assert thường là sự kết hợp của hai yếu tố
- Không muốn giữ kiểm tra runtime vì ghét chi phí hiệu năng hoặc việc ứng dụng bị crash
- Không đủ tin rằng assert luôn đúng, nên sợ chương trình hoạt động sai khi assert được dùng cho tối ưu hóa
- Như matklad nhắc lại trong một thảo luận liên quan, đúng là có những tình huống có lý do kỹ thuật chính đáng để phải tránh crash
- Nhưng với phần mềm nói chung, lấy việc tránh crash làm mặc định bị xem là một lựa chọn tệ
- Khi vô hiệu hóa assert, kể cả khi điều kiện vốn được giả định là không thể xảy ra thật sự xuất hiện thì chương trình vẫn tiếp tục chạy
- Chương trình sẽ tiếp tục vận hành dựa trên giả định sai, và đó là một dạng hoạt động sai ngay cả khi chưa phải unchecked illegal behavior
- Lý do unchecked illegal behavior hay undefined behavior trong C nguy hiểm là vì chúng có thể trở thành con đường biến chương trình thành một weird machine
- Trong phần mềm đủ phức tạp, kể cả không có UIB thì chương trình vẫn có thể bị xoắn theo những cách ngoài ý muốn
- Việc assert trở thành false ở runtime là một tình huống đi lệch khỏi đặc tả, và bản thân nó đã có thể khiến chương trình thực hiện những việc không mong muốn
- SQL injection là một ví dụ cụ thể và phổ biến về kiểu hoạt động sai cấp weird-machine mà không cần UIB
- Nếu chi phí của việc chương trình hoạt động sai là quá lớn thì nên để assert bật
- Nếu hiệu năng cực kỳ quan trọng và có thể chấp nhận rủi ro hoạt động sai thì nên dùng assert như cơ hội tối ưu hóa
- Nếu vô hiệu hóa assert, bạn vừa bỏ lỡ hiệu năng vừa dễ tự huyễn hoặc rằng mình an toàn hơn thực tế
Cách assert sai đánh lừa cả codebase
- Rủi ro cốt lõi là assert sai có thể không lộ ra trong test mà chỉ thất bại ở production
- Nếu có thể bảo đảm mọi assert luôn đúng thì việc dùng assert cho tối ưu hóa sẽ không còn gây tranh cãi
- Nếu có thể bảo đảm test bắt được mọi assert sai thì tối ưu hóa trong production cũng trở nên an toàn
- Nhưng trên thực tế, lập trình viên có thể viết assert sai, và test cũng không chắc sẽ bắt được
- Khi tắt assert trong production, bạn đánh mất cơ hội phát hiện assert sai sớm nhất có thể
- Tệ hơn nữa là về sau mã khác sẽ tiếp tục được viết dựa vào chính assert sai đó
- Trong ví dụ, có giả định bằng assert rằng
processThingchỉ được gọi vớithingđã được khởi độngfn processThing(thing: Thing) void { // this function must always be invoked on // a thing that has already been started assert(thing.is_started); // ... } - Assert này có thể không thất bại trong test, và vì bị vô hiệu hóa trong production nên người ta có thể bỏ qua thực tế rằng ngoài đời nó vẫn có thể là false
- Nếu người dùng không quan sát thấy lỗi rõ ràng thì mọi thứ sẽ trông như không có vấn đề và việc phát triển vẫn tiếp tục
- Về sau, ai đó có thể thêm mã vì cho rằng
thingđã được khởi động, nên có thể gọibazmà không cần chuẩn bị thêmfn processThing(thing: Thing) void { // this function must always be invoked on // a thing that has already been started assert(thing.is_started); // ... // Since thing is already started, we don't // need to foo the bar before bazzing the qux. // It would be really bad to baz the qux otherwise, // so we add an assert for good measure. assert(thing.is_fooed); thing.baz(qux); } - Bản thân assert thứ hai có thể đúng về mặt logic, nhưng nếu assert đầu tiên thực ra có thể là false thì rủi ro sẽ xuất hiện
- Trong test, assert đầu tiên không thất bại nên assert thứ hai cũng không thất bại
- Trong production, vì assert đã bị vô hiệu hóa nên có thể không ai nhận ra đúng khoảnh khắc lỗ hổng lọt vào codebase
- Nếu các assert trong mã đang đánh lừa lập trình viên, thì việc viết mã đúng sẽ trở nên khó một cách vô lý
Lựa chọn phụ thuộc vào mức ưu tiên của chương trình
- Mỗi chương trình có mức ưu tiên khác nhau, và có những chương trình mà việc ưu tiên hiệu năng hơn là giảm tối đa rủi ro hoạt động sai là hoàn toàn hợp lý
- Trong trường hợp đó, biến assert thành cơ hội tối ưu hóa là một lựa chọn tự nhiên
- Việc vô hiệu hóa assert trong production theo quán tính bị đánh giá là lựa chọn kém hơn cả việc giữ assert bật lẫn việc chủ động tận dụng tối ưu hóa hiệu năng
- Zine là static site generator, hiện chủ yếu được dùng để build blog cá nhân
- Chưa có threat model rõ ràng, và đó cũng không phải ưu tiên hàng đầu
- Tác giả chọn phát hành bản build ReleaseFast vì thích việc nó chạy nhanh hơn Hugo tới một bậc độ lớn
- Awebo là một lựa chọn thay thế Discord có thể self-host, đang ở giai đoạn pre-alpha
- Đã rõ rằng đây là phần mềm xử lý dữ liệu riêng tư và sẽ lộ ra Internet
- Khi phát hành sẽ cung cấp bản build ReleaseSafe
- Tuy vậy, một số dependency cốt lõi như FFmpeg, Xiph Opus, SQLite sẽ được build bằng ReleaseFast
- Ở những dependency đó, tác giả đánh giá mức tăng hiệu năng rõ ràng quan trọng hơn việc giảm thêm rủi ro chương trình hoạt động sai
Lựa chọn của các dự án thực tế và các trường hợp bảo mật
- TigerBeetle là cơ sở dữ liệu tài chính và luôn để assert bật
- Ghostty là terminal emulator và phát hành bản build ReleaseFast cho macOS
- Dự án cũng khuyến nghị cách làm tương tự cho các downstream consumer, chẳng hạn các maintainer bản phân phối Linux
- Hai CVE công khai tương đối nghiêm trọng của Ghostty đều là trường hợp có thể thực thi lệnh tùy ý mà không cần memory corruption
- Memory corruption hay UIB không phải là toàn bộ câu chuyện về rủi ro
Những assert ngầm định không thể biến mất hoàn toàn trong Zig
- Dù assert tự viết có thể bị vô hiệu hóa, những assert mà bản thân ngôn ngữ Zig ngầm chèn vào mã thì không thể tắt
- Tràn số nguyên, chia cho 0, vượt phạm vi mảng... đều thuộc nhóm này
- Những điều kiện đó либо gây runtime panic, либо được dùng phục vụ tối ưu hóa
- Thói quen vô hiệu hóa assert trong production có thể khiến các assert sai âm thầm mục ruỗng và lan rộng trong codebase
- Kết quả là sự hoang tưởng về UIB tăng lên, và lập trình viên có thể vô thức sợ bật lại assert để đối diện với hậu quả
- Kết luận khó tránh là không nên che đậy bằng cách tắt assert mà phải sửa những assert sai
- Tính đúng đắn của chương trình phải được theo đuổi trên toàn bộ hệ thống, chứ không phải chỉ một tập con nào đó
1 bình luận
Ý kiến trên Lobste.rs
Tôi đồng ý rằng trong
assert, việc đơn giản làm chương trình crash, hoặc chỉ làm crash tác vụ như panic của Rust, nhìn chung là lựa chọn tốt nhất. Nhưng tôi khó đồng ý rằng dùngassertnhư gợi ý tối ưu hóa luôn tốt hơn việc просто loại bỏ nóThứ nhất,
asserttùy ý nhiều khi không giúp ích mấy cho tối ưu hóa, và cũng có nhiều điều kiện mà trình tối ưu hóa không thể tận dụng ngay. Nếu không phải là những giả định trực tiếp như “nhánh này tuyệt đối không bao giờ chạy”, thì việc rải các giả định ngẫu nhiên khắp mã có lẽ không đem lại lợi ích hiệu năng lớnThứ hai, khi biến
assertthành giả định, bán kính ảnh hưởng của sai sót sẽ tăng lên rất nhiều. Ví dụ, trong một hệ thống xử lý dữ liệu được tách riêng theo từng project hoặc từng người dùng, giả sử có mộtassertở giữa hàm tính toán để bắt một trạng thái vốn dĩ không thể xảy ra. Nếu trong bản build phát hành nó bị tắt vì chi phí cao, thì nếu chỉ đơn thuần vô hiệu hóa, thiệt hại có thể chỉ giới hạn ở một project hay một người dùng và còn có thể bị phát hiện ở các bước kiểm tra sau. Ngược lại, nếu biến nó thành hành vi không xác định, phép tính có thể nhảy sang đoạn mã hoàn toàn sai, phá hỏng bộ nhớ một cách tùy tiện và làm hỏng dữ liệu của mọi projectCuối cùng, nếu chọn
assertkhông an toàn làm mặc định cho bản build phát hành, thì tức là bạn đang vội vàng tối ưu hóa các điểm ngẫu nhiên trong mã để đổi lấy việc giảm khả năng cô lập thiệt hại khi có sự cố. Tôi cho rằng Rust được thiết kế khá tốt:assert!()luôn panic,debug_assert!()chỉ panic trong chế độ debug, cònassert_unchecked()thì panic ở debug và trở thành gợi ý tối ưu hóa ở bản releaseReleaseSafethay vìReleaseFastassertriêng lẻ, mà phản đối việc tắt hàng loạt như một thực hành khuyến nghị chungViệc kết luận rằng tác động hiệu năng quá lớn nên không thể giữ nó trong build phát hành là hoàn toàn hợp lý. Hơn nữa, như đã nói ở trên, các
assertcó chi phí tính toán cao cũng hầu như không có khả năng dẫn đến cải thiện hiệu năngZine cũng có vài ví dụ như vậy:
https://github.com/kristoff-it/zine/…
https://github.com/kristoff-it/zine/…
Zig không có “chế độ release mặc định”. Bạn luôn phải tự chọn cách xử lý
assert, và tùy chọn áp dụng toàn cục là crash hoặc tối ưu hóa; không bên nào có thể xem là mặc định hơn bên nàoViệc cả hai CVE tương đối nghiêm trọng đã được công bố cho đến nay của Ghostty đều dẫn đến thực thi lệnh tùy ý mà không cần làm hỏng bộ nhớ khiến tôi thấy rất lạ. Việc điều đó vẫn xảy ra dù được phát hành bằng ReleaseFast hoàn toàn trái với hiểu biết của tôi về cách thế giới này vận hành
Với kinh nghiệm từng làm về terminal emulator, các lỗ hổng này đúng là kiểu vấn đề đau đầu rất dễ đoán trước. Không phải để hạ thấp nhà phát triển hay nhà nghiên cứu, nhưng kiểu chèn lệnh ở những chỗ kỳ quặc như vậy gần như là vấn đề đi kèm trong lĩnh vực này, tương tự như ở các mảng khác sẽ đi kèm những lỗ hổng injection kiểu khác
Thật buồn cười khi tôi đã nghe lập luận rằng nên tắt
assertvà kiểm tra biên trong production “vì hiệu năng” suốt gần 40 năm rồi. Trong thời gian đó máy tính đã nhanh hơn biết bao nhiêu bậc độ lớn, và phần mềm cũng đã ăn sâu vào cuộc sống của mọi người hơn rất nhiều, nên tính đúng đắn khi chạy thực tế quan trọng hơn bao giờ hếtNếu nói theo hướng tích cực hơn, ngày xưa ở Microsoft có một loại assert để báo cáo mà ngoài các
assert,checkthông thường ra thì tôi hiếm thấy ở nơi khác. Nó dùng khi có một điều kiện mà tôi không hoàn toàn kiểm soát được; tôi giả định nó là đúng, nhưng vẫn xử lý phòng thủ nếu nó sai, đồng thời muốn biết qua log hay telemetry từ hiện trường xem trên thực tế nó có trở thành sai hay không. Ví dụ như giả định người dùng sẽ không đưa quá 1000 mục vào một danh sách nên dùng thuật toán bậc hai, hoặc giả định độ trễ mạng dưới 200ms nên dùng một giao thức có nhiều vòng khứ hồicheck?Là một trong những người được liên kết ở đây, tôi thấy bài này đã biến suy nghĩ của tôi về
assertthành một ngụy biện lưỡng phân lố bịch và đầy tính biếm họa. Như tôi đã viết trong một bình luận khác, tôi thích quyết định việc có chuyển sang hành vi không xác định hay không theo từngassert. Phê phán của tôi với ReleaseFast là nó gộp lựa chọn đó không chỉ với mọiasserttrong một phạm vi nhất định mà còn với mọi kiểm tra an toànTôi đồng ý với kristoff rằng việc tắt một
assertchưa được sửa chỉ vì nó gây crash là điều ngớ ngẩn. Tuy vậy, tôi không đồng ý rằng chỉ có “crash hoặc hành vi không xác định” mới là những phương án thay thế hợp lý. Ý kiến của goldstein ở bình luận cùng nhánh gần với suy nghĩ của tôi hơnRất khó biện hộ cho việc biến hành vi của
assert_unchecked()thành mặc định toàn cục, nhưng như một kỹ thuật tối ưu hiệu năng thì nó có thể hợp lý. Nếu việc biến mọiassertthành giả định làm build production nhanh hơn đáng kể, thì có thể tồn tại một số ít giả định, hy vọng chỉ một giả định, tạo ra phần lớn mức cải thiện hiệu năng, và có thể tìm ra chúng bằng cách như tìm kiếm nhị phânReleaseSafevàReleaseFast/ReleaseSmallTrong tài liệu phân tích chương trình có một tính đối ngẫu chia các mệnh đề khẳng định hoặc
asserttrong mã thành hai dạng. Một là điều kiện về ngữ cảnh xung quanh mã, với hàm thì là điều kiện mà bên gọi phải thỏa mãn; dạng kia là điều kiện về chính bản thân mã, với hàm thì là điều kiện mà hàm phải thỏa mãnSự phân biệt này trở nên rõ ràng nếu nhìn qua khái niệm học thuật chuẩn trong tài liệu về hợp đồng và kiểu dần dần là “trách nhiệm” (blame). Nếu mệnh đề về ngữ cảnh thất bại thì không phải lỗi của chúng ta mà trách nhiệm thuộc về ngữ cảnh hoặc bên gọi, dù cũng có thể bên gọi đúng và chính mệnh đề đó mới là bug. Nếu mệnh đề về bản thân mã thất bại thì trách nhiệm thuộc về chúng ta, dù cũng có thể mã đúng và chính mệnh đề đó mới là bug
Ở cấp độ hàm, tiền điều kiện là mệnh đề về ngữ cảnh, còn hậu điều kiện là mệnh đề về bản thân mã. Tuy vậy cả hai đều có thể được đặt ở giữa mã. Một số framework kiểm chứng dùng
assertcho các mệnh đề về mã vàassumecho các mệnh đề về ngữ cảnh. Điều này cũng liên quan tới cách một số framework kiểm thử, đặc biệt là framework kiểm thử ngẫu nhiên, diễn giải chúng. Nếuassertthất bại thì đánh dấu là test thất bại, còn nếuassumethất bại thì bỏ qua testCó vẻ điều này đang ám chỉ Bun, nên tôi muốn làm mối liên hệ đó chính thức hơn một chút. Có một issue của Zig do Jarred Sumner, tác giả Bun, đề xuất năm 2024 rằng
unreachablenên panic trong ReleaseFast. Các bình luận của Andrew Kelley và Matthew Lugg trong thread đó có liên quan đến thảo luận này=> https://github.com/ziglang/zig/issues/19664
Bun dùng các hàm
assertriêng, và trong chế độ phát hành thì chúng panic hoặc bị loại bỏ, nhưng không đưa vào hành vi không xác định. Tuy vậy cũng cần nhớ chú thích của Loris: “với tư cách là một ngôn ngữ, Zig ngầm thêm rất nhiềuassertkhông thể vô hiệu hóa vào mã”Tôi không muốn nói quá dài về Bun, vì đó là một dự án đơn lẻ của một đội nhỏ. Điểm cốt lõi là nếu có dù chỉ một chút lo ngại thì hãy dùng ReleaseSafe. ReleaseSafe có tiếng là chậm, nhưng trong các dự án Zig quy mô nhỏ của tôi, tôi không đo được chênh lệch benchmark giữa ReleaseSafe và ReleaseFast. Dù vậy nó có lẽ vẫn nhanh hơn nhiều ngôn ngữ khác
Hoặc nếu phù hợp với bối cảnh, bạn có thể phát hành bản thực thi ReleaseFast rồi nếu bắt đầu nhận các báo cáo bug không xác định do hành vi không xác định thì quay lại ReleaseSafe. Khi đó bạn có thể thu thập các báo cáo bug có thể hành động được về
assertnào đã thất bại, kể cả truy cập ngoài phạm vi hay tràn số, rồi sửa mã. Tôi còn khuyến nghị cách tiếp cận này ngay cả khi ban đầu bạn đã quyết định như vậy trong một bối cảnh lẽ ra không nên phát hành bằng ReleaseFast :^)Bạn cũng có thể điều chỉnh dependency và dùng
@setRuntimeSafetyđể áp dụng cách tương tự chỉ cho một phần của dự án. Cuối cùng, miễn là bạn có ý định làm cho thông minh thì mọi công cụ cần thiết đều đã có sẵnKhông nên viết theo kiểu có thể đặt biểu thức có tác dụng phụ vào trong lời gọi
assert. Đó là một thực hành tồi. Cũng nên tránh dùngassertđể kiểm tra lỗi. Công bằng mà nói thì tác giả có vẻ không chủ trương như vậyNgược lại, cũng có giải thích rằng nếu
assertphụ thuộc vào một phép tính phức tạp thì ở chế độ unchecked phép tính đó không nhất thiết bị loại bỏ, nên phải bảo vệ bằngcomptime ifTôi hy vọng tác giả không bỏ lỡ sự mỉa mai trong câu “một cơ hội tốt để vứt bỏ chấn thương do macro để lại và chấp nhận sự đơn giản”. Thực ra đó là đang bảo người đọc chấp nhận “sự đơn giản” của việc phải cân nhắc chế độ build của chương trình và rải
comptime ifmang tính phòng thủ khắp nơiTôi có viết một ít mã tính toán số bằng C#, và dùng rất nhiều
assertbị tắt trong bản phát hành. Chúng quá đắt nếu chạy trong mọi vòng lặp dày đặc, nhưng trong unit test thì rất hữu ích khi routine nổ tung ngay khoảnh khắc đầu tiên nó thấy đầu vào NaNNhững giá trị NaN như vậy thường không đến từ đầu vào người dùng mà phát sinh do bug trong mã, chẳng hạn optimizer đi tới nơi không nên đi, và khi đó cần các ràng buộc biên tốt hơn. Tất nhiên đầu vào người dùng có thể cần được làm sạch, nhưng việc đó nên làm ở ranh giới ngoài cùng chứ không phải sâu trong thuật toán. Sẽ thật tuyệt nếu có một hệ thống chứng minh cho phép chứng minh các bất biến nội bộ của thuật toán mà không cần
assert, như là kết quả của việc làm sạch đầu vào người dùng, nhưng đây là một side project và nếu nó nổ thì cũng chẳng ai chết90% bất đồng về
assertxuất phát từ việc định nghĩa của từ này quá kém và mang nhiều nghĩa, khiến việc suy nghĩ và giao tiếp bị mờ đi. Vì vậy nên tách khái niệm này thành ba tên gọi sau và dùng chúng một cách nghiêm ngặtassert(bool)hoặc trong Rust làassert_unchecked()là thứ mà lập trình viên tin là luôn đúng, và compiler cũng giả định là luôn đúng để dùng cho tối ưu hóa. Để tránh gợi liên tưởng với kiểu assert có kiểm tra của các ngôn ngữ cũ, có lẽ nên gọi nó làassume()check(bool)sẽ panic nếu điều kiện sai và tiếp tục nếu đúng, và nó luôn hoạt động như vậydebug_check(bool)thì trong chế độ debug sẽ giốngcheck(), còn trong chế độ phát hành thì luôn tiếp tục. Trên thực tế nó được điều khiển bằng cờ--debug_checks, mặc định bật trong chế độ debugNgoài ra còn cần cờ compiler
--check_assertsđể biếnassert()thànhcheck(). Nó được dùng khi bạn nghi ngờ chính cácassertcủa mình và muốn xác minh chúng, và trong chế độ debug thì mặc định được bật. Nếu không cực kỳ rõ ràng rằng khi nói “assert” là đang chỉ cái gì, thì không thể có thảo luận nghiêm túc, chỉ tổ tốn lời