Mục lục
Mục Tiêu Bài Học
Sau bài học, bạn sẽ:
- Hiểu khái niệm associated type — type member nằm trong thân trait, mỗi impl chốt một type cụ thể.
- Phân biệt associated type với generic trait parameter — biết lý do stdlib chọn associated type cho
Iterator,Add,Deref. - Viết được cú pháp
type Item;trong trait vàtype Item = i32;trong impl, tham chiếu quaSelf::Item. - Đọc hiểu chữ ký
fn next(&mut self) -> Option<Self::Item>trong traitIterator. - Dùng được trait
Addcho operator overload vớitype Outputtuỳ biến cho mỗi cặp toán hạng. - Viết được generic bound dạng
fn process<I: Iterator<Item = i32>>(iter: I)để ràng buộc associated type. - Áp dụng được tiêu chí chọn associated type vs generic trait khi tự thiết kế trait.
Associated Type Là Gì
Trait thường khai báo method — chữ ký hành vi. Nhưng trait còn được phép khai báo type — gọi là associated type, một "type member" của trait. Khi impl, ngoài viết body method, phải gắn cụ thể type đó.
trait Iterator {
type Item;
fn next(&mut self) -> Option<Self::Item>;
}
Ở đây Item là associated type. Trait Iterator không quan tâm cụ thể element là gì — chỉ ràng buộc "phải có một type tên Item" và "method next() trả Option chứa giá trị thuộc type đó". Mỗi impl tự chốt:
struct Counter { value: u32 }
impl Iterator for Counter {
type Item = u32;
fn next(&mut self) -> Option<Self::Item> {
self.value += 1;
if self.value <= 5 { Some(self.value) } else { None }
}
}
Với Counter, Item = u32; với impl Iterator for Lines chẳng hạn, Item = String. Mỗi cặp (Self, Trait) chỉ có đúng một Item — gắn xong là chốt.
Khác Generic Trait
Tại sao không dùng generic trait trait Iterator<T>? Trên giấy hai cách "đều truyền một type vào trait", nhưng ngữ nghĩa khác nhau.
// Phiên bản generic — chỉ để minh hoạ
trait GIterator<T> {
fn next(&mut self) -> Option<T>;
}
struct Mixed;
// Cùng một type, impl nhiều lần với T khác nhau — hợp lệ
impl GIterator<i32> for Mixed {
fn next(&mut self) -> Option<i32> { None }
}
impl GIterator<String> for Mixed {
fn next(&mut self) -> Option<String> { None }
}
Generic trait cho phép cùng một Self có nhiều impl với các T khác nhau. Hệ quả: khi gọi mixed.next(), compiler không biết bạn muốn i32 hay String — phải viết <Mixed as GIterator<i32>>::next(&mut mixed). Phiền hà mỗi lần dùng.
Associated type cấm trường hợp này. impl Iterator for Counter chỉ được khai báo đúng một type Item. Muốn iterator trả i32 và iterator trả String phải tạo hai struct khác nhau. Đổi lại: gọi counter.next() không cần annotation — compiler suy ra từ impl duy nhất.
Pattern thực tế: khi mỗi Self chỉ có một cách hợp lý để impl trait, associated type cho API gọn. Khi cần nhiều impl khác nhau trên cùng type (như From<T> — một struct có thể chuyển từ nhiều type), generic trait phù hợp hơn.
Cú Pháp Define
Trong trait body, khai báo associated type bằng type Tên; — y như khai báo method không có default, nhưng dùng từ khoá type:
trait Container {
type Item; // associated type, chưa chốt
type Index; // có thể có nhiều associated type trong một trait
fn get(&self, idx: Self::Index) -> Option<&Self::Item>;
}
Trong impl block, mỗi associated type phải được chốt bằng type Tên = ConcreteType;:
struct IntList(Vec<i32>);
impl Container for IntList {
type Item = i32;
type Index = usize;
fn get(&self, idx: Self::Index) -> Option<&Self::Item> {
self.0.get(idx)
}
}
Quên chốt associated type sẽ compile lỗi E0046: not all trait items implemented — y như quên impl required method. Cũng có thể đặt default associated type trong trait (type Index = usize; ngay trong trait body), nhưng tính năng này vẫn ở dạng unstable một phần, chưa khuyến nghị dùng rộng.
Reference Qua Self::Item
Trong thân trait và thân impl, tham chiếu associated type qua tiền tố Self:::
trait Iterator {
type Item;
fn next(&mut self) -> Option<Self::Item>;
// Default method dùng Self::Item
fn collect_vec(mut self) -> Vec<Self::Item>
where Self: Sized,
{
let mut v = Vec::new();
while let Some(x) = self.next() {
v.push(x);
}
v
}
}
Self::Item đọc là "type Item của Self dưới trait này". Khi compiler monomorphize impl Iterator for Counter, mọi chỗ Self::Item được thay bằng u32 — biến signature fn next(&mut self) -> Option<Self::Item> thành fn next(&mut self) -> Option<u32>.
Bên ngoài trait, viết <Counter as Iterator>::Item để gọi tên associated type cụ thể, hoặc dùng trong generic bound (mục 8). Trong impl block, viết tắt được Self::Item hoặc thẳng tên concrete (u32) — hai cách tương đương.
Stdlib Iterator Là Classic Example
std::iter::Iterator là ví dụ kinh điển nhất về associated type. Phiên bản tối giản:
pub trait Iterator {
type Item;
fn next(&mut self) -> Option<Self::Item>;
// Hơn 70 default method, tất cả tham chiếu Self::Item
// fn map<B, F>(self, f: F) -> Map<Self, F>
// where F: FnMut(Self::Item) -> B, Self: Sized { ... }
// fn filter<P>(self, predicate: P) -> Filter<Self, P>
// where P: FnMut(&Self::Item) -> bool, Self: Sized { ... }
}
Mỗi struct iterator gắn Item riêng: std::vec::IntoIter<T> có type Item = T;; std::str::Chars<'a> có type Item = char;; std::io::Lines<B> có type Item = io::Result<String>;.
Giả sử Rust dùng generic trait Iterator<T> — viết for x in v.iter() sẽ phải chỉ định T mỗi lần vì iter() trên cùng Vec<i32> trên lý thuyết có thể impl thêm cho String, bool... Tổ hợp generic + iterator chain sẽ phải kéo theo annotation khắp nơi. Chọn associated type, Rust khoá Item theo struct iterator → for x in v.iter() biết ngay x: &i32, không cần ghi gì.
Use Case Add Trait
Trait std::ops::Add dùng cho operator overload với toán tử + kết hợp cả generic và associated type. Phiên bản đơn giản hoá:
trait Add<Rhs = Self> {
type Output;
fn add(self, rhs: Rhs) -> Self::Output;
}
Rhs là generic parameter (kiểu toán hạng phải), default bằng Self. Output là associated type — kiểu kết quả. Sự phân chia này có chủ ý: cần cùng một Self cộng được với nhiều Rhs khác nhau (cho Vector + Vector, Vector + Scalar...) → Rhs là generic; còn Output được xác định duy nhất bởi cặp (Self, Rhs) → associated type, không cần khai báo bên ngoài.
#[derive(Debug, Clone, Copy)]
struct Point { x: i32, y: i32 }
impl Add for Point {
type Output = Point;
fn add(self, rhs: Point) -> Point {
Point { x: self.x + rhs.x, y: self.y + rhs.y }
}
}
// Cộng Point + i32 trả Point (dịch chuyển)
impl Add<i32> for Point {
type Output = Point;
fn add(self, k: i32) -> Point {
Point { x: self.x + k, y: self.y + k }
}
}
fn main() {
let a = Point { x: 1, y: 2 };
let b = Point { x: 3, y: 4 };
let c = a + b; // dùng impl Add for Point
let d = a + 10; // dùng impl Add<i32> for Point
println!("{:?} {:?}", c, d);
}
Hai impl Add cùng tồn tại với Rhs khác nhau, mỗi impl tự khai báo type Output. Operator + trong source code thực chất chỉ là cú pháp đường ngọt gọi Add::add.
Generic Constraint Với Associated Type
Khi viết hàm generic nhận một Iterator, thường cần ràng buộc cả "là iterator" và "iterator đó sinh ra element loại gì". Cú pháp I: Iterator<Item = i32> ngắn gọn cho việc này:
fn sum_iter<I>(iter: I) -> i32
where I: Iterator<Item = i32>,
{
let mut total = 0;
for x in iter { total += x; }
total
}
fn main() {
let v = vec![1, 2, 3, 4, 5];
let s = sum_iter(v.into_iter()); // OK: Item = i32
println!("{s}"); // 15
// Compile lỗi: Item của into_iter trên Vec<String> là String, không phải i32
// let v2 = vec![String::from("a")];
// sum_iter(v2.into_iter());
}
I: Iterator<Item = i32> nghĩa là "I phải impl Iterator, và associated type Item của nó phải bằng i32". Đây là cú pháp equality constraint. Có thể dùng nhiều: I: Iterator<Item = T>, T: Display — yêu cầu Item đặt tên T rồi ràng buộc T: Display.
Mở rộng thêm với trait có nhiều associated type: C: Container<Item = i32, Index = usize>. Mỗi associated type được chốt riêng. Cú pháp này nằm trong mọi signature generic phức tạp của stdlib và crate — đặc biệt Iterator, Future, Stream, Service.
Khi Nào Dùng Associated Vs Generic Trait
Tiêu chí chọn quay quanh một câu hỏi: cùng một Self có cần nhiều impl với type khác nhau không?
- Không (mỗi
Selfchỉ có một cách hợp lý) → dùng associated type. API gọi gọn, suy luận type tự nhiên, không annotation. Stdlib chọn cách này choIterator::Item,Deref::Target,IntoIterator::Item,Add::Output. - Có (cần nhiều impl) → dùng generic trait parameter. Mỗi impl khai báo trait với một type khác nhau. Stdlib chọn cách này cho
From<T>(StringimplFrom<&str>,From<char>,From<Vec<u8>>...),Into<T>,TryFrom<T>.
Trường hợp lai: Add<Rhs> với type Output — generic cho phía toán hạng (nhiều Rhs đều cộng được với cùng Self), associated cho phía kết quả (mỗi cặp (Self, Rhs) chốt đúng một Output).
Sai lầm thường gặp khi tự thiết kế trait: dùng generic trait Container<T> khi thực ra mỗi struct container chỉ chứa một loại element → consumer phải viết <MyVec as Container<i32>>::get(...) đầy redundancy. Đổi sang trait Container { type Item; ... } giải quyết.
Tổng Kết
- Associated type là type member nằm trong thân trait, khai báo bằng
type Tên;và chốt trong impl bằngtype Tên = ConcreteType;. - Tham chiếu trong trait/impl qua
Self::Tên; bên ngoài qua<Type as Trait>::Tên. - Khác generic trait: associated type cho phép đúng một impl trên một cặp
(Self, Trait)— gọi API không phải annotation. Generic trait cho phép nhiều impl với type khác nhau trên cùngSelf— flexibility cao hơn nhưng phải annotation khi gọi. Iteratorlà ví dụ kinh điển:type Item;+fn next(&mut self) -> Option<Self::Item>;. Hơn 70 default method tham chiếuSelf::Item.Addlai cả hai:Add<Rhs = Self>generic ở phía toán hạng phải,type Outputassociated ở phía kết quả.- Generic bound với associated type:
I: Iterator<Item = i32>— equality constraint giữ associated type khi viết hàm generic. - Tiêu chí chọn: cần nhiều impl trên cùng
Self→ generic; chỉ một impl hợp lý → associated. Mặc định ưu tiên associated để API gọn.
Bài Tập Củng Cố
Tự trả lời, đáp án ở cuối:
- Viết trait
Countercó associated typeValuevà methodcurrent(&self) -> Self::Value. Impl cho structIntCounter { n: u32 }vớitype Value = u32trả vềself.n. - Vì sao
impl Iterator for Counterchỉ được phép có đúng mộttype Item? Điều này khác gìimpl From<T> for StringnơiStringcó hàng chục impl khác nhau? - Trong trait
Add, vì saoOutputđể associated màRhsđể generic? Đảo lại (Rhs associated, Output generic) sẽ gặp vấn đề gì? - Viết generic function
print_first<I>(iter: I) where I: Iterator<Item = String>in phần tử đầu tiên của iterator. Gọi vớivec!["a".to_string()].into_iter(). - Khi nào nên dùng generic trait
trait Container<T>thay vìtrait Container { type Item; }? Cho một use case cụ thể.
Đáp án
trait Counter { type Value; fn current(&self) -> Self::Value; }.impl Counter for IntCounter { type Value = u32; fn current(&self) -> u32 { self.n } }. Gọiic.current()được suy ra trảu32mà không cần annotation.- Associated type ràng buộc uniqueness: mỗi cặp
(Self, Trait)chốt một type duy nhất.impl Iterator for Counterđã chốtItem = u32thì không thể có thêmimpl Iterator for CountervớiItem = String. CònFrom<T>dùng generic parameterT— đếm là một cặp khác mỗi khiTkhác, nênStringcó thểimpl From<&str>,impl From<char>,impl From<Vec<u8>>đồng thời. Outputđược xác định duy nhất bởi cặp(Self, Rhs)— biếtPoint + i32thì biết luôn kết quả làPoint, không có "lựa chọn" nên associated.Rhsngược lại: cùngPointcó thể cộng được với nhiều thứ (Point,i32,Vector...), mỗi cách là một impl khác → phải generic. Đảo lại sẽ ép mỗiSelfchỉ cộng được với đúng mộtRhs, mất tính linh hoạt của operator+.fn print_first<I>(mut iter: I) where I: Iterator<Item = String> { if let Some(s) = iter.next() { println!("{s}"); } }. Gọi:print_first(vec!["a".to_string()].into_iter());ina.- Khi cùng một struct cần "đóng vai container" cho nhiều loại element khác nhau qua các impl độc lập. Ví dụ giả định: trait
Convertible<T>với methodconvert(&self) -> T— một structJsonValuecó thểimpl Convertible<String>,impl Convertible<i64>,impl Convertible<Vec<u8>>để biểu diễn nhiều cách chuyển đổi. Tương tự design củaFrom<T>vàTryFrom<T>trong stdlib.
Bài Tiếp Theo
Bài 175: Conditional Impl — impl<T: Display> Foo for T — chuyển sang một cơ chế trait nâng cao khác: blanket implementation. Cú pháp impl<T: Display> ToString for T trong stdlib cho phép tự động impl ToString cho mọi type đã impl Display. Bài sẽ mổ xẻ blanket impl, conditional impl theo bound, ví dụ ToString từ Display, và caveat orphan rule khi viết blanket impl cho type/trait ngoài crate.
