32 điểm bởi GN⁺ 2025-12-07 | 1 bình luận | Chia sẻ qua WhatsApp
  • Giới thiệu thói quen viết mã giúp ngăn chặn lỗi từ sớm bằng cách tận dụng tích cực hệ thống kiểu và trình biên dịch của Rust
  • Đưa ra các ví dụ về mùi mã (Code Smell) dễ gây lỗi như đánh chỉ mục vector, lạm dụng Default, match không đầy đủ, tham số boolean không cần thiết, đồng thời giải thích các phương án thay thế
  • Nguyên tắc cốt lõi là thiết kế cấu trúc để trình biên dịch ép buộc các bất biến, bằng cách tận dụng pattern matching, trường riêng tư, thuộc tính #[must_use], v.v.
  • Trình bày cụ thể các kỹ thuật phòng thủ ở cấp độ mã nguồn thực tế như dùng TryFrom, phân rã đầy đủ struct, tính khả biến tạm thời, xác thực trong hàm khởi tạo
  • Những mẫu này rất cần thiết để đảm bảo tính ổn định khi refactor và nâng cao khả năng bảo trì lâu dài

Tổng quan về lập trình phòng thủ

  • Những chỗ có chú thích // this should never happenvị trí mà các bất biến ngầm định đã bị phá vỡ
    • Trong đa số trường hợp, lập trình viên không lường hết mọi điều kiện biên hoặc các thay đổi mã trong tương lai
  • Trình biên dịch Rust đảm bảo an toàn bộ nhớ, nhưng lỗi trong logic nghiệp vụ vẫn hoàn toàn có thể xảy ra
  • Các mẫu thói quen nhỏ (idiom) rút ra từ nhiều năm kinh nghiệm thực tế có thể cải thiện đáng kể chất lượng mã

Code Smell: đánh chỉ mục vector

  • Dạng if !vec.is_empty() { let x = &vec[0]; }rủi ro panic lúc chạy vì việc kiểm tra độ dài và đánh chỉ mục bị tách rời
  • Nếu dùng pattern matching trên slice (match vec.as_slice()), trình biên dịch sẽ buộc kiểm tra mọi trạng thái
    • Có thể xử lý tường minh mọi trường hợp như vector rỗng, một phần tử, phần tử trùng lặp, v.v.
  • Đây là ví dụ tiêu biểu của việc thiết kế để trình biên dịch đảm bảo bất biến

Code Smell: sử dụng Default bừa bãi

  • ..Default::default() gây ra vấn đề dễ bỏ sót khi thêm trường mớithiết lập giá trị ngầm định
  • Nếu khởi tạo tường minh tất cả các trường, trình biên dịch sẽ buộc phải thiết lập các trường mới
  • Có thể phân rã struct giá trị mặc định rồi ghi đè có chọn lọc bằng dạng let Foo { field1, field2, .. } = Foo::default();
    • Giúp cân bằng giữa việc giữ giá trị mặc định và ghi đè một cách tường minh

Code Smell: cài đặt Trait mong manh

  • Khi so sánh bằng cách phân rã đầy đủ các trường của struct, việc thêm trường mới sẽ tạo lỗi biên dịch để cảnh báo
    • Ví dụ: trong cài đặt PartialEq, dùng let Self { size, toppings, .. } = self;
  • Nếu thêm trường mới như extra_cheese, logic so sánh sẽ buộc phải được rà soát lại
  • Nguyên lý này cũng áp dụng tương tự cho các trait khác như Hash, Debug, Clone

Code Smell: cần TryFrom thay vì From

  • Nếu phép chuyển đổi không phải lúc nào cũng thành công, hãy diễn đạt khả năng thất bại bằng TryFrom thay vì From
  • Việc dùng unwrap_or_elsedấu hiệu che giấu thất bại tiềm ẩn, trong khi cách fail fast an toàn hơn

Code Smell: match không đầy đủ

  • Các mẫu catch-all như _ => {}nguy cơ bỏ sót khi thêm variant mới
  • Nếu liệt kê tường minh mọi variant, trình biên dịch sẽ cảnh báo khi thiếu xử lý case mới
  • Cùng một logic vẫn có thể được nhóm lại theo dạng Variant3 | Variant4

Code Smell: lạm dụng placeholder _

  • Chỉ dùng _ sẽ khiến không rõ biến nào đã bị lược bỏ
  • Viết như has_fuel: _, has_crew: _ sẽ cải thiện khả năng đọc nhờ tên gọi tường minh

Pattern: tính khả biến tạm thời (Temporary Mutability)

  • Khi dữ liệu chỉ nên khả biến trong lúc khởi tạo, có thể dùng dạng let mut data = ...; data.sort(); let data = data;
  • Nếu tận dụng phạm vi khối, có thể tránh để lộ biến tạm ra bên ngoài
    • Ví dụ: let data = { let mut d = get_vec(); d.sort(); d };
  • Trong quá trình khởi tạo dùng nhiều biến tạm, cách này giúp phân tách phạm vi rõ ràng

Pattern: buộc xác thực trong hàm khởi tạo

  • Khi tạo struct, cần ép buộc phải đi qua logic xác thực
    • Nếu thêm trường _private: (), bên ngoài sẽ không thể khởi tạo trực tiếp
    • Thuộc tính #[non_exhaustive] có tác dụng chặn việc khởi tạo từ ngoài crate và báo hiệu khả năng mở rộng trong tương lai
  • Nếu muốn ép buộc cả trong module nội bộ, có thể dùng cấu trúc module lồng nhau với kiểu riêng tư (Seal)
    • Seal chỉ tồn tại ở bên trong, không thể khởi tạo trực tiếp ngoài new()
  • Nếu để các trường ở chế độ riêng tư và cung cấp getter, có thể duy trì trạng thái bất biến
  • Tiêu chí áp dụng
    • Chặn mã bên ngoài: _private hoặc #[non_exhaustive]
    • Chặn mã nội bộ: module riêng tư + Seal
    • Chuyển logic xác thực thành bảo đảm ở cấp độ trình biên dịch

Pattern: tận dụng thuộc tính #[must_use]

  • #[must_use] giúp ngăn việc bỏ qua các giá trị trả về quan trọng
    • Ví dụ: #[must_use = "Configuration must be applied to take effect"]
  • Nếu người dùng bỏ qua giá trị trả về, trình biên dịch sẽ phát cảnh báo
  • Đây là biện pháp phòng thủ đơn giản nhưng mạnh mẽ, cũng được dùng rộng rãi trong thư viện chuẩn như với Result

Code Smell: tham số boolean

  • Dạng fn process_data(..., compress: bool, encrypt: bool, validate: bool)ý nghĩa không rõ ràng và dễ lỗi do sai thứ tự
  • Có thể dùng enum Compression, enum Encryption, v.v. để biểu đạt ý định một cách tường minh
  • Nếu có nhiều tuỳ chọn, nên dùng struct tham số (Params struct)
    • Các phương thức cấu hình sẵn như ProcessDataParams::production() giúp tăng khả năng tái sử dụng
  • Khi thêm tuỳ chọn mới, ảnh hưởng tới các chỗ gọi hiện có sẽ được giảm thiểu

Tự động hoá bằng Clippy Lints

  • Các mẫu phòng thủ quan trọng có thể được tự động kiểm tra bằng lint của Clippy
    • indexing_slicing: cấm đánh chỉ mục trực tiếp
    • fallible_impl_from: khuyến nghị TryFrom thay vì From
    • wildcard_enum_match_arm: cấm mẫu _
    • fn_params_excessive_bools: cảnh báo quá nhiều tham số boolean
    • must_use_candidate: đề xuất các ứng viên cho #[must_use]
  • Có thể áp dụng cho toàn dự án bằng #![deny(clippy::...)] hoặc cấu hình trong Cargo.toml

Kết luận

  • Cốt lõi của lập trình phòng thủ trong Rust là tận dụng tích cực hệ thống kiểu và trình biên dịch để biến các bất biến thành thứ tường minh và có thể được kiểm chứng
  • Những mẫu này góp phần đảm bảo ổn định khi refactor, giảm thiểu khả năng phát sinh lỗi và tăng cường khả năng bảo trì lâu dài
  • Đây là cách tiếp cận hiện thực hoá nguyên tắc “lỗi tốt nhất là lỗi không thể biên dịch

1 bình luận

 
GN⁺ 2025-12-07
Ý kiến Hacker News
  • Bài viết hay. Tuy vậy, ví dụ PizzaOrder tạo cảm giác nhồi quá nhiều mối quan tâm vào một struct
    Nếu mục đích là loại ordered_at khỏi phép so sánh, tôi nghĩ tách thành hai struct PizzaDetailsPizzaOrder sẽ tốt hơn
    Làm vậy sẽ giúp việc chỉ so sánh details khi triển khai PartialEq trở nên rõ ràng hơn

    • Nhận xét rất hay. Nhưng về mặt logic, tôi vẫn cho rằng đây là mô hình hóa sai
      Nếu thời điểm đặt hàng khác nhau thì đó không phải cùng một đơn hàng, nên việc định nghĩa chúng là bằng nhau ở cấp độ kiểu dữ liệu là nguy hiểm
      Đặt PartialEq trên PizzaDetails thì ổn, nhưng logic so sánh đơn hàng nên nằm trong một hàm nghiệp vụ riêng
    • Cách tiếp cận tách cấu trúc là tốt, nhưng vấn đề là khi sửa PizzaDetails, thay đổi đó có thể ảnh hưởng đến logic loại bỏ pizza trùng lặp
      Lý tưởng nhất là struct chỉ nên được dùng để gom dữ liệu lại với nhau
      Để thay đổi không ảnh hưởng sang nơi khác, cũng có thể cân nhắc đặt một kiểu riêng như PizzaComparator hoặc PizzaFlavor
      Sẽ hay nếu giống Protobuf, có thể đặt chú thích trường như {important_to_flavour=true} cho từng field
    • Việc tách cấu trúc chỉ để phục vụ một cách so sánh khác thì không thể khái quát hóa
      Ví dụ nếu muốn so sánh chuỗi mà không phân biệt chữ hoa chữ thường thì sẽ tách kiểu thế nào?
  • Một điểm thực sự tuyệt vời ở Rust là có rất nhiều trường hợp không cần lập trình phòng thủ
    Nhờ quy tắc ownership và reference, ta có thể được đảm bảo rằng quyền truy cập đến một đối tượng cụ thể là duy nhất trong toàn bộ chương trình
    Reference không thể là null, và smart pointer cũng không thể là null
    Hệ thống kiểu cũng đảm bảo rằng nếu chuyển quyền sở hữu của self thì sau đó không thể tiếp tục gọi phương thức nữa
    Nhờ vậy, tính an toàn luồng, vòng đời, khả năng sao chép v.v. đều được kiểm chứng toàn cục ở thời điểm biên dịch

    • Tôi cũng nghĩ điểm mạnh thực sự của Rust nằm ở những thứ mà ta “không phải bận tâm đến”
      Ở các ngôn ngữ khác, phải giữ tính bất biến theo phong cách hàm mới có được lợi ích đó, còn Rust thì ép buộc bằng hệ thống kiểu
    • Nhưng có vẻ bình luận này không liên quan đến bài gốc
      Chủ đề của bài là những lỗi logic mà borrow checker cũng không bắt được
    • Nội dung bài chủ yếu tập trung vào các mẫu mã giúp tránh sai lầm logic khi cải tiến chương trình lặp đi lặp lại
  • Tôi thấy tránh lập chỉ mục trực tiếp vào mảng hay vector là một lựa chọn khôn ngoan
    Đúng ngày xảy ra sự cố unwrap của Cloudflare, tôi cũng phát hiện một lỗi slice vượt quá cuối vector
    Từ đó tôi chuyển sang cách tiếp cận dựa trên iterator và cảm thấy an toàn hơn nhiều

    • Tôi không nghĩ cần coi sự cố unwrap là một “tai nạn”
      unwrap trong Rust giống như assert trong C. Nó chỉ có nhiệm vụ báo cho ta biết khi có vấn đề xảy ra
      Trong Rust, ta vẫn hoàn toàn có thể viết ra bug
    • Rốt cuộc vẫn là cùng một vấn đề. Phía Rust thì kêu gọi bỏ C, nhưng ngay trong C, việc dùng handle thay vì chỉ số cũng đã là chuyện phổ biến
  • Một trong những thói quen mà lập trình viên Rust cần đề phòng là thêm phụ thuộc crate không cần thiết
    Rust có xu hướng khuyến khích kiểu thói quen này. Ví dụ, việc Rust Book dùng crate rand trong ví dụ cơ bản cũng tạo ra bầu không khí như vậy
    Tất nhiên đây là một lựa chọn mang tính chiến lược để có thể dễ dàng thay thế các gói liên quan đến mật mã, nhưng việc nó trở thành thói quen vẫn là vấn đề

    • Ban đầu tôi cũng có ác cảm với Rust vì ví dụ đó
      Nhưng sau này tôi hiểu được dụng ý của nó và đã đổi suy nghĩ
  • Phần triển khai đẳng thức từng phần khá thú vị
    Một điều nữa tôi tò mò là cách dùng enum khi muốn tránh tham số boolean
    Tôi thường dùng struct bọc quanh bool, nhưng thấy tiếc vì không thể dùng nó như bool thông thường
    Tôi tự hỏi có cách nào để dùng enum như bool hay không

    • Tôi cũng gần như luôn ưu tiên enum + match!
      Tôi xử lý bằng cách gom logic cần thiết vào Trait, hoặc thêm các phương thức dùng chung trong khối impl <Enum>
      Làm vậy vừa dễ đọc, vừa có thể định nghĩa rõ hành vi cho từng thành viên
    • Cũng có thể thử dùng impl Deref, nhưng tôi không chắc đó có phải ý hay hay không
  • Câu lệnh match trong ví dụ đầu tiên có cảm giác hơi quá tay
    Vec.first() hoặc Vec.iter().nth(0) rõ ràng hơn và đúng với ý định hơn

    • Tôi cũng đồng ý. Dùng match lại thành ra giải pháp phức tạp hơn vấn đề
      Nếu có thể bỏ if thì cũng có thể bỏ match, nên về mặt an toàn không có khác biệt gì
      first() ngắn gọn và rõ ràng hơn nhiều
    • Nếu muốn diễn đạt cùng hành vi theo cách đơn giản hơn nữa, cũng có thể dùng exactly_one của itertools
    • Tuy vậy, match vẫn có ý nghĩa ở chỗ nó thúc đẩy ta xử lý cả trường hợp “có từ một phần tử trở lên”
      Nói cách khác, nó thể hiện nguyên tắc tránh tách rời phần kiểm tra và phần mã phụ thuộc vào nó
  • Mỗi lần đọc những bài như thế này, tôi lại tự hỏi vì sao không có một đội chuyên trách theo dõi các mẫu mã
    Sẽ rất tốt nếu có một nhóm giống SOC hay QA để quan sát các pattern trong codebase về dài hạn
    Các công cụ tự động phát hiện code smell có giới hạn của chúng

    • Công ty tôi (quy mô khoảng 300 người) có một đội chuyên xử lý nợ kỹ thuật làm đúng vai trò này
      Họ phụ trách quản lý quy tắc lint, tài liệu hóa, đào tạo lập trình viên và bảo trì thư viện dùng chung
      Khi nhiều đội lặp lại cùng một vấn đề, họ sẽ thiết kế API cốt lõi để hợp nhất nó
    • Ở các công ty công nghệ lớn thì hầu như đều có những đội như vậy
      Tuy nhiên, thực tế là khi mã nguồn lên đến hàng triệu dòng thì việc quản lý trở nên cực kỳ khó khăn
  • Tôi đang suy nghĩ làm sao để khuyến khích các mẫu lập trình tốt này trong nội bộ nhóm
    Trong lúc review code, nó thường biến thành “tranh cãi về phong cách” và trở nên thiếu hiệu quả
    Nhưng kỳ lạ là khi linter đưa ra cảnh báo, những cuộc tranh cãi như vậy gần như biến mất

  • Việc trait TryFrom được thêm vào ở phiên bản 1.34 thực sự rất hữu ích
    Có lẽ đoạn mã dùng unwrap_or_else() là tàn dư từ thời kỳ trước đó
    Tài liệu về trait From giờ đây giải thích rất rõ khi nào nên triển khai nó

    • Tôi vẫn đang học Rust, và cái tên unwrap_or_else() nghe khá buồn cười, như thể đang “ra lệnh kiểu đe dọa cho máy tính” vậy
  • Tôi nghĩ những mẫu lập trình phòng thủ như thế này cũng sẽ giúp cải thiện chất lượng sinh mã AI ở quy mô lớn
    Phản hồi cụ thể từ Clippy hay trình biên dịch Rust có thể đóng vai trò rất lớn trong việc giúp các tác nhân AI giảm sai sót và định hướng đúng hơn