Danh sách bài viết

Bài 174: Associated Type — Type Member Của Trait

Bài 174 của series Rust Cơ Bản — associated type là kiểu được khai báo ngay trong thân trait dưới dạng type member: trait Iterator { type Item; fn next(&mut self) -> Option<Self::Item>; }. Mỗi impl gắn cụ thể một type vào Item, ví dụ impl Iterator for Counter { type Item = u32; ... }. Khác với generic trait Iterator<T> cho phép cùng một type có nhiều impl với T khác nhau, associated type chốt một type duy nhất cho mỗi cặp (Self, Trait) — cho API gọn hơn khi thực tế chỉ có một lựa chọn hợp lý. Pattern này nằm khắp stdlib: Iterator::Item, Add::Output, Sub::Output, Deref::Target, IntoIterator::IntoIter. Bài đi qua cú pháp khai báo, tham chiếu qua Self::Item, ví dụ Iterator và Add (operator overload với Output riêng từng cặp), generic bound dạng I: Iterator<Item = i32>, và tiêu chí chọn associated vs generic trait.

09/06/2026
10 phút đọc
2 lượt xem
1

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 qua Self::Item.
  • Đọc hiểu chữ ký fn next(&mut self) -> Option<Self::Item> trong trait Iterator.
  • Dùng được trait Add cho operator overload với type Output tuỳ 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.
2

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.

3

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.

4

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.

5

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.

6

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>type Item = T;; std::str::Chars<'a>type Item = char;; std::io::Lines<B>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ì.

7

Use Case Add Trait

Trait std::ops::Add dùng cho operator overload với toán tử + kết hợp cả generic 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.

8

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.

9

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 Self chỉ 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 cho Iterator::Item, Deref::Target, IntoIterator::Item, Add::Output.
  • (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> (String impl From<&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.

10

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ằng type 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ùng Self — flexibility cao hơn nhưng phải annotation khi gọi.
  • Iterator là ví dụ kinh điển: type Item; + fn next(&mut self) -> Option<Self::Item>;. Hơn 70 default method tham chiếu Self::Item.
  • Add lai cả hai: Add<Rhs = Self> generic ở phía toán hạng phải, type Output associated ở 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.
11

Bài Tập Củng Cố

Tự trả lời, đáp án ở cuối:

  1. Viết trait Counter có associated type Value và method current(&self) -> Self::Value. Impl cho struct IntCounter { n: u32 } với type Value = u32 trả về self.n.
  2. Vì sao impl Iterator for Counter chỉ được phép có đúng một type Item? Điều này khác gì impl From<T> for String nơi String có hàng chục impl khác nhau?
  3. Trong trait Add, vì sao Output để associated mà Rhs để generic? Đảo lại (Rhs associated, Output generic) sẽ gặp vấn đề gì?
  4. 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ới vec!["a".to_string()].into_iter().
  5. 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
  1. trait Counter { type Value; fn current(&self) -> Self::Value; }. impl Counter for IntCounter { type Value = u32; fn current(&self) -> u32 { self.n } }. Gọi ic.current() được suy ra trả u32 mà không cần annotation.
  2. 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ốt Item = u32 thì không thể có thêm impl Iterator for Counter với Item = String. Còn From<T> dùng generic parameter T — đếm là một cặp khác mỗi khi T khác, nên String có thể impl From<&str>, impl From<char>, impl From<Vec<u8>> đồng thời.
  3. Output được xác định duy nhất bởi cặp (Self, Rhs) — biết Point + i32 thì biết luôn kết quả là Point, không có "lựa chọn" nên associated. Rhs ngược lại: cùng Point có 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ỗi Self chỉ cộng được với đúng một Rhs, mất tính linh hoạt của operator +.
  4. 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()); in a.
  5. 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 method convert(&self) -> T — một struct JsonValue có 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ủa From<T>TryFrom<T> trong stdlib.
12

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.