Rust-- 从Hello world 到 Hello Universe, Builder Pattern介绍

242 阅读3分钟

本文通过综合使用 builder pattern, enumstraits来构造一个程序,输出不同语言版本的hello world

// 第一个版本的代码,我们希望输出不同语言的hello world
// greet函数接收参数,根据参数来决定输出哪种语言的hello world
// 可以发现该代码有个缺陷, greet的参数是&str类型,如果调用greet时拼写错误,则不容易发现问题, 因此这种字符串对比的场景更推荐使用enum

fn greet(language: &str) {
    if language == "english" {
        println!("Hello World");
    }
}

fn main() {
    greet("english");
    greet("English"); // 不容易察觉的问题
}

使用enum来改进字符串对比

//使用enum改进
// greet函数中换成了match

enum Language {
    English,
    German,
    Chinese,
}

fn greet(language: Language) {
    match language {
        Language::English => println!("Hello World"),
        Language::German => println!("Hallo Welt"),
        Language::Chinese => println!("你好,世界"),
    }
}

fn main() {
    greet(Language::English);
    greet(Language::Chinese);
}

Hello World
你好,世界

还可以对greet函数改进,使其更符合rust习惯写法。 另外我们希望有一种默认语言,比如不指定语种时,默认为英语的“hello world”, 但是rust语言不支持keyword argument,想实现类似的效果,可以使用builder pattern


enum Language {
    English,
    German,
    Chinese,
}

struct Greeter {
    language: Language,
}

impl Greeter {
    fn new() -> Self {
        Self {
            language: Language::English,
        }
    }

    fn with_language(mut self, language: Language) -> Self {
        self.language = language;
        self // 返回Greeter类型,为了在main中调用时使用chaining写法
    }

    fn greet(&self) {
        let greeting = match self.language {
            Language::English => "Hello World",
            Language::German => "Hallo Welt",
            Language::Chinese => "你好,世界",
        };

        println!("{}", greeting);
    }
}

fn main() {
    let greeter = Greeter::new();
    greeter.greet();

    let greeter_zh = Greeter::new().with_language(Language::Chinese);
    greeter_zh.greet();
}



Hello World
你好,世界

以上代码可以运行,但先实例化一个Greeter实例,然后再调用 greet()方法,感觉上有点重复,如果可以直接打印Greeter实例就能达到效果呢?

实际上,rust确实提供了这种可能,通过实现Display trait, 官网对Display trait的描述如下,还附有一个例子,接下来我们也对代码进行改造,使其实现Display trait:

截屏2022-10-07 下午4.03.11.png

use std::fmt;

enum Language {
    English,
    German,
    Chinese,
}

struct Greeter {
    language: Language,
}

impl fmt::Display for Greeter {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        let greeting = match self.language {
            Language::English => "Hello World",
            Language::German => "Hallo Welt",
            Language::Chinese => "你好,世界",
        };

        write!(f, "{}", greeting)
    }
}

impl Greeter {
    fn new() -> Self {
        Self {
            language: Language::English,
        }
    }

    fn with_language(mut self, language: Language) -> Self {
        self.language = language;
        self // 返回Greeter类型,为了在main中调用时使用chaining写法
    }
}

fn main() {
    let greeter = Greeter::new();

    println!("{}", greeter);

    let greeter_zh = Greeter::new().with_language(Language::Chinese);
    println!("{}", greeter_zh);

    let greeter_de = Greeter::new().with_language(Language::German);
    assert_eq!(format!("{}", greeter_de), "Hallo Welt");
}



Builder Pattern

Rust has the following problems:

  • no default values for function arguments
  • no function overloading
  • no keyword arguments
  • therefore no support for optional arguments

the workaround: abusing the builder pattern to simulate optional arguments

useful crate: derive builder

//From Python to Rust中的例子
// 我们希望在实例化User的时候, id、email是必传参数,而first_name和last_name可选

#[derive(Debug)]
struct User {
    id: i32,
    email: String,
    first_name: Option<String>,
    last_name: Option<String>,
}

struct UserBuilder {
    id: i32,
    email: String,
    first_name: Option<String>,
    last_name: Option<String>,
}

// rust社区约定俗成,builder的名字为xxx+Buider
impl UserBuilder {
    // 类似于python中 id 和 email 是必传的参数, first_name和 last_name是可选的
    fn new(id: impl Into<i32>, email: impl Into<String>) -> Self {
        Self {
            id: id.into(),
            email: email.into(),
            first_name: None,
            last_name: None,
        }
    }

    fn first_name(mut self, first_name: impl Into<String>) -> Self {
        // 返回Self是为了chaining写法
        self.first_name = Some(first_name.into());
        self
    }

    fn last_name(mut self, last_name: impl Into<String>) -> Self {
        self.last_name = Some(last_name.into());
        self
    }

    fn build(self) -> User {
        // consume Builder and create the desired struct instance
        let Self {
            id,
            email,
            first_name,
            last_name,
        } = self; // destruct, so that we can use the property names without colon notation

        User {
            id,
            email,
            first_name,
            last_name,
        }
    }
}

impl User {
    fn builder(id: impl Into<i32>, email: impl Into<String>) -> UserBuilder {
        UserBuilder::new(id, email)
    }

    fn complete(&self) -> bool {
        self.last_name.is_some()
            && self.id > 0
            && !self.email.is_empty()
            && self.first_name.is_some()
    }
}

fn main() {
    // 使用Builder Pattern模拟keyword arguments
    let bob = User::builder(13, "bob@example.com")
        .first_name("Bob")
        .build();

    println!("Complete? {}", bob.complete());
    println!("bob_the_builder = {:#?}\n", bob);
}

参考

[1] www.youtube.com/watch?v=STW…

[2] From Python to Rust