Rust 基础知识16 Trait

简介

  • 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());
}

  • 返回结果与之前一致


    image.png
  • 通过上面的例子我们需要知道,使用某个类的 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() )
}

image.png

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() );
}
image.png
  • 对于上面的例子实际上也可以改成 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 无法编译通过因为我们的NewsArticleTweet并没有实现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 没问题的,实际上上面是接口用法还是很好理解的:
    image.png
  • 接下来看一下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
}
  • 直接编译后报错,根据出错的地提示在方法前面对接口泛型做接口限制:
    image.png
  • 修改 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 接口也可以,参考代码如下:


    image.png

使用 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.

你可能感兴趣的:(Rust 基础知识16 Trait)