Danh sách bài viết

Bài 173: Supertrait — Trait Bao Trùm Trait Khác

Bài 173 của series Rust Cơ Bản — supertrait là cú pháp khai báo một trait bắt buộc type implementor phải đồng thời implement một (hoặc nhiều) trait khác làm điều kiện tiên quyết. Viết trait OutlinedDisplay: Display đọc là "OutlinedDisplay có Display làm supertrait" — compiler ép mọi type impl OutlinedDisplay phải có sẵn impl Display, biến điều này thành ràng buộc compile-time. Nhờ vậy default method bên trong subtrait gọi được method của supertrait (như format!("{self}") qua Display::fmt). Bài đi qua cú pháp khai báo, default method gọi method supertrait, multi-supertrait nối bằng +, lỗi E0277 khi thiếu impl, các ví dụ stdlib (Eq: PartialEq, Copy: Clone, Ord: Eq + PartialOrd), và điểm khác biệt với OO: vì trait Rust chỉ ràng buộc behavior chứ không kế thừa data, supertrait không tạo diamond problem như multiple inheritance kinh điển.

09/06/2026
9 phút đọc
1 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 supertrait: trait mà một trait khác yêu cầu implementor phải có sẵn impl để compile được.
  • Viết được cú pháp trait Sub: Super — dấu : sau tên trait, theo sau là danh sách supertrait nối bằng +.
  • Hiểu vì sao default method bên trong subtrait được phép gọi method của supertrait — compiler đã có bảo đảm về sự tồn tại của impl.
  • Đọc và sửa được lỗi E0277 "the trait `Display` is not implemented" khi quên impl supertrait trước khi impl subtrait.
  • Nhận diện các pattern supertrait trong stdlib: Eq: PartialEq, Copy: Clone, Ord: Eq + PartialOrd — và biết vì sao thứ tự derive lại phải tuân theo đúng chiều.
  • Phân biệt supertrait Rust với extends trong OO interface: cả hai cùng tạo "hierarchy" nhưng Rust không kế thừa data, không có diamond problem.
2

Supertrait Là Gì

Tưởng tượng bạn muốn viết trait OutlinedDisplay — in một giá trị gói trong khung sao (*) cho đẹp. Nếu giá trị đã in được dạng text (qua Display), việc còn lại chỉ là thêm border. Vậy default method của OutlinedDisplay rất muốn gọi format!("{self}") để tận dụng Display::fmt sẵn có:

use std::fmt::Display;

trait OutlinedDisplay: Display {
    fn outline(&self) {
        let text = format!("{self}");
        let len = text.len();
        let border = "*".repeat(len + 4);
        println!("{border}");
        println!("* {text} *");
        println!("{border}");
    }
}

Dòng trait OutlinedDisplay: Display đọc là "OutlinedDisplay có Display làm supertrait". Ý nghĩa: mọi type muốn impl OutlinedDisplay bắt buộc phải đồng thời impl Display — nếu không, rustc từ chối compile. Đây là một ràng buộc compile-time, không phải kiểm tra runtime: lỗi xuất hiện ngay khi viết impl OutlinedDisplay for MyTypeMyType chưa có impl Display.

Quy ước thuật ngữ: Display ở đây là supertrait, OutlinedDisplaysubtrait. Mối quan hệ hoàn toàn ở mức trait (behavior), không phải ở mức type (data). Một type bất kỳ vẫn là chính nó — không "kế thừa" gì cả; nó chỉ đồng thời thoả mãn hai trait.

3

Cú Pháp Supertrait

Khai báo supertrait đặt dấu : ngay sau tên trait, theo sau là danh sách trait phải có nối bằng +:

// 1 supertrait
trait A: Display { /* ... */ }

// nhiều supertrait
trait B: Display + Clone + Debug { /* ... */ }

Cú pháp này giống hệt trait bound đặt lên type parameter (fn foo<T: Display + Clone>) — đó không phải trùng hợp: về mặt formal, một supertrait chính là trait bound đặt lên Self. Khi bạn viết trait OutlinedDisplay: Display, compiler hiểu nó tương đương với trait OutlinedDisplay where Self: Display — và cả hai dạng đều hợp lệ, dạng thứ hai dài hơn nhưng đặt được ràng buộc phức tạp hơn trên expression liên quan đến Self.

So sánh với OO: cú pháp gợi nhớ "extends" trong Java/TypeScript interface (interface OutlinedDisplay extends Display) — đọc tương tự, ngữ nghĩa cũng tương tự (interface mở rộng tạo điều kiện tiên quyết). Khác biệt: Rust trait không có method body của struct lồng vào, không có constructor kế thừa, không truy cập state cha — chỉ thuần behavior contract.

4

Default Method Gọi Method Của Supertrait

Đây là lý do chính khiến supertrait hữu dụng. Trong body của default method outline, biểu thức format!("{self}") gọi macro format! với format specifier mặc định {} — macro này sinh ra lệnh gọi Display::fmt(&self, formatter). Nếu không có dòng : Display ở header trait, compiler báo lỗi đại loại "Self doesn't implement Display" vì bên trong định nghĩa trait, kiểu cụ thể của self chưa biết — chỉ biết "một type nào đó sẽ impl trait này". Với supertrait đã khai báo, compiler có thông tin: bất kỳ type nào impl OutlinedDisplay chắc chắn impl Display, vậy gọi {self} luôn an toàn.

use std::fmt::Display;

trait OutlinedDisplay: Display {
    fn outline(&self) {
        // được phép gọi Display::fmt qua format! / {} vì Display là supertrait
        let text = format!("{self}");
        println!("--> {text} <--");
    }
}

struct Point { x: i32, y: i32 }

impl Display for Point {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "({}, {})", self.x, self.y)
    }
}

// vì Point đã impl Display, được phép impl OutlinedDisplay
impl OutlinedDisplay for Point {}

fn main() {
    let p = Point { x: 3, y: 7 };
    p.outline(); // in --> (3, 7) <--
}

Để ý impl OutlinedDisplay for Point {} — body trống vì outline đã có default. Có hai impl: Display cung cấp fmt, OutlinedDisplay đánh dấu "được phép gọi outline". Cả hai đều cần — supertrait chỉ là điều kiện, không tự động cung cấp impl giúp bạn.

5

Multi-Supertrait

Một trait có thể có nhiều supertrait — nối bằng + như trait bound:

use std::fmt::{Debug, Display};

trait Reportable: Display + Clone + Debug {
    fn report(&self) -> String {
        let display = format!("{self}");
        let debug = format!("{self:?}");
        let cloned = self.clone();
        let _ = cloned; // chứng minh Clone tồn tại
        format!("[Report] display={display} | debug={debug}")
    }
}

Type nào muốn impl Reportable phải đồng thời có cả ba: Display, Clone, Debug. Default method report tự do gọi {self} (qua Display), {self:?} (qua Debug), và self.clone() (qua Clone) vì compiler đã biết tất cả đều có. Multi-supertrait phù hợp khi bạn muốn cung cấp default method tận dụng nhiều capability của implementor — thay vì ép caller pass đủ trait bound mỗi lần gọi.

Thứ tự liệt kê không quan trọng (Display + Clone = Clone + Display); rustfmt thường giữ nguyên thứ tự bạn viết — convention là đặt trait stdlib quen thuộc trước, custom trait sau.

6

Compile Error Khi Thiếu Impl Supertrait

Đây là "lưới an toàn" mà supertrait đặt ra. Nếu cố impl subtrait mà type chưa impl supertrait, rustc trả lỗi E0277:

use std::fmt::Display;

trait OutlinedDisplay: Display {
    fn outline(&self) { /* ... */ }
}

struct MyType;

// LỖI: MyType không impl Display
impl OutlinedDisplay for MyType {}
error[E0277]: `MyType` doesn't implement `std::fmt::Display`
  --> src/main.rs:10:1
   |
10 | impl OutlinedDisplay for MyType {}
   | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ `MyType` cannot be formatted with the default formatter
   |
   = help: the trait `std::fmt::Display` is not implemented for `MyType`
   = note: required by the bound in `OutlinedDisplay`

Thông điệp này rất rõ: bound (Display là supertrait của OutlinedDisplay) chưa được thoả mãn bởi MyType. Sửa bằng cách thêm impl Display trước:

impl Display for MyType {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "MyType instance")
    }
}

impl OutlinedDisplay for MyType {} // bây giờ OK

Lỗi xảy ra tại site impl, không tại site gọi method — sai sớm, dễ fix. Đây cũng là khác biệt với một số ngôn ngữ duck-typed: vấn đề lộ ra ngay khi compile, không đợi đến runtime.

7

Use Case Trong Stdlib

Stdlib Rust dùng supertrait rất nhiều cho các trait so sánh và copy. Định nghĩa thực tế trong std::cmpstd::clone:

// std::cmp - rút gọn cho dễ đọc
pub trait Eq: PartialEq { }

pub trait Ord: Eq + PartialOrd {
    fn cmp(&self, other: &Self) -> Ordering;
    /* ... */
}

// std::marker / std::clone
pub trait Copy: Clone { }

Ý nghĩa:

  • Eq: PartialEq — equality "đầy đủ" (reflexive, symmetric, transitive) là phiên bản mạnh hơn của partial equality (chấp nhận NaN). Mọi type Eq đều phải PartialEq trước; Eq không thêm method, chỉ đánh dấu "type này không có NaN-like behavior".
  • Copy: CloneCopy chỉ thực hiện được nếu Clone cũng có; Copy là "phiên bản đơn giản" (bitwise copy implicit) chồng lên Clone (deep copy explicit). Vì thế khi #[derive(Copy, Clone)], hai derive luôn đi kèm — không thể Copy mà không Clone.
  • Ord: Eq + PartialOrd — total ordering yêu cầu cả full equality lẫn partial ordering trước. Vì Eq đã yêu cầu PartialEq, một type impl Ord thực chất phải có đủ 4: PartialEq, Eq, PartialOrd, Ord — đó là lý do #[derive(Ord, PartialOrd, Eq, PartialEq)] luôn đi cùng nhau.

Hierarchy này không phải arbitrary — nó phản ánh cấu trúc toán học của các quan hệ tương đương và thứ tự. Khi bạn đọc tài liệu stdlib và thấy "Ord: Eq + PartialOrd", đó chính là cú pháp supertrait đang dùng.

8

Diamond Problem Không Tồn Tại Trong Rust

Trong C++ với multiple inheritance, "diamond problem" xảy ra khi class D kế thừa cả BC, trong đó BC cùng kế thừa AD rốt cuộc có hai bản data của A, gây ambiguity khi gọi method hay truy cập field. Java cấm multiple inheritance giữa class chính vì lý do này.

Rust trait không có diamond problem vì lý do căn bản: trait không kế thừa data. Khi trait OutlinedDisplay: Display, không có field nào của Display "chui vào" OutlinedDisplay; supertrait chỉ là behavior bound — chỉ định "type implementor phải có hành vi Display". Cho dù bạn xây hierarchy phức tạp:

trait A { fn name(&self) -> &str; }
trait B: A { /* ... */ }
trait C: A { /* ... */ }
trait D: B + C { /* ... */ }

struct MyType;

impl A for MyType { fn name(&self) -> &str { "my" } }
impl B for MyType {}
impl C for MyType {}
impl D for MyType {}

MyType impl D phải đồng thời impl B, C, và (qua B hay C) A — nhưng chỉ có một impl A cho MyType. Gọi my.name() không ambiguous, không tồn tại "hai bản A". Đó là khác biệt thanh lịch: trait là contract, không phải class — không có state, không có constructor, không có data layout — nên không có cách nào tạo trùng lặp.

Hệ quả thực dụng: thoải mái xây trait hierarchy nhiều tầng mà không lo về vấn đề kinh điển của OO multiple inheritance. Trait Rust thiết kế cho composition, không phải inheritance — đó là một trong những điểm Rust học bài học từ Java/C++ và quyết định đi đường khác.

9

Tổng Kết

  • Supertrait là trait mà một trait khác yêu cầu type implementor phải có sẵn impl. Khai báo bằng trait Sub: Super.
  • Cú pháp dấu : sau tên trait, theo sau là danh sách supertrait nối bằng +; tương đương where Self: Super dạng đầy đủ.
  • Default method trong subtrait được phép gọi method của supertrait vì compiler đã có bảo đảm về sự tồn tại của impl — đây là use case chính.
  • Multi-supertrait (trait Foo: Display + Clone + Debug) cho phép default method tận dụng nhiều capability cùng lúc, không phải ép caller pass đủ bound mỗi lần.
  • Thiếu impl supertrait → E0277 "the trait Display is not implemented" tại site impl Subtrait for MyType; sửa bằng cách impl supertrait trước.
  • Stdlib pattern: Eq: PartialEq, Copy: Clone, Ord: Eq + PartialOrd — phản ánh cấu trúc toán học của equality/ordering; derive luôn đi kèm theo đúng chiều.
  • Rust trait không có diamond problem: trait chỉ ràng buộc behavior, không kế thừa data — không thể tạo "hai bản" của supertrait dưới subtrait.
  • Supertrait giống "extends" trong OO interface về cú phápý nghĩa hierarchy, nhưng khác về thực chất: composition behavior, không inheritance data.
10

Bài Tập Củng Cố

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

  1. Viết trait Loggable với supertrait Debug; cung cấp default method log(&self) in ra "[LOG] {self:?}". Impl Loggable cho một struct Event đã derive Debug. Gọi event.log() trong main.
  2. Cho trait Reportable: Display + Clone. Một developer viết impl Reportable for MyEvent trong khi MyEvent chỉ derive Clone, chưa impl Display. Mã lỗi compiler trả về là gì và sửa thế nào?
  3. Đoán xem hai dạng sau có tương đương ngữ nghĩa không, vì sao: (a) trait Foo: Display { /* ... */ } và (b) trait Foo where Self: Display { /* ... */ }.
  4. Stdlib quy định Ord: Eq + PartialOrd. Tại sao khi #[derive(Ord, PartialOrd, Eq, PartialEq)] phải có cả 4? Nếu chỉ derive Ord mà thiếu các derive còn lại, compiler báo lỗi ở đâu?
  5. Vì sao Rust không gặp diamond problem khi có trait D: B + C với B: AC: A? So sánh với class C++ multiple inheritance để chỉ ra điểm khác biệt căn bản.
Đáp án
  1. use std::fmt::Debug; trait Loggable: Debug { fn log(&self) { println!("[LOG] {self:?}"); } } #[derive(Debug)] struct Event { id: u32 } impl Loggable for Event {} fn main() { Event { id: 1 }.log(); }. Default method log gọi {self:?} nhờ Debug là supertrait.
  2. E0277 "the trait `std::fmt::Display` is not implemented for `MyEvent`". Sửa: thêm impl Display for MyEvent { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { write!(f, "...") } } trước dòng impl Reportable for MyEvent. Compiler không tự cung cấp Display giúp — supertrait chỉ là điều kiện, không phải auto-derive.
  3. Hoàn toàn tương đương. Dạng (a) là syntactic sugar cho (b); cả hai đều thêm bound Self: Display vào trait Foo. Dạng (b) đầy đủ hơn, dùng khi cần bound phức tạp trên Self hoặc các associated type (sẽ học ở bài 174).
  4. Vì hierarchy là Ord: Eq + PartialOrdEq: PartialEq — kéo theo: impl Ord yêu cầu Eq + PartialOrd; impl Eq lại yêu cầu PartialEq; impl PartialOrd cũng yêu cầu PartialEq. Tổng cộng cần đủ 4 trait. Thiếu một → E0277 báo trait còn thiếu, tại dòng #[derive(Ord)] hoặc impl Ord for ....
  5. Trait Rust chỉ ràng buộc behavior — không có field, không có data layout, không có constructor. Khi D: B + C, B: A, C: A, type implementor chỉ có một impl A; gọi method của A không ambiguous. Trong C++, class BC mỗi cái mang theo một bản data của A, lớp D rốt cuộc có hai bản A, gây ambiguity khi truy cập field/method — phải dùng virtual inheritance để giải. Rust thiết kế trait không có data nên không xảy ra tình huống này từ đầu.
11

Bài Tiếp Theo

Bài 174: Associated Type — Type Member Của Trait — bài 173 đã cho thấy trait có thể yêu cầu trait khác qua supertrait; bài 174 đi tiếp một cấp: trait có thể chứa type member như trait Iterator { type Item; fn next(&mut self) -> Option<Self::Item>; } — một "associated type" gắn liền với mỗi impl, khác hẳn generic parameter cho phép nhiều impl khác nhau. Đây là cú pháp nền tảng của Iterator, Future, Add, và là điểm Rust khác biệt mạnh với generic phong cách Java/C++.