此部分内容为原文档第十章《泛型、trait 和生命周期》中第二部分关于 trait 的内容,将拆为三部分来学习。

定义泛型的行为也可称为定义可共享的行为,即对于不同类型如果调用相同的方法,这些类型就可以共享相同的行为,这些类型也就可以抽象为泛型。

可以把 trait 简单地概括为可继承可默认实现的 Java 接口,而且一种类型还可以同时实现多个 trait。

定义 trait

trait 定义是一种将方法签名组合起来的方法,目的是定义一个实现某些目的所必需的行为的集合。

如:

pub trait Display {
	fn fmt(&self) -> String;
    /*
    罗列方法签名
    */
}

为类型实现 trait

使用 impl 声明实现,并使用 for 指定实现的类型。

如:

pub struct Point {
	x: i32,
	y: i32,
}

impl Display for Point {
	fn fmt(&self) -> String {
        format!("({}, {})", self.x, self.y)
    }
}

在实现时有如下规则:

  • 实现完整
  • 相干性coherence),或者更具体的说是 孤儿规则orphan rule)。只能为类型实现内部作用域中定义或是导入的 trait。
  • 默认实现。当存在默认实现时,已经默认实现的方法可以不必再次实现,也可再次实现来覆盖(重载)原有的默认实现。

trait 的继承

这是第一个与 Java 接口所不同的区别。看一个百度来的例子:

use std::fmt;

fn main() {

    struct Point {
        x: i32,
        y: i32,
    }

    // OutlinePrint 这个 trait 继承自 fmt::Display, 
    // 并提供一个额外的 outline_print 功能.
    trait OutlinePrint: fmt::Display {
        // outline_print 是一个默认实现功能函数.
        fn outline_print(&self) {
            let output = self.to_string();
            println!("{}", output);
        }
    }

    // 这里可以仅声明一下 Point 拥有这个 trait 即可.
    // 也可和下面的代码一样, 覆盖默认实现.
    impl OutlinePrint for Point {
        fn outline_print(&self) {
            let output = self.to_string();
            let len = output.len();
            println!("{}", "*".repeat(len + 4));
            println!("*{}*", " ".repeat(len + 2));
            println!("* {} *", output);
            println!("*{}*", " ".repeat(len + 2));
            println!("{}", "*".repeat(len + 4));
        }
    }
    
    // 这里仍然需要声明 Point 拥有 fmt::Display, 这是 Rust 的语法要求.
    impl fmt::Display for Point {
        
        // 为什么这里要写这个方法?
        // 那是因为fmt::Display只是定义了一个接口, 并没有实现这个方法.
        fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
            write!(f, "({}, {})", self.x, self.y)
        }
    }

    let p = Point {x: 1, y: 2};
    p.outline_print();

    // output:
    /*
    **********
    *        *
    * (1, 2) *
    *        *
    **********
    */
}

trait 作为参数

可以想象一下 Java 中使用接口类型作为参数的场景,任何实现了此接口的类的对象均可作为合法参数传入函数。

trait 作为参数是类似的,仅需注意一下使用的语法即可,看例子。

impl Trait 语法

pub struct NewsArticle {
    pub headline: String,
    pub location: String,
    pub author: String,
    pub content: String,
}

pub trait Summary {
    fn summarize(&self) -> String {
        String::from("(Read more...)")
    }
}

impl Summary for NewsArticle {}

pub fn notify(item: impl Summary) {
    println!("Breaking news! {}", item.summarize());
}

fn main() {
	let article = NewsArticle {
    	headline: String::from("Penguins win the Stanley Cup Championship!"),
    	location: String::from("Pittsburgh, PA, USA"),
    	author: String::from("Iceburgh"),
    	content: String::from("The Pittsburgh Penguins once again are the best
    hockey team in the NHL."),
	};
	
	notify(article);
}

Trait Bound 语法

impl Trait 语法适用于直观的例子,它是 trait bound 语法的一个较长形式的语法糖。trait bound 语法更加适用于复杂场景。

比如,对于下面的函数签名

fn notify(item1: impl Summary, item2: impl Summary)

如果要求传入的 item 具体类型要严格一致,而不能一是 NewsArticle,另一个是其它实现了 Summary trait 的类型,这时应该怎么办?

这只有在使用 trait bound 时才有可能。

fn notify<T: Summary>(item1: T, item2: T)

泛型 T 被指定为 item1item2 的参数限制,如此传递给参数 item1item2 值的具体类型必须一致。

通过 + 来指定多个 Trait

可以从两个方面来理解 + 的作用:

  • + 为类型增加了更为严格的约束,传入的类型参数需要同时实现多个 trait。
  • + 为函数体中增添了更多的已被实现的 trait 提供的方法。

语法如下:

  • fn notify(item: impl Summary + Display){}
  • fn notify<T: Summary + Display>(item: T) {}

通过 where 简化 Trait Bound

为了避免过长的 trait bound 语法,Rust 提供了 where 语法糖,看例子:

  • fn some_function<T: Display + Clone, U: Clone + Debug>(t: T, u: U) -> i32 {}
  • fn some_function<T, U>(t: T, u: U) -> i32 where T: Display + Clone, U: Clone + Debug {}

返回实现了 trait 的类型

与上面的函数参数类似,在函数签名的返回值位置,也可以使用 impl Trait 语法。

但是需要注意,函数中只能确定的返回一种实现了待返回 trait 的具体类型,而不能出现不同情况返回不同实现了待返回 trait 的具体类型的情况。

使用 trait bound 有条件地实现方法

【待补充】


Trait 是 Rust 另一个核心功能,它是很多其它进阶功能的基础,如迭代和闭包,需要好好掌握。

另外,本部分内容只是介绍了关于 trait 的初级内容,之后官方文档还对 trait 的一些高级内容做了介绍。