Wang's blog

七、特性

Published on

特性是为未知类型Self定义的一组方法,类似于Java的接口。特性可以为任意类型实现,包括内置类型,如i64/&str等。

定义特性

// 定义一个特性,无需实现
pub trait Summary {
    fn summarize(&self) -> String;
}

// 定义特性时可以提供默认实现
pub trait Summary {
    fn summarize(&self) -> String {
        String::from("(Read more...)")
    }
}

实现特性

特性必须为某个类型实现,可以认为是为这个类型添加了相应的方法。

impl Summary for NewsArticle {
    fn summarize(&self) -> String {
        format!("{}, by {} ({})", self.headline, self.author, self.location)
    }
}

使用impl Trait

impl Trait是一个语法糖,它相当于在泛型函数中使用泛型范围(trait bound)。可以将impl Trait作为函数的输入参数或者返回值,使代码更简洁易读,但是需要注意此时的函数是静态泛型函数,并不能动态接收或返回不同的类型。

作为输入参数

// 使用impl Trait作为输入参数
pub fn notify(item: &impl Summary) {
    println!("Breaking news! {}", item.summarize());
}

// 使用+表示参数需要实现多个特性
pub fn notify(item: &(impl Summary + Display)) {}

作为返回值

// 使用impl Trait作为返回值
fn returns_summarizable() -> impl Summary {}

使用dyn返回实现特性的动态类型

Rust编译器必须知道函数返回值占用空间的大小,但是对于同样一个特性,不同的实现类型占用空间不同,因此不能直接将特性作为返回值。但是,可以通过返回一个Box指向一个实现特性的变量来解决这一问题。由于Box大小确定,因此这是可行的。由于Rust要求在堆上分配内存时必须显示说明,因此这种情况下需要使用dyn关键字。

struct Sheep {}
struct Cow {}

trait Animal {
    fn noise(&self) -> &'static str;
}

impl Animal for Sheep {
    fn noise(&self) -> &'static str {
        "baaaaah!"
    }

impl Animal for Cow {
    fn noise(&self) -> &'static str {
        "moooooo!"
    }
}

fn random_animal(random_number: f64) -> Box<dyn Animal> {
    if random_number < 0.5 {
        Box::new(Sheep {})
    } else {
        Box::new(Cow {})
    }
}

常用特性

操作符特性

Rust中的操作符是通过实现特性进行重载的。例如,加法是通过实现Add特性实现的。全部操作符特性列表可在这里找到。

Drop

Drop特性只有一个drop方法,会在变量离开作用域时自动调用。该特性用于完成释放资源的工作。

Iterator

Iterator特性只有一个next方法,用于获取下一个元素。该特性用于实现自定义迭代器。

Clone

Clone特性用于实现资源的复制。

派生(Derive)

通过使用#[derive]属性,Rust编译器可以为一些常用特性提供默认实现,这些特性也可以手动实现。

  • 比较特性:Eq, PartialEq, Ord, PartialOrd
  • Clone:生成一个变量的复制
  • Copy:使用复制语义代替移动语义
  • Hash:计算变量的hash值
  • Default:为一个类型生成一个空值
  • Debug:使用{:?}进行格式化

关联类型

关联类型是在定义特性时指定的类型占位符,在实现特性时才被指定具体类型。

// 特性声明
pub trait Iterator {
    // 声明关联类型
    type Item;
    fn next(&mut self) -> Option<Self::Item>;
}

// 特性实现
impl Iterator for Counter {
    // 指定关联类型的实际类型
    type Item = u32;
    fn next(&mut self) -> Option<Self::Item> {
        ...
    }
}

使用关联类型与使用泛型类似。但是如果使用泛型,则Counter的Iterator实现可以有多个,在使用时必须提供类型说明要使用哪个。如果使用关联类型,则Counter的Iterator实现只能有一个,使用时无需指定类型。

pub trait Iterator<T> {
    fn next(&mut self) -> Option<T>;
}

默认泛型类型参数

当在特性中使用泛型参数时,可以提供默认值。

// Rhs的默认值为Self
trait Add<Rhs=Self> {
    type Output;
    fn add(self, rhs: Rhs) -> Self::Output;
}

// 使用默认类型
impl Add for Point {
    type Output = Point;
    fn add(self, other: Point) -> Point {
        ...
    }
}

// 使用指定类型
impl Add<Meters> for Millimeters {
    type Output = Millimeters;
    fn add(self, other: Meters) -> Millimeters {
        Millimeters(self.0 + (other.0 * 1000))
    }
}

在两种情况下会用到默认类型参数:

  • 在不修改现有代码的情况下扩展一个类型
  • 提供大多数用户不需要的自定义功能

冲突特性歧义消解

一个类型可以实现多个特性,如果实现的两个特性的方法中,或者特性方法与类型方法中,有相同的方法名,则出现冲突。

  • 实现方法时,不同的特性的方法实现在不同的代码块中,不会出现冲突
  • 调用方法时,使用完全语法调用即可
// 两个特性具有相同方法名
trait Pilot {
    fn fly(&self);
}
trait Wizard {
    fn fly(&self);
}

// 实现时无冲突
struct Human;
impl Pilot for Human {
    fn fly(&self) {
        println!("This is your captain speaking.");
    }
}
impl Wizard for Human {
    fn fly(&self) {
        println!("Up!");
    }
}
impl Human {
    fn fly(&self) {
        println!("*waving arms furiously*");
    }
}

let person = Human;
// 调用特性方法时需要使用完全语法指定特性
Pilot::fly(&person);
Wizard::fly(&person);
// 直接调用类型方法
person.fly();

在关联函数中,由于不存在self参数,因此需要一种更复杂的语法。

trait Animal {
    fn baby_name() -> String;
}

struct Dog;

impl Dog {
    fn baby_name() -> String {
        String::from("Spot")
    }
}

impl Animal for Dog {
    fn baby_name() -> String {
        String::from("puppy")
    }
}

// 可以直接使用类中的关联方法
println!("A baby dog is called a {}", Dog::baby_name());
// 错误,不能直接使用特性中的关联函数,因为不知道要使用哪个类型的实现
println!("A baby dog is called a {}", Animal::baby_name());
// 正确使用方式
println!("A baby dog is called a {}", <Dog as Animal>::baby_name());

父特性

Rust中并不存在继承,但是可以将一个特性定义为另一个的超集,实现子特性时要求必须实现父特性。

trait Person {
    fn name(&self) -> String;
}

trait Student: Person {
    fn university(&self) -> String;
}

使用newtype模式为外部类型实现外部特性

Rust不支持直接为外部类型实现外部接口,这样保证了代码的一致性。但是可以使用元组结构体声明一个外部类型的包装器,即可为该包装器实现外部特性。

// 声明外部类型Vec<String>的包装器
struct Wrapper(Vec<String>);

// 包装器为内部类型,可以实现外部特性
impl fmt::Display for Wrapper {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "[{}]", self.0.join(", "))
    }
}