Bài này chúng ta sẽ tìm hiểu về Trait, Trait BoundTrait Object, Các kiểu của Trait trong Rust.Chúng ta cũng sẽ biết 4 kiểu của Trait như Marker Trait, Super Trait, Derived TraitAssociated Trait. Đây là một trong bài học quan trọng tuy không quá khó để đọc.

Thuật ngữ

Trait là một tính năng ngôn ngữ của Rust cho phép bạn định nghĩa các abstract behaviours (hành vi khái quát) và phương thức mà một kiểu nào đó có thể thực hiện nó.

Trait Object cũng là một tính năng trong Rust cho phép để để dynamic dispatch (truyền tham số động) hoặc phân giải đối tượng tại thời điểm runtime.

Trait Bound thuật ngữ chỉ một kiểu generic nào đó có một giới hạn bởi một Trait.

Lý thuyết là như vậy, giờ đi xem cụ thể nó là như nào

Khai báo một trait: Định nghĩa abstract behavior

Sử dụng từ khoá trait

Ví dụ: Một hình kín bất ký đều có diện tích. Một phương thức area nhận đầu vào là loại hình dạng này và đầu ra là diện tích của nó. Điều này định nghĩa hành vi khái quát như lý thuyết.

trait Shape {
    fn area(&self) -> f32;

Thực hiện một trait

Chúng ta thực hiện hành vi của trait này cho đối tượng Reactangle (hình chữ nhật) và Square(hình vuông)

struct Rectangle {
    height: f32,
    width: f32
}

impl Shape for Rectangle {
   fn area(&self) -> f32 {
     let area_of_rect  = self.height * self.width;
      area_of_rect
   }
}

struct Square {
    side: f32,
}

impl Shape for Square {
   fn area(&self) -> f32 {
     let area_of_square  = self.side * self.side;
      area_of_square
   }
}

Sử dụng từ khoá impl. Cú pháp là impl {Trait} for {Object}. Đọc là thực hiện hành vi Trait cho đối tượng object

Trait Bound

Giả sử chúng ta có một hàm như sau dạng generic

fn area<T>(shape: T) -> f32;

Bạn có thể thấy nó nhận vào kiểu generic T và trả về diện tích. Nhưng liệu bất kỳ kiểu T nào cũng có diện tích ?

Do đó chúng ta cần phải giới hạn T lại, nó phải là một kiểu mà có hành vi như trait Shape

fn area<T: Shape>(shape: T) -> f32;

Hoặc có thể viết

fn area<T>(shape: T) -> f32
where T: Shape;

Hai cách là như nhau. Cách sau tốt cho việc có nhiều kiểu biến generic với cho dễ đọc các tham số đầu vào

Ngoài ra chúng ta có thể viết một cách khác sử dụng từ khoá impl. Lúc này chúng ta sẽ bỏ kiểu generic T

fn area(shape: impl Shape) -> f32;

Đọc là bất kỳ kiểu shape nào mà thực hiện trait Shape

Trait Object

Bất viết kiểu impl này có một case thế này

fn return_shape() -> impl Shape {
    let sq = Square { side : 5.2 };
    let rect = Rectangle { height: 10.2, width: 3.2 }
    if sq.side > rect.hight {
        sq
    } else {
        rect
    }
}

Nó sẽ báo lỗi như này tại dòng `else { rect}

`if` and `else` have incompatible types
expected `Square`, found `Rectangle`rustcClick for full compiler diagnostic
traitobject_test.rs(33, 9): expected because of this
traitobject_test.rs(32, 5): `if` and `else` have incompatible types
traitobject_test.rs(26, 22): you could change the return type to be a boxed trait object: `Box<dyn`, `>`
traitobject_test.rs(33, 9): if you change the return type to expect trait objects, box the returned expressions: `Box::new(`, `)`, `Box::new(`, `)`
// size = 8, align = 0x4
let rec: Rectangle

Nghĩa là if với else trả về 2 kiểu khác nhau mặc dù chúng ta đã chỉ thị là kiểu trả về cứ thực hiện impl Shape là được. Compiler ngu quá ? Có vẻ đây là luật của compiler. Để mình giải thích cách Compiler hoạt động chỗ này và lý do nó đưa ra lỗi

Khi chúng ta khai báo generic như trên thì lúc compile Compiler sẽ tạo ra 2 hàm như sau:

fn return_shape() -> Square{
    let sq = Square { side : 5.2 };
    let rect = Rectangle { height: 10.2, width: 3.2 }
    if sq.side > rect.hight {
        sq
    } else {
        rect
    }
}

fn return_shape() -> Rectangle{
    let sq = Square { side : 5.2 };
    let rect = Rectangle { height: 10.2, width: 3.2 }
    if sq.side > rect.hight {
        sq
    } else {
        rect
    }
}

Vậy khi viết như trên chúng ta nhận ra kiểu trả về rõ ràng không thể tương thích được đúng không? Định trả về Rectangle nhưng trong code lại có đoạn trả về Square thế kia thì không chạy được rồi!

Nhưng compiler đã gợi ý cho chúng ta cách sửa là sử dụng Box<dyn X>

Thay đổi như sau:

fn return_shape() -> Box<dyn Shape> {
    let sq = Square { side: 5.2 };
    let rec = Rectangle {
        height: 10.2,
        width: 3.2,
    };
    if sq.side > rec.height {
        Box::new(sq)
    } else {
        Box::new(rec)
    }
}

Complier is happy now!

Từ khoá dyn + Trait ở trên chính là Trait Object giúp chúng ta trả về một giá trị động của Shape. Nghĩa là lúc runtime nó tự nhận biến đó là đối tượng kiểu Rectangle hoặc Square. Cái này thấy rất nhiều trong Java.

Các kiểu trait

Có 4 kiểu như sau

  • Super trait
  • Marker trait
  • Derived Trait
  • Associated Trait (Cái này gọi là associated types trong trait thì đúng hơn)

Bài viết chi tiết và ví dụ 4 kiểu này sẽ ở bài sau.

Happy coding!