Rust 基础知识22 - Rust面向对象编程

Rust的面向对象编程

  • Rust通过trait实现多态。
  • Rust没有继承的概念,但是作为替代方案可以使用Rust中默认的trait方法进行代码共享。
  • Rust是支持封装的,通过pub关键对外暴露结构体中的细节(假定结构体就是类)

知识汇总

模拟GUI绘图(假的)

  • 让所有在画布上的类实现 Draw trait 即可。
  • draw/src/lib.rs 这里定义了 Draw trait,并且定义了一个Screen 结构,并拥有一个 components 属性用来存放所有 Draw 的动态指针(因为无法事先确定Vec的元素数量)

pub trait Draw {
    fn draw(&self);
}

pub struct Screen {
    pub components: Vec<Box<dyn Draw>>,
}

impl Screen {
    pub fn run (&self) {
        for component in self.components.iter() {
            component.draw();
        }
    }
}
  • 然后定义组件,这里定义了一个Button、一个Option 建立文件 components/src/lib.rs 这两个结构都实现了 Draw
use draw::{Draw} ;

pub struct Button {
    pub width: u32,
    pub height: u32,
    pub label: String,
}


impl Draw for Button {
    fn draw(&self) {
        //
        println!("Draw button on ({},{}) | Label = {}", self.width, self.height, self.label);
    }
}

pub struct SelectBox {
    pub width: u32,
    pub height : u32,
    pub options: Vec<String>,
}

impl Draw for SelectBox {
    fn draw(&self) {
        println!("Draw select-box on ({},{}) | Options : {:?}", self.width, self.height, self.options);
    }
}
  • chapter17/src/main.rs 主函数中,只要初始化Screen,最后调用 .run() 方法即可了,其实就是接口的传统用法。
use draw;
use components::{Button,SelectBox};
use draw::Screen;

fn main() {
    // 定义一个屏幕
    let screen = Screen {
        components: vec![
            Box::new(SelectBox{
                width: 75,
                height: 10,
                options: vec![
                    String::from("Yes"),
                    String::from("Maybe"),
                    String::from("No")
                ]
            }),
            Box::new(Button{
                width: 50,
                height: 10,
                label: "Ok".to_string()
            }),
        ]
    };
    screen.run();
}

trait 对象会执行动态派发

  • Rust编译器会在泛型使用trait约束是执行单态化,编译器会为每一个具体类型生成对应泛型函数和泛型方法的非泛型实现,并使用这些具体类型来替换泛型参数。

trait 对象必须保证对象安全

  • 如果一个trait中定义的所有方法满足下面两条规则,那么这个trait就是对象安全的

1、方法的返回类型不是Self。
2、方法中不包含任何泛型参数。

  • trait对象必须是安全的,因为Rust无法在使用trait对象时确定实现这个trait的具体类型究竟是什么,所以编译器无法在trait方法返回Self是使用原来的具体类型。
  • 标准库的Clone trait 就是一个不符合对象安全的列子:
pub trait Clone {
    fn clone(&self) -> Self;
}

实现一个简单的状态模式小实例

  • 模拟一篇博客文章可能拥有的各种行文,二期封装到不同的状态中,而Post 自身的方法对这些行文一无所知,通过这种代码的组织方式,我们只需要查看一个地方便能知晓已发布文章的行为差异。
  • 如果采用其他的实现来替代状态模式,那么就可能需要在Post甚至是main函数中使用match表达式来检查文章的状态,并根据状态执行不同的行为。
  • 而且用match在状态增加的时候还会导致代码的复杂度上升。
  • 基于状态模式可以免于在Post的方法或者使用Post的代码中添加match表达式。
  • 代码参考 首先是 main.rs:
// use draw;
// use components::{Button,SelectBox};
// use draw::Screen;
use blog::Post ;
fn main() {
    let mut post = OldPost::new();
    post.add_text("I ate a salad for lunch today.");
    assert_eq!("", post.content());
    println!("文章建立后的状态:{:?}", post.get_state());


    post.request_review();
    println!("提交审核的状态:{:?}", post.get_state());
    post.reject();
    println!("审核拒绝后的:{:?}", post.get_state());

    // 尝试修改文章,这里面修改应该是成功的,因为审核被拒绝后的状态是 Draft ,只有 Draft 才能修改。
    post.edit("See you at work!");
    println!("------");
    assert_eq!("", post.content(), "但是由于没有发布所以content() 还是无法返回内容的");

    post.request_review();
    println!("在审核:{:?}", post.get_state());

    // 继续修改,这是后状态式 PendingReview 所以修改会失败,那么内容理论上还是 "See you at work!"
    post.edit("I love you.");

    // 审核没有同意内容就拿不到
    assert_eq!("", post.content());

    // 通过
    post.approve();
    println!("同意后的状态:{:?}", post.get_state());
    assert_eq!("See you at work!", post.content());
}
  • 代码参考 blog/lib.rs
use std::fmt::{Debug, Formatter};

// use std::fmt::Display;

#[derive(Debug)]
pub struct Post {
    state: Option<Box<dyn State>>,
    content: String,
}

impl Post {
    pub fn new () -> Post {
        Post {
            state: Some(Box::new(Draft{})),
            content: String::new(),
        }
    }
    pub fn get_state(&self) -> &Option<Box<dyn State>>{
        &self.state
    }
    pub fn add_text(&mut self, text:&str) {
        self.content.push_str(text);
    }
    pub fn content(&self) -> &str {
        // 无聊的问题: ?为啥不直接 &self.content 这个问题很无语,如果这样就不叫状态模式了,状态模式中要把状态全部封装到State中
        // 这里面 unwrap() 方法不会返回Option的None,最终肯定返回T对象,也就是 State (接口)定义的返回值。
        // 通过 unwrap() 的过滤,那么实际上返回的就是 State (对象)了,调用状态上的content() 才可以哦。
        // 另外需要注意这里使用了 as_ref 这会得到一个 Option<&Box<dny State>>
        self.state.as_ref().unwrap().content(self)
    }
    // 这个和接口 State::request_review 没有任何关系,参数定义也不一样,这就是个内部方法。
    pub fn request_review(&mut self) {
        // 题外话:为什么需要调用 self.state.take() 而不是其他的 ( *self.state,这样不行如果解引用生命周期就不对了)(self.state,这样不行不运行借用)。
        // 首先要了解一下 Option<T> 的默认值 ,它实现了 Default 接口,也就是说Option<T> 是有默认值的,默认值时None
        // take() 方法用来取出state 中Some 值的所有权,这样就可以踏踏实实的从Some(s)中取出,并在原来的位置留下None

        if let Some(s) = self.state.take() {
            // 更改state 的状态 s.request_review -> Box<dyn State>
            self.state = Some(s.request_review());
        }
    }
    pub fn approve(&mut self) {
        if let Some(s) = self.state.take() {
            self.state = Some(s.approve());
        }
    }
    pub fn reject(&mut self) {
        if let Some(s) = self.state.take() {
            self.state = Some(s.reject());
        }
    }
    pub fn edit( &mut self, new_content:&str) {
        if let Some(s) = self.state.take() {
            self.state = Some(s.edit(self, new_content));
        }
    }
}

pub trait State {
    fn request_review(self:Box<Self>) -> Box<dyn State>;
    fn status_name(&self) -> String;
    fn approve(self:Box<Self>) -> Box<dyn State>;
    fn reject(self:Box<Self>) -> Box<dyn State>;
    // fn edit(&self, post:&mut Post,new_text:&str) {
    //
    // }
    fn edit(self:Box<Self> , post:&mut Post, new_str:&str) ->Box<dyn State>;

    // 这里涉及一个生命周期的概念要注意下,因为函数本身返回 post 参数的一部分
    // 所以要标明返回值 str 的生命周期,如下定义的生命周期长度与post 参数长度一致。
    fn content<'a> (&self, post:&'a Post) -> &'a str {
        // 这个默认的实现实际上隐藏了状态上要处理的 post.content 返回值。
        ""
    }
    // 如下方法没有定义生命周期所以会报错,可以放开试试,体会一下生命周期的作用。
    // fn content (&self, post:& Post) -> &str {
    //     // 这个默认的实现实际上隐藏了状态上要处理的 post.content 返回值。
    //     ""
    // }
}

// 给State 实现一个Debug 接口用于显示调试信息
impl Debug for dyn State {
    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
        f.debug_tuple("")
            .field(&"#########################".to_string())
            .field(&self.status_name())
            .field(&"#########################".to_string())
            .finish()
    }
}

#[derive(Debug)]
pub struct Draft {}

#[derive(Debug)]
pub struct PendingReview {}

#[derive(Debug)]
pub struct Published {}

impl State for Draft {
    // 注意这里的 self: Box<Self> 定义,而不是 self、&self、&mut self。
    // 这个语法意味着该方法只能被包裹着当前类型的Box实例调用,也就是上面 s = Box<dyn State> ,s.request_review()
    // ? 它会在调用过程中获取Box<Self>的所有权,并使旧的状态失效,从而将Post的状态值转换为一个新的状态。
    fn request_review(self: Box<Self>) -> Box<dyn State> {
        // 返回预览的状态
        Box::new(PendingReview{} )
    }

    fn status_name(&self) -> String {
        "Draft state".to_string()
    }

    fn approve(self: Box<Self>) -> Box<dyn State> {
        self
    }

    fn reject(self: Box<Self>) -> Box<dyn State> {
        self
    }

    fn edit(self:Box<Self> , post:&mut Post, new_str:&str) -> Box<dyn State> {
        post.content.clear();
        post.content.push_str(new_str);
        self
    }
}

impl State for PendingReview {
    // 对于一个已经处于审核状态的文章来说再次发起审批请求不会改变任何状态。
    fn request_review(self: Box<Self>) -> Box<dyn State> {
        self
    }

    fn status_name(&self) -> String {
        "PendingReview state".to_string()
    }

    fn approve(self: Box<Self>) -> Box<dyn State> {
        Box::new(Published {})
    }
    fn reject(self: Box<Self>) -> Box<dyn State> {
        Box::new(Draft {})
    }
    fn edit(self:Box<Self> , post:&mut Post, new_str:&str) -> Box<dyn State> {
        self
    }
}


impl State for Published {
    fn request_review(self: Box<Self>) -> Box<dyn State> {
        self
    }

    fn status_name(&self) -> String {
        "Published state".to_string()
    }

    fn approve(self: Box<Self>) -> Box<dyn State> {
        self
    }
    fn content<'a> (&self, post:&'a Post) -> &'a str {
        &post.content
    }
    fn reject(self: Box<Self>) -> Box<dyn State> {
        self
    }
    fn edit(self:Box<Self> , post:&mut Post, new_str:&str) -> Box<dyn State> {
        self
    }

    // // 没有生命周期的函数编译会报错,不信可以试试
    // fn content (&self, post:& Post) -> &str {
    //     // 这个默认的实现实际上隐藏了状态上要处理的 post.content 返回值。
    //     &post.content
    // }
}

上面状态模式小实例的改进

  • 上面的例子告诉我们Rust 可以做一些面向对象的模式,这没有问题,但是就上面例子的功能而言,其实有刚好的方法进行处理,我们可以将状态和行为编码编码成类型,这样更明了和简单,更重要的是,它不会产生很多无意义的空方法,比如 Post 类既然调用 .content() 返回的是空字符,那么不如让他根本没有 .content()
  • 参考下面的例子,首先是修改后的 main.rs
use blog2::{Post,PostDraft,PostReview};

/// 注意这里面的实现,很有指导意义,尤其对于我们理解Rust的设计和使用思路。
/// 我相信一旦对设计思路理解深入,那么对于使用上也就会明了,那么Rust 也会成为很顺手的工具。
fn main() {

    let mut blog = Post::new(); // 这会创建一个 PostDraft 也就是草稿
    blog.add_text("Hello rust, I love u.");
    let blog = blog.request_review();
    let blog = blog.approve();
    println!("--------------------------");
    println!("Blog publish success, content is : {}", blog.content());

    // 现在想编辑文章,但是 blog.edit() 并不存在,因为现在 blog:Post , 而 .edit() 是存在于 PostDraft 的。
    // 所以需要先进性状态转换调用 Post::unapprove() 然后  PostReview::reject() 最后调用 PostDraft::edit()
    let blog = blog.unapprove();
    let mut blog = blog.reject();
    blog.edit("So I will persevere learning it.");

    // 需要留意下面的写法是行不通的,(注释掉的这一行)
    // 因为Rust 在语义上表达式与变量是有很大不同的,这涉及到一些生命周期的问题。
    // let blog_content = blog.request_review().approve().content();
    let blog = blog.request_review();
    let blog = blog.approve();
    let blog_content = blog.content();
    println!("Blog edit success, content is : {}", blog_content);
}
  • 然后是我们的新类库(我没有删掉旧的,留个纪念)blog2/lib.rs


// 发布后的文章
pub struct Post {
    content: String,
}

// 文章草稿
pub struct PostDraft {
    content: String,
}

// 文章预览的状态
pub struct PostReview {
    content: String,
}

impl Post {
    // 新建立一个草稿
    pub fn new() -> PostDraft  {
        PostDraft {
            content : String::new(),
        }
    }

    // 获取文章内容
    pub fn content (& self) -> &String {
       &self.content
    }

    // 撤销文章的发布状态
    pub fn unapprove(self) -> PostReview {
        PostReview {
            content: self.content
        }
    }
}

impl PostDraft {

    // 返回一个预览状态的对象
    pub fn request_review(self) -> PostReview {
        PostReview {
            content : self.content
        }
    }

    // 向草稿添加文字
    pub fn add_text(&mut self, content:&str) {
        self.content.push_str(content);
    }

    // 编辑文本
    pub fn edit(&mut self, content:&str) {
        self.content.clear();
        self.add_text(content);
    }
}

impl PostReview {
    pub fn approve(self) -> Post {
        Post {
            content : self.content
        }
    }
    pub fn reject(self) -> PostDraft {
        PostDraft {
            content: self.content
        }
    }
}

结束

  • 感谢阅读,See you at work.
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,293评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,604评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,958评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,729评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,719评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,630评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,000评论 3 397
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,665评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,909评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,646评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,726评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,400评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,986评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,959评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,197评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,996评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,481评论 2 342

推荐阅读更多精彩内容