Mục lục
Mục Tiêu Bài Học
Sau bài học, bạn sẽ:
- Hiểu vì sao cú pháp inline
<T: A + B + C>trở nên khó đọc khi số bound tăng — nhồi tên type parameter và trait bound vào cùng dòng signature. - Viết được cú pháp
wheretách rời: declare type parameter trần trong<>, bound đẩy xuống blockwheregiữa signature và thân hàm. - Khẳng định inline và
wheretương đương tuyệt đối về ngữ nghĩa — chỉ khác cách trình bày, không khác hành vi compile/runtime. - Nắm các trường hợp
wherebắt buộc: bound trên expression nhưVec<T>: Displayhoặcfn() -> T: Sendmà inline không biểu diễn được. - Dùng
wheretrênimplblock để giữ signature impl gọn, và trên method riêng để conditional method. - Format
wheretheo convention rustfmt: multi-line, mỗi bound một dòng cho readability. - Preview lifetime bound trong
wherenhư'a: 'b— chủ đề chính của Group 23.
Vấn Đề: Bound Inline Dài Khó Đọc
Signature dùng inline bound:
use std::fmt::{Debug, Display};
use std::hash::Hash;
fn process<T: Display + Clone + Debug + PartialEq, U: Hash + Eq + Send + Sync>(
t: T,
u: U,
) -> String {
format!("{t} {t:?}")
}
Đọc dòng đầu này không dễ chịu: mắt phải đồng thời theo dõi tên parameter (T, U), trait dán lên mỗi cái, và dấu phân cách (: giữa parameter và bound, + giữa các trait, , giữa các parameter). Khi signature dài đến mức wrap sang dòng thứ hai, mỗi format tool đặt dấu xuống dòng một kiểu khác nhau làm reviewer thêm khó chịu.
Có thêm vấn đề: tên type parameter (T) và bound (Display + Clone + Debug + PartialEq) là hai khái niệm khác cấp — type parameter là danh tính của type, bound là thuộc tính của nó. Trộn lẫn cả hai vào trong dấu <> khiến reader phải tốn công tách ý nghĩa ra. Ở góc độ kỹ thuật, một số bound còn không thể biểu diễn inline: ví dụ ràng Vec<T>: Display hay fn() -> T: Send đặt bound lên một expression, không phải lên một type parameter riêng lẻ — cú pháp <T: ...> không có chỗ chứa được. where giải quyết cả hai vấn đề này cùng lúc.
Cú Pháp where
Cú pháp tổng quát: declare type parameter trần trong <>, sau đó viết từ khoá where, rồi liệt kê bound theo dạng Type: Trait + Trait + ... cách nhau dấu phẩy.
use std::fmt::{Debug, Display};
fn foo<T, U>(t: T, u: U)
where
T: Display + Clone,
U: Debug + 'static,
{
println!("{t}");
println!("{u:?}");
let _cloned = t.clone();
}
fn main() {
foo(String::from("hello"), 42);
}
Vị trí where: ngay sau danh sách parameter (t: T, u: U) (kèm return type nếu có), và trước dấu { mở thân hàm. Mỗi bound viết theo dạng Type: trait1 + trait2; nhiều bound ngăn nhau bằng dấu phẩy; rustfmt đặt mỗi bound trên một dòng riêng cho dễ đọc.
Để ý: trong dấu <T, U> giờ chỉ có tên type parameter, không có thông tin trait. Toàn bộ ràng buộc dồn vào block where — signature ngắn lại, bound nhìn rõ hơn.
Tương Đương Với Inline
Hai signature sau hoàn toàn tương đương với compiler:
// Dạng inline
fn a<T: Display + Clone>(t: T) { /* ... */ }
// Dạng where
fn b<T>(t: T)
where
T: Display + Clone,
{
/* ... */
}
Cả hai sinh ra cùng một function signature, cùng một bound set, cùng một monomorphization. Compiler không phân biệt — error message hiển thị bound y nhau, ABI giống nhau, performance giống nhau. Đây là điều quan trọng cần nhấn mạnh: where là syntactic sugar đặt cùng tầng với inline, không phải feature riêng có sức mạnh thêm — chỉ là cách viết khác.
Có thể trộn hai dạng trong cùng một signature: fn foo<T: Display, U>(t: T, u: U) where U: Debug — hợp lệ, một số bound inline, một số xuống where. Tuy nhiên convention idiomatic là chọn một: hoặc tất cả inline, hoặc tất cả xuống where, để consistent. Trộn lẫn trông lộn xộn và làm reviewer phải nhìn cả hai chỗ.
Khi Nào Dùng where
Convention thực dụng:
- 1 bound đơn giản (
<T: Display>): inline — gọn hơnwhere. - 2+ bound trên 1 parameter hoặc bound trên 2+ parameter: dùng
where— đáng đẩy xuống cho dễ đọc. - Bound phức tạp trên expression (không chỉ trên type parameter trần):
wherelà cách duy nhất.
use std::fmt::Display;
// Bound trên expression Vec<T> — chỉ where biểu diễn được
fn print_vec<T>(v: Vec<T>)
where
Vec<T>: Display,
{
println!("{v}");
}
// Bound trên associated type / kết quả của trait
fn iter_print<I>(iter: I)
where
I: IntoIterator,
I::Item: Display,
{
for item in iter {
println!("{item}");
}
}
Trường hợp đầu ràng Vec<T> (expression) phải impl Display — không phải T. Cú pháp <T: Display> không nói được điều này. Trường hợp thứ hai dùng I::Item — một associated type phụ thuộc I, lại càng không có chỗ trong inline. Đây là lý do where không chỉ là "version dễ đọc của inline" mà còn là syntactic superset: biểu diễn được nhiều bound hơn.
where Trên impl Block
where dùng được trên impl block y như trên hàm, đặt sau impl<...> Type<...> và trước dấu { mở body:
use std::fmt::{Debug, Display};
struct MyStruct<T> { inner: T }
impl<T> MyStruct<T>
where
T: Display + Clone + Debug,
{
fn show(&self) {
println!("{}", self.inner);
println!("{:?}", self.inner);
}
fn duplicated(&self) -> T {
self.inner.clone()
}
}
So với inline impl<T: Display + Clone + Debug> MyStruct<T> { ... }, dạng where giữ signature impl<T> MyStruct<T> gọn — đọc nhanh ra "đây là impl cho MyStruct<T>", rồi mới xuống bên dưới đọc bound. Trên các struct generic phức tạp (nhiều type parameter, bound dài), khác biệt readability rất rõ.
Mọi method bên trong impl block đều kế thừa bound where của block. Method nào cần bound thêm thì khai báo thêm trên signature của nó (xem mục 7).
where Trên Method
Method bên trong impl<T> Type<T> không bound có thể có where riêng — đây là cách thanh lịch để tạo method conditional mà không phải mở thêm impl block:
use std::fmt::Display;
struct Container<T> { value: T }
impl<T> Container<T> {
fn new(value: T) -> Self { Container { value } }
fn print(&self)
where
T: Display,
{
println!("{}", self.value);
}
fn duplicated(&self) -> T
where
T: Clone,
{
self.value.clone()
}
}
Container::new luôn có cho mọi T; print chỉ gọi được khi T: Display; duplicated chỉ gọi được khi T: Clone. Tất cả nằm trong cùng một impl block, mỗi method tự khai báo bound nó cần — đẹp và rõ. Cách viết bằng nhiều impl block riêng (như bài 155 mục 6) cũng đúng, nhưng where trên method giữ code gần nhau hơn, dễ navigate hơn.
Lưu ý: bound trên method cộng thêm vào bound của impl block (nếu có). Method không "lùi" được bound của impl — chỉ tăng được, không giảm.
Multiline Format Convention
rustfmt áp dụng quy tắc format đặc thù cho where:
// rustfmt format chuẩn
fn complex<T, U, V>(t: T, u: U, v: V) -> String
where
T: Display + Clone,
U: Debug + Send + Sync,
V: Iterator<Item = T>,
{
format!("{t} {u:?}")
}
Quy tắc: từ khoá where trên một dòng riêng, indent một mức (4 space) so với fn; mỗi bound chiếm một dòng, dấu phẩy ở cuối từng dòng (kể cả bound cuối — trailing comma); dấu { mở body đặt trên dòng kế tiếp, cùng indent với fn. Format này tối ưu cho diff Git: thêm/sửa/xoá một bound chỉ ảnh hưởng đúng một dòng, không ảnh hưởng dòng khác.
Khi where chỉ có một bound ngắn, rustfmt vẫn ưu tiên đẩy xuống multi-line cho consistent — đó là idiom đặc thù của Rust, hơi khác C++/TypeScript nơi people thường gom inline. Nếu thấy rustfmt format theo cách bạn không quen, hãy theo nó — toàn ecosystem Rust dùng cùng style nhờ rustfmt nên đọc code thư viện không cần thích nghi style mới.
Lifetime Trong where
where không chỉ nhận trait bound mà còn nhận lifetime bound. Cú pháp 'a: 'b đọc là "lifetime 'a sống lâu ít nhất bằng 'b" — gọi là lifetime subtyping (hoặc outlives):
fn combine<'a, 'b>(x: &'a str, y: &'b str) -> &'b str
where
'a: 'b, // 'a phải sống ít nhất bằng 'b
{
if x.len() > y.len() { x } else { y }
}
Bài này chỉ preview — lifetime là chủ đề rộng được dành hẳn Group 23 trong series. Điều cần ghi nhớ ở mức beginner: cú pháp where không bó hẹp vào trait bound, nó là generic constraint syntax chấp nhận cả lifetime constraint cùng nơi. Ngoài 'a: 'b, dạng T: 'static (T không chứa borrow ngắn hạn) cũng đặt được trong where y như trait bound thường — bài 155 dùng U: Debug + 'static ở ví dụ mục 3 cũng theo quy tắc này.
Tóm gọn: where = chỗ tập trung mọi ràng buộc generic, không phân biệt trait hay lifetime, không phân biệt parameter trần hay expression.
Tổng Kết
- Cú pháp
wheretách trait bound khỏi danh sách type parameter — declare tên parameter trần trong<>, bound xuống blockwheretrước thân hàm/impl. - Inline
<T: Trait>vàwhere T: Traittương đương tuyệt đối về ngữ nghĩa — compiler, ABI, performance giống hệt; chỉ khác cách trình bày. - Convention: 1 bound đơn giản dùng inline; 2+ bound hoặc bound phức tạp dùng
where. Bound trên expression nhưVec<T>: DisplayhoặcI::Item: Displaybắt buộc dùngwhere— inline không biểu diễn được. wheretrên impl block: giữ signatureimpl<T> Type<T>gọn, bound xuống dưới — đọc nhanh hơn khi bound dài.wheretrên method riêng trong impl block: tạo method conditional mà không cần mở thêm impl block riêng — code gần nhau, dễ navigate.- rustfmt format multi-line: từ khoá
wheredòng riêng, mỗi bound 1 dòng, trailing comma — tối ưu cho diff Git và consistent với toàn ecosystem. wherenhận cả lifetime bound:'a: 'b(lifetime subtyping),T: 'static— là chỗ tập trung mọi ràng buộc generic, không phân biệt trait/lifetime.- Lifetime sẽ được khai thác sâu ở Group 23; bài 156 chỉ preview để nhận diện cú pháp.
Bài Tập Củng Cố
Tự trả lời, đáp án ở cuối:
- Chuyển signature inline
fn report<T: Display + Clone, U: Debug + Send>(t: T, u: U)sang dạngwheretheo rustfmt format. Đặt mỗi bound một dòng và có trailing comma. - Viết hàm
print_iternhận tham số kiểuI, in mỗi item quaprintln!("{}"). Bound nào trênIvà bound nào trênI::Item? Vì sao bắt buộc dùngwhere? - Cho
struct Bag<T> { items: Vec<T> }. Viếtimpl<T> Bag<T>chứa:new()không bound,add(item)không bound,print()với boundT: Display(đặt where trên method, không trên impl). - Vì sao đoạn
fn foo<T>(v: Vec<T>) where Vec<T>: Display { println!("{v}"); }compile được nhưng gọifoo(vec![1, 2])sẽ báo lỗi? Hãy giải thích bằng khái niệm "bound chưa thoả mãn ở caller". - Đoạn sau dùng cả inline lẫn where:
fn mix<T: Display, U>(t: T, u: U) where U: Debug { ... }. Viết lại theo convention idiomatic (toàn bộ inline hoặc toàn bộ where) và giải thích lựa chọn.
Đáp án
fn report<T, U>(t: T, u: U). Mỗi bound 1 dòng, trailing comma cuối block.
where
T: Display + Clone,
U: Debug + Send,
{ /* ... */ }fn print_iter<I>(iter: I) where I: IntoIterator, I::Item: Display { for it in iter { println!("{it}"); } }. Bound thứ haiI::Item: Displaydán lên associated type, không phải lên parameter trần — không có cú pháp inline tương đương; chỉwherebiểu diễn được.impl<T> Bag<T> { fn new() -> Self { Bag { items: vec![] } } fn add(&mut self, item: T) { self.items.push(item); } fn print(&self) where T: Display { for x in &self.items { println!("{x}"); } } }.printcówhere T: Displayriêng — chỉ gọi được khiT: Display;newvàaddluôn dùng được.- Hàm compile được vì bound chỉ check ngay tại site khai báo (T trần, không yêu cầu gì); nhưng caller
foo(vec![1, 2])truyềnVec<i32>trong khiVec<i32>không implDisplay(Vec không impl Display) → bound không thoả mãn → caller báo E0277. Đây là lỗi tại caller, không phải tại định nghĩa — bound trên expression vẫn hợp lệ về cú pháp. - Toàn where (idiomatic hơn vì có 2 bound):
fn mix<T, U>(t: T, u: U) where T: Display, U: Debug { ... }. Trộn inline với where làm reviewer phải nhìn cả hai chỗ — consistent một dạng tốt hơn. Toàn inline cũng đúng:fn mix<T: Display, U: Debug>(t: T, u: U)— vẫn đọc được vì chỉ 2 bound đơn.
Bài Tiếp Theo
Bài 157: Monomorphization — Generic Không Tốn Runtime — bài 156 đã xong phần "cú pháp" của generic; bài 157 đào sâu phần "compile thế nào, sinh ra binary ra sao": monomorphization là quá trình rustc tạo concrete code cho mỗi instantiation cụ thể (mỗi Vec<i32>, Vec<String> sinh ra hai bản code riêng), khác hẳn Java erasure (xoá type tại runtime, dùng cast) và C++ template (template instantiation tương tự nhưng error message khó hơn nhiều); zero-cost abstraction nghĩa là gì, trade-off binary size khi nào đáng lo.
