5分钟速读之Rust权威指南(十八)

264 阅读6分钟

trait

trait与TS中的接口(interface)的功能类似,但也不完全相同,比如说interface中可以定义属性和方法,而trait中只能定义方法。

定义trait

使用trait关键字后边跟trait名称,大括号中标识具有该trait的类型应该实现的方法:

trait Summary {
  // 一个trait可以包含多个方法:每个方法签名占据单独一行并以分号结尾。
  fn summarize(&self) -> String;
}

为类型实现trait

可以理解为TS中的implements关键字,在rust中使用impl后面跟trait名称,再跟for关键字跟类型名称,下面为为NewsArticle与Tweet类型实现Summary trait:

struct NewsArticles {
  headlline: String,
  content: String,
  author: String,
  location: String
}

// 为NewsArticles实现Summary
impl Summary for NewsArticles {
  fn summarize(&self) -> String {
    // self表示当前调用方法的具体类型,跟TS中的this很像
    format!("{}-{}-{}-{}", self.headlline, self.author, self.location, self.content)
  }
}

let news = NewsArticles {
  headlline: String::from("headline"),
  content: String::from("content"),
  author: String::from("author"),
  location: String::from("location")
};

// 调用summarize()方法
println!("{}", news.summarize()); // headline-author-location-content

struct Tweet {
	username: String,
	content: String,
	reply: String,
	retweet: String,
}

// 为Tweet实现Summary
impl Summary for Tweet {
  fn summarize(&self) -> String {
    format!("{}-{}-{}-{}", self.username, self.content, self.reply, self.retweet)
  }
}

let tweet = Tweet {
  username: String::from("username"),
  content: String::from("content"),
  reply: String::from("reply"),
  retweet: String::from("retweet"),
};

// 调用summarize()方法
println!("{}", tweet.summarize()) // username-content-reply-retweet

为外部结构实现自定义trait:

// 为标准库实现自定义trait
impl Summary for String {
  fn summarize(&self) -> String {
    "impl Summary for String".to_string()
  }
}
// 调用为字符串实现的summarize()方法
println!("{}", String::from("abc").summarize()) // impl Summary for String

为我们定义的结构实现外部库的trait:

use std::fmt;
// 实现标准库中的Display trait后可以将类型在控制台打印
// 这里先不用关注代码细节,只需了解可以为我们自定义的类型实现标准库中的trait即可
impl fmt::Display for Tweet {
  fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
    write!(f, "({}, {}, {}, {})", self.username, self.content, self.reply, self.retweet)
  }
}
// 实现Display后能够正常打印tweet,而无需加{:?}启用Debug模式
println!("{}", tweet); // (username, content, reply, retweet)

为外部结构体实现外部的trait:

// 这里为标准库中的Vec类型实现标准库中的Display trait
impl<T> std::fmt::Display for std::vec::Vec<T> {
  // 报错,只有当trait或或者其中一个类型定义于我们的库中时,
  // 才能为该类型实现对应的trait。
}

上边这种报错是因为rust存在孤儿规则:之所以这么命名是因为它的父类型没有定义在当前库中。这一规则也是程序一致性(coherence)的组成部分,它确保了其他人所编写的内容不会破坏到你的代码,反之亦然。如果没有这条规则,那么两个库可以分别对相同的类型实现相同的trait,rust将无法确定应该使用哪一个版本。

默认实现

trait的方法签名可以做默认实现,减少多个类型的方法相同时的重复代码书写:

trait User {
  fn user(&self) -> String {
    String::from("unknow")
  }
}

// 为NewsArticles实现User trait
impl User for NewsArticles {
  // 由于user有默认实现,所以这里可以不用实现
}
println!("{}", news.user()) // unknow

还可以在默认实现中调用相同trait中的其他方法,哪怕这些方法没有默认实现:

trait User {
  fn user(&self) -> &String;
  fn info(&self) -> &String {
    // user()没有默认实现,但是可以调用,因为在结构体实现User trait时
    // 一定会实现user()方法
    self.user()
  }
}

impl User for NewsArticles {
  // 这里会实现user()方法
  fn user(&self) -> &String {
    &self.author
  }
}

println!("{}", news.info()) // author

使用trait来约束参数

使用impl关键字,在函数中要求参数必须实现了某个trait:

fn notify(item: impl Summary) { // 要求item实现了Summary
  println!("{}", item.summarize())
}
notify(news) // headline-author-location-content

上边其实只是一种语法糖,完整的”trait约束“是用泛型来实现的:

// 在泛型参数后面跟要求实现的trait
fn notify<T: Summary>(item: T) {
  println!("{}", item.summarize())
}

impl Trait语法糖更适用于短小的示例,而trait约束则更适用于复杂情形,例如多个参数的时候,trait约束更加简洁:

// impl Trait
fn notify(item: impl Summary, item2: impl Summary) {}
// trait约束
fn notify<T: Summary>(item: T, item2: T) {}

允许多个trait对参数做限制:

// 使用加号来要求item需要同时实现Summary和Display两个trait
fn notify<T: Summary + std::fmt::Display>(item: T) {
  println!("{}", item.summarize())
}

使用where语法简化trait限制:

fn some_function<T: Summary + std::fmt::Display, U: Debug + Clone>(param1: T, param2: U) {}

// 等同于:
fn some_function<T, U>(param1: T, param2: U) where
  T: Summary + std::fmt::Display,
  U: Summary + Clone {
    // ...
}

限制返回值类型

使用trait还可以要求返回值实现了某个trait:

// 要求返回值实现了Summary trait
fn notify(item: String) -> impl Summary {
  Tweet {
    username: item,
    content: String::from("content"),
    reply: String::from("reply"),
    retweet: String::from("retweet"),
  }
}
notify("str".to_string());

特殊情况,Tweet和NewArticles都实现了Summary,却无法通过编译:

fn notify(switch: bool) -> impl Summary {
  if switch {
    NewsArticles {
      headlline: String::from("headline"),
      content: String::from("content"),
      author: String::from("author"),
      location: String::from("location"),
    }
  } else {
    // 报错, if 和 else类型不兼容,这是由于trait的工作方式导致的
    // 后便会讲如何使用”trait对象“来存储不同类型的值
    Tweet {
      username: String::from("username"),
      content: String::from("content"),
      reply: String::from("reply"),
      retweet: String::from("retweet"),
    }
  }
}
notify(true);

使用trait解决泛型章节的largest函数问题

还记得前面章节中largest函数吗,当时忽略了报错的问题,这里我们来使用trait来解决,先回忆一下这个函数:

// 求数组中的最大值
fn largest<T>(list: &[T]) -> T {
  let mut largest = list[0];
  for &item in list.iter() {
    if item > largest { // 这里会报错,因为T可能是任意类型,并不是所有类型都可以比较
      largest = item
    }
  }
  largest
}
println!("{}", largest(&['a','b','c','d']))
println!("{}", largest(&[1,2,3,3]))

使用trait约束来解决:

// 要求T类型实现PartialOrd和Copy trait
// PartialOrd用于实现比较功能,
// Copy用于取值时对值进行复制,而不是所有权的转移
fn largest<T: std::cmp::PartialOrd + Copy>(list: &[T]) -> T {
  let mut largest = list[0]; // 这里需要Copy,因为比如参数全是String类型时,成员所有权是不能被移出的
  for &item in list.iter() {
    if item > largest { // 这里需要std::cmp::PartialOrd,因为并不是所有数据都是支持比较的
      largest = item
    }
  }
  largest
}
println!("{}", largest(&['a', 'b', 'c', 'd'])); // d
println!("{}", largest(&[1, 2, 3, 3])) // 3

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

为结构体实现方法时,可以有条件的实现,比如当结构体满足trait限制条件时,才为结构体实现某个方法:

#[derive(Debug)]
struct Pair<T> {
  x: T,
  y: T,
}

// 实现一个含有通用类型T的方法
impl<T> Pair<T> {
  fn new(x: T, y: T) -> Self {
    Self {
      x,y
    }
  }
}
let pair = Pair::new(100, 50);
println!("{:?}", pair); // Pair { x: 100, y: 50 }

// 实现一个对 结构体成员类型 有要求的方法
// 这里要求T类型实现Display和PartialOrd trait
// 只有实现了这两个trait的类型Pair<T>,
// 才实现cmp_display方法
impl<T: std::fmt::Display + std::cmp::PartialOrd> Pair<T> {
  fn cmp_display(&self) -> bool {
    if self.x > self.y {
      println!("larger:{}", self.x)
    } else {
      println!("smaller:{}", self.y)
    }
  }
}
pair.cmp_display(); // larger:100

覆盖实现(blanketimplementation)

对满足trait约束的所有类型实现,这个特性真的炸裂:

// 定义一个Log trait
trait Log {
  fn log(&self);
}

// 注意这里的 for T 直接为所有实现了Display的类型实现log方法
impl<T: std::fmt::Display> Log for T {
  fn log(&self) {
    println!("{}", self)
  }
}

2.log(); // 2
news.log() // error, 上边的news未实现Display

不加trait限制可以为所有类型实现:

trait LogAlwaysOne {
  fn log_one(&self) {
    println!("{}", 1)
  }
}

// 为所有类型实现LogAlwaysOne trait
impl<T> LogAlwaysOne for T {}

// 例如下面类型都可以调用log_one()方法
1.log_one(); // 1
'1'.log_one(); // 1
"1".log_one(); // 1
news.log_one(); // 1