概述
相对其他语言(java/C#)提供了接口来满足对不同类型的值进行操作的代码(甚至包括那些尚未实现的类型),并能够结合泛型来实现语言的“多态化”;同样Rust为了达到对“多态”的支持提供了trait(特型)和generic(泛型)。
而trait(特型)算是rust对接口或抽象基类的实现。
举个类型: 标准库中关于Write定义 std::io::Write
trait Write {
fn write(&mut self, buf: &[u8]) -> Result<usize>;
fn flush(&mut self) -> Result<()>;
fn write_all(&mut self, buf: &[u8]) -> Result<()> {
// 省略部分代码
}
// 省略代码
}
同样在标准类型库中File、TcpStream和Vec<u8>等实现了std::io::Write
use std::io::Write; // 需要引入对应的trait
fn say_hello(out: &mut Write) -> std::io::Result<()> {
out,write_all(b"welcome to rust world!!!")?; // 使用Vec<u8>
out.flush();
}
/// 测试用例
use std::fs::File;
fn main() {
let mut file = std::fs::File::create("hello_world.txt").unwrap();
say_hello(&mut file);
let mut bytes = vec![];
say_hello(&mut bytes);
println!("bytes={:?}", bytes);
}
同样使用trait(特型)添加给类型不用额外内存
使用trait(特型)
在rust中trait(特型)是一种任何类型都可以支持或不支持;trait可以认为某类型能够做什么的一种能力;(当使用trait特型方法时需要确保其本身必须存在当前作用域中,否则trait特型所有的方法都是不可用的;若是trait特型是标准前置模块的一部分,这样rust就会默认自动将其引入)。
1、trait object(特型对象/特型目标)
use std::io::Write;
fn main() {
let mut buf: Vec<u8> = vec![];
// let writer: Write = buf; // 不能通过编译,编译时不知道write真实大小
let writer: &mut Write = &mut buf;
}
上面的例子中line-error-1这行是不能通过编译的,在rust中变量大小在编译期间必须可知的,但是实现Write的类型大小是任意的,这一点不同于其他语言(java/C#),因为这些语言默认定义的变量是一个引用,并执行任何实现了interface或abstract的对象;但是在rust的引用必须是显式的。并且在rust将一个trait特型类型的引用称为trait object(特型对象/特型目标)。
不过需要知道的是:trait object(特型对象/特型目标)在编译期间通常是不知道引用对象/目标的类型,那就需要trait object(特型对象/特型目标)包含一些关于trait object(特型对象/特型目标)的额外信息,并且这个信息仅限rust内部自身使用,确保在调用trait特型方法时知道类型信息,这样才能动态调用正确的方法。
2、trait object(特型对象/特型目标)的内存分布
在内存中, trait object(特型对象/特型目标)是一个胖指针,包含指向值的指针和指向表示该值类型的表的指针; 每个trait object(特型对象/特型目标)都会占用两个机器字;
let mut buf: Vec<u8> = vec![];
let writer: &mut Write = &mut buf;
内存布局如下:
data:数据指针内容 | &mut buf as &mut Write | 值类型的表指针 | ||||
---|---|---|---|---|---|---|
buf | data:数据指针 | destructor | ||||
buffer:数据 | vptr :值类型的表指针 | size | ||||
capacity: 容量 | alignment | |||||
length: 大小 | .wirte() | |||||
.flush() | ||||||
.write_all() |
在rust中会在编译时生成虚拟表并且只生成一次,并有同类型的所有对象共享(impl trait_name for traint_imple_name);这样在调用一个trait object(特型对象/特型目标)的方法时,会自动使用虚拟表,已确定调用了哪个实现。
3、traint特型的定义和实现
在前面的例子中定义trait特型其实是很简单的:
关键字trait Trait_Name {
// trait体 定义不同的方法
// 方法
}
样例:
trait Action {
fn draw(&self, canvas: &mut Canvas);
fn hit_test(&self, x:i32, y:i32) -> bool;
}
同样,实现trait也是相对比较简单的:
关键字 impl Trait_Name for Type_Name {
// 实现的方法
}
样例:
impl Action for Broom {
fn draw(&self, canvas: &mut Canvas) {
for y in self.y - self.height -1 .. self.y {
canvas.write_at(self.x, y, '|');
}
canvas.write_at(self.x, self.y, 'M');
}
fn hit_test(&self, x: i32, y: i32) -> bool {
self.x == x
&& self.y - self.height - 1 <= y
&& y <= self.y
}
}
而非trait特型的方法,可以使用impl Type_Name来定义:
impl Type_Name {
fn method_name(&self, ...<参数>...);
fn method_name(...<参数>...);
...
}
不过在实现trait时,若是其中某个或某些方法相对通用,可以在trait特型中提供当前方法的默认实现。 这样通过impl Trait_Name for Type_Name 语法来实现trait时,可以使用这个默认方法。
在Rust允许在任意类型上实现任意trait特型,这样就可以给任意类型扩张功能:
样例: 扩展现有char类型的功能
trait IsEmoji {
fn is_emlji(&self) -> bool;
}
impl IsEmoji for char {
fn is_emoji(&self) -> bool {
...
}
}
同样,可以一次性全部实现一个扩展trait特型: 使用泛型impl语法让一个类型“家族”都具有某个扩展trait特型(关于泛型这块会有单独的介绍)。
样例:对每个实现Write的类型,在实现trait特型WriteHtml
use std::io::{self, Write};
trait WriteHtml {
fn write_html(&mut self, html: &HtmlDocument) -> io::Result;
}
impl<W: Write> WriteHtml for W {
fn write_html(&mut self, html: &HtmlDocument) -> io::Result<()> {
......
}
}
在rust中trait特型也能够实现其他语言的“继承”, 通过impl来实现trait时需要实现当前trait特型及其父trait特型。
样例:子trait特型
trait Trait_Name: Parent_Trait_Name {
...
}
引用
trait特型
rust之trait-知乎