简介
- Trait 类似其他语言中的接口,但是又不完全一样,Rust 可以扩展的事儿更多。
什么叫特征Trait
- 可以定义抽象的共享行为
- Trait bounds (约束), 泛型类型参数指定为实现了特定行为的类型。
如何定义
- Trait 的定义,把方法签名放在一起,来定义某种目的所必须的一组行文
1、使用关键字 trait
2、通常情况下只有方法签名,没有具体实现,或者说无需具体实现。
3、trait 可以有多个方法,每个方法签名占一行,以`;`结尾。
4、实现该trait 的类型必须提供具体的方法实现。
- 举例:
// 定义一个Trait
pub trait Summary {
fn summarize(&self) -> String;
}
在类型上实现trait
- 实现方法与结构实现方法类似。
- 不同之处是需要添加 for 那个具体结构,比如 :
impl TName for Struct
,这个设计思路和传统语言也是有差别。 - 举例,实现上面的 Summary
fn main() {
let mut user = Tweet {
user_name: String::from("linhai"),
replay_count : 0,
like_count : 0,
};
user.like();
user.like();
user.like();
println!("value: {}, summary : {}", &user.get_like(), &user.summarize());
}
// 定义一个Trait
pub trait Summary {
fn summarize(&self) -> String;
}
// 定义Tweet
pub struct Tweet {
user_name :String,
replay_count :u32,
like_count :u32,
}
// 定义一个文章
pub struct NewsArticle {
pub title :String,
pub content :String,
pub author :String,
}
// 给结构添加普通方法
impl Tweet {
fn like(&mut self) {
self.like_count += 1;
}
fn get_like(&self) -> u32 {
self.like_count
}
}
// 实现接口到某个具体类比如 Tweet
impl Summary for Tweet {
// 接口方法的具体实现
fn summarize(&self) -> String {
format!("{} like count :{} , replay count :{}", &self.user_name, &self.replay_count, &self.like_count)
}
}
- 上面的函数太长了,准备结构上优化一下,将乱七八糟的代码挪到
lib.rs
中,这时候后会发现一些需要修改的问题:
1、头部加上 use hello::{Summary, Tweet}; 为什么要引入Trait ?这就是特性!
2、impl Tweet 的两个方法需要公开也就是添加pub 关键字,否则main就拿不到了。
3、因为原来的Tweet 的结构属性定义的全都是私有,所以你要清楚属性全都访问不到了,但是我并不想暴露私有属性,怎么办呢,添加个 ::create 创建方法吧。
- 新建立 lib.rs 并改进后的代码为:
// 定义一个Trait
pub trait Summary {
fn summarize(&self) -> String;
}
// 定义Tweet
pub struct Tweet {
user_name :String ,
replay_count :u32,
like_count :u32,
}
// 定义一个文章
pub struct NewsArticle {
pub title :String,
pub content :String,
pub author :String,
}
// 给结构添加普通方法
impl Tweet {
// 因为 user_name ,replay_count, like_count 都是私有的所以通过这个方法进行初始化值
pub fn create(user_name: String ) -> Tweet {
Tweet {
user_name ,
replay_count: 0 ,
like_count: 0,
}
}
pub fn like(&mut self) {
self.like_count += 1;
}
pub fn get_like(&self) -> u32 {
self.like_count
}
}
// 实现接口到某个具体类比如 Tweet
impl Summary for Tweet {
// 接口方法的具体实现
fn summarize(&self) -> String {
format!("{} like count :{} , replay count :{}", &self.user_name, &self.replay_count, &self.like_count)
}
}
- 此时 main.rs 被精简为:
use hello::{Summary, Tweet};
fn main() {
let mut user = Tweet::create("linhai".to_string());
user.like();
user.like();
user.like();
println!("value: {}, summary : {}", &user.get_like(), &user.summarize());
}
-
返回结果与之前一致
- 通过上面的例子我们需要知道,使用某个类的 trait 方法时一定要将trait 定义包含进来,否则无法使用,这是因为 Rust 的实现主题是trait 而非具体的结构。
实现Trait 的约束
- 这个比较特殊需要注意,可以在某个类型上实现某个trait 的前提条件是:
- 这个类型或这个trait 是在本地crate里定义的。
- 就是要遵循孤儿原则,其实就是一种安全行考量。
- 通俗的说就是要么本地类实现非本地接口,要么非本地类扩展实现本地接口,主要是为了防止破坏他人代码结构,实际上不难理解。
默认实现
- 默认实现是可以被重写的,这个就很有意思了,其实类似传统抽象类的概念。
- 学习了一段时间,实际上Rust 还是挺简单的,从使用上规避了很多C系列语言的一些常见Bug,还是挺好的。
- 而功能上实际上一点也不弱,很有创新性的语言挺好的。
- 比如接口定义可以改成:
pub trait Summary {
// fn summarize(&self) -> String;
fn summarize(&self) -> String {
"... more".to_string()
}
}
// 增加实现接口到某个具体类比如 NewsArticle
impl Summary for NewsArticle { }
- 修改后测试一下:(main.rs)
use hello::{Summary, Tweet, NewsArticle};
fn main() {
// 我们知道对于 NewsArticle 并没有对接口实现,但是因为接口中存在默认值所以 summarize() 也是可以使用的。
let article = NewsArticle {
author: "linhai".to_string(),
title: "This's a good man.".to_string(),
content: "Very good for the earth and he learing RUST very hard.".to_string(),
};
println!("summary : {}", article.summarize() )
}
Trait 作为参数
- 越来越有意思了啊,接下来看看Trait 的一些其他用法,先看看作为参数时。
- Trait bound 的标准用法,可以用于复杂的情况,举例:
use hello::{Summary, Tweet, NewsArticle};
fn main() {
// 我们知道对于 NewsArticle 并没有对接口实现,但是因为接口中存在默认值所以 summarize() 也是可以使用的。
let article = NewsArticle {
author: "linhai".to_string(),
title: "This's a good man.".to_string(),
content: "Very good for the earth and he learing RUST very hard.".to_string(),
};
let mut tweet = Tweet::create("linhai".to_string());
tweet.like();
notify_msg(article);
notify_msg(tweet);
}
// Trait 作为参数,当做一个普通类型用就可以了,实际上和接口类似
fn notify_msg (info: T) {
println!("summary : {}", info.summarize() );
}
- 对于上面的例子实际上也可以改成
fn notify_msg (info: impl Summary) {}
就是参数后面增加impl Trait 名称
,但是这种方式只能应用一些简单的约束,如果约束过多参数过多会比感觉比较乱,了解一下就好。 - 另外通过使用 + 可以指定多个Trait bound 进行更复杂的约束,例如:
// Trait 作为参数
fn notify_msg (info: T) {
println!("summary : {}", info.summarize() );
println!("display implement info : {}", info);
}
// Trait 作为参数
fn notify_msg2 (info:impl Summary + Display) {
println!("summary : {}", info.summarize() );
println!("display implement info : {}", info);
}
- 此时main.rs 无法编译通过因为我们的
NewsArticle
和Tweet
并没有实现std::fmt::Display
,不过有了之前的基础我们很容易改进一下我们的代码修改lib.rs
添加如下:
// 下面的代码没有百度,全靠代码提示,所以VSCode的代码提示还是要安装好的,很有用
// 实现Display接口
impl Display for NewsArticle {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "({}, {}, {})", self.author, self.title, self.content)
}
}
// 实现Tweet 接口
impl Display for Tweet {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "({}, {}, {})", self.user_name, self.replay_count, self.like_count)
}
}
- 此时
cargo run
试一下吧,OK 没问题的,实际上上面是接口用法还是很好理解的:
- 接下来看一下
where 子句
,主要是解决在泛型定义上过多的接口约束导致阅读上的难以理解,我们改一版程序看看效果即可,就是把泛型定义抽象到where 子句中,就是让无知的人类看起来更方便而已。
// Trait 作为参数
fn notify_msg (info: T)
where
T: Summary + Display,
{
println!("summary : {}", info.summarize() );
println!("display implement info : {}", info);
}
使用 Trait 作为返回类型
- 说实话看到这里我是有点费解的,接口作为函数返回值类型在普通不过了,但是到了Rust 这里返回值如果是Trait的话那么有点不同。
- impl Trait 只能返回确定的同一类型,返回不同的类型即便都实现了该Trait 也会报错。
- 仔细想了一下,Rust 中实际上实现定义的结构主体实际上是 Trait 而不是结构本身,所以在编译的时候Rust 必须对返回类型进行预编译定性,这导致它无法像普通语言那样按接口返回值,因为本身Trait 不是传统语言意义上的接口,这里只是为了好理解这样讲罢了。
- OK 看一下下面的例子就好了,如下代码段是非法的,除非都返回Tweet,或者NewsArticle,既然Rust 拥有许多成功开发的项目这个小缺陷,我想并不是大问题。
// 定义一个无聊的方法
fn get_summary_class(swt : bool) -> impl Summary {
if swt {
Tweet::create("linhai".to_string())
} else {
// Tweet::create("linhai".to_string())
// 返回NewsArticle 会报错,即便 Summary 也做了 NewsArticle的相关实现。
NewsArticle {
author: "linhai".to_string(),
title: "This's a good man.".to_string(),
content: "Very good for the earth and he learing RUST very hard.".to_string(),
}
}
}
学会了Trait 后看看之前的问题
- 先回顾一下之前的情况,看看能不能自己给他修复了,大致问题代码如下:
fn main() {
// 定义一个整数数组序列
let arr1 = [1,2,3,4];
// 定义一个字符数组序列
let arr2 = ['A','b','D','a'];
println!("largest : {}", largest(&arr1));
println!("largest : {}", largest(&arr2));
}
// 返回某个类型数组的最大值(这段代码有问题,先参考一下)
fn largest (list: &[T]) -> T {
let mut largest = list[0];
for &item in list {
if item > largest {
largest = item;
}
}
largest
}
- 直接编译后报错,根据出错的地提示在方法前面对接口泛型做接口限制:
- 修改
fn largest
如下:
// 返回某个类型数组的最大值(这段代码有问题,先参考一下)
fn largest (list: &[T]) -> T {
let mut largest = list[0];
for &item in list {
if item > largest {
largest = item;
}
}
largest
}
- 然后继续编译,继续出现错误提示,大致意思是说T还需要实现Copy trait:
move occurs because `item` has type `T`, which does not implement the `Copy` trait
| help: consider removing the `&`: `item`
- 直接修改代码加上Copy 这个接口试试:
fn main() {
// 定义一个整数数组序列
let arr1 = [1,2,3,4];
// 定义一个字符数组序列
let arr2 = ['A','b','D','a'];
println!("largest : {}", largest(&arr1));
println!("largest : {}", largest(&arr2));
}
// 返回某个类型数组的最大值(这段代码有问题,先参考一下)
fn largest (list: &[T]) -> T
where
T: std::cmp::PartialOrd + Copy,
{
let mut largest = list[0];
for &item in list {
if item > largest {
largest = item;
}
}
largest
}
-
另外如果使用 Clone 接口也可以,参考代码如下:
使用 Trait Bound 有条件的实现方法
- 在使用泛型类型参数的 impl 块上使用 Trait bound,我们可以有条件的为实现了特定Trait 的类型来实现方法。
- 举个栗子:
use std::{fmt::Display};
fn main() {
let var1 = Point::create(1, 2);
var1.cmp_display();
let var2 = Point::create('林', '海');
var2.cmp_display();
let var3 = Point::create(Result::Ok("yes"), Result::Err("what"));
var3.cmp_display(); // 这个编译的时候就会出错
}
// 定一个泛型结构
struct Point {
x: T,
y: T,
}
// 给泛型结构加上一个pub 的create 方法
impl Point {
pub fn create(x:T, y:T) -> Point {
Self {x, y}
}
}
// 接口的约束性实现,这种实现方式限定了使用cmp_display的类型,
// 比如i32既被Display实现,也被PartialOrd实现。
// 但是如果T是Result类型,这个类型就不符合Display+PartialOrd的接口约束所以也就看不到 cmp_display 这个方法
impl Point {
pub fn cmp_display (&self){
if self.x > self.y {
println!("X比较大");
}else{
println!("Y比较大")
}
}
}
结束
- 感谢阅读,See you at work.