10分钟从 Go 入门 Rust

266 阅读16分钟

Rust 是什么

Rust 和 Go 有什么不同

Rust 的语法特性

变量

在 Rust 中变量默认是不可变的,一个值绑定一个名称,这意味着不能对不可变变量进行重新赋值。通过在编译层面保证变量值不会被改变,显式确定语义,可以有效减少程序中 bug 的产生。当然,如果想要变量可变,只需要在变量名前加上 mut 声明即可。

Rust 允许在同一个作用域中定义重名的变量名,本质上是通过复用变量名去创建新变量,解决了计算机领域两大难题之一,而这在 Go 中是不允许的。

func main() {
    var x = 3
    x = 4
    var x = 'c'
}
fn main() {
    let x = 3;
    x = 4; // 编译错误,不能给不可变变量赋值两次
    let x = 'c';
  
    let mut y = 3;
    y = 4; // 编译正确
}

元组

元组是一个可以包含各种类型值的组合。元组使用括号 () 来构造(construct),而每个元组自身又是一个类型标记为 (T1, T2, ...) 的值,其中 T1、T2 是每个元素的类型。在 Rust 中,函数不支持返回多个值,但可以使用元组来实现返回多个值的效果。

// 元组可以充当函数的参数和返回值
fn reverse(pair: (i32, bool)) -> (bool, i32) {
    // 可以使用 `let` 把一个元组的成员绑定到一些变量
    let (integer, boolean) = pair;

    (boolean, integer)
}

let (a,b) = reverse((b,a))

表达式

在 Rust 中,代码块也是表达式,所以它们可以用作赋值中的值。代码块中的最后一个表达式将赋给适当的表达式,例如局部变量。但是,如果代码块的最后一个表达式结尾处有分号,则返回值为 ()

fn main() {
    let x = 5u32;

    let y = {
        let x_squared = x * x;
        let x_cube = x_squared * x;

        // 将此表达式赋给 `y`
        x_cube + x_squared + x
    };

    let z = {
        // 分号结束了这个表达式,于是将 `()` 赋给 `z`
        2 * x;
    };
    
    let a = if x == y {
        z
    } else {
        x
    }

    println!("x is {:?}", x);
    println!("y is {:?}", y);
    println!("z is {:?}", z);
}

枚举

Go 中没有枚举,只能曲线救国

type enumType int

const (
    a enumType = iota
    b
    c
)

Rust 中的枚举功能要比 Go 要强大的多,体现在可以很容易的为枚举中的每个成员绑定不同类型和数量的值。

enum Message {
    a(u32),
    b(String),
    c(u32, u32, u32), // 绑定元组
    d{x: i32, y: i32}, // 绑定匿名结构体
    e,
}

// 拥有隐式辨别值(implicit discriminator,从 0 开始)的 enum
enum Number {
    Zero,
    One,
    Two,
}

let n = Number::Zero as u8 // value is 0

// 拥有显式辨别值(explicit discriminator)的 enum
enum Color {
    Red = 0xff0000,
    Green = 0x00ff00,
    Blue = 0x0000ff,
}

匹配

Rust 中的匹配类似于 Go 中的 switch,但其功能更强大,支持各种类型的匹配,并经常用来对包装类型(enum)进行解构。

match message {
    Message::a(val) => val,
    Message::c(c1, c2, c3) => {
        println!("{}-{}-{}", c1, c2, c3);
        c1 // 无分号表示表达式返回值
    },
    _ => 0, // _ 匹配所有情况 
}
    
// 只写了 a 和 c,报错
match message {
    Message::a(val) => val,
    Message::c(c1, c2, c3) => {
        println!("{}-{}-{}", c1, c2, c3);
        c1 // 无分号表示表达式返回值
    },
}

fn main() {
    let pair = (2, -2);
    match pair {
        (x, y) if x == y => println!("These are twins"),
        // ^ `if` 条件部分是一个卫语句
        (x, y) if x + y == 0 => println!("Antimatter, kaboom!"),
        (x, _) if x % 2 == 1 => println!("The first one is odd"),
        _ => println!("No correlation..."),
    }
}

match 16 {
    0             => println!("I haven't celebrated my first birthday yet"),
    // 可以直接匹配(`match`) 1 ..= 12,但那样的话孩子会是几岁?
    // 相反,在 1 ..= 12 分支中绑定匹配值到 `n` 。现在年龄就可以读取了。
    n @ 1  ..= 12 => println!("I'm a child of age {:?}", n),
    n @ 13 ..= 19 => println!("I'm a teen of age {:?}", n),
    // 不符合上面的范围。返回结果。
    n             => println!("I'm an old person of age {:?}", n),
}

enum Option {
    Some(u32),
    None
}

if let Some(v) = some_value {
    // do someting 
} else {
    // do else something
}
// 语法糖,相当于
match some_value {
    Some(v) => // do something
    _ => // do else something
}

方法

在 rust 中,struct 的方法必须写在 impl Struct 包括起来的代码块中

struct Point {
    x: f64,
    y: f64,
}

// 实现的代码块,`Point` 的所有方法都在这里给出
impl Point {
    // 这是一个静态方法(static method)
    // 静态方法不需要被实例调用
    // 这类方法一般用作构造器(constructor)
    fn origin() -> Point {
        Point { x: 0.0, y: 0.0 }
    }

    // 另外一个静态方法,需要两个参数:
    fn new(x: f64, y: f64) -> Point {
        Point { x: x, y: y }
    }
    
    // 这是一个实例方法(instance method)
    // `&mut self` 是 `self: &mut Self` 的语法糖(sugar),其中 `Self` 是方法调用者的
    // 类型。在这个例子中 `Self` = `Point`
    fn set(&mut self, x: f64, y: f64) {
        self.x = x;
        self.y = y;
    }
}

默认值

Rust 里面不存在默认值,要求所有的变量都要被显示赋值之后再被使用

struct A {
    b: u8
    c: u8
}

fn main() {
    let x: i32;
    println!("{x}"); // 编译错误

    let a = A{b: 1} // 编译错误
}


字符串 str、&str 与 String

在 Golang 中,[]byte与string的实现存在差异,所以导致了两者之间相互转换需要进行深拷贝

在 Rust 中,存在两种字符串类型,str和String,他们两个的实现分别和[u8]以及Vec<u8>完全相同,所以在相互转换的时候,两者不需要进行拷贝。但 Rust 中要求字符串里的数据必须是 UTF8,所以在字符切片转换成字符串时,需要进行UTF8检查。

str的含义是内存里的一块长度未知的字符串,而&str是固定长度字符串的引用,由两部分组成:指向字符串序列的指针和字符串长度值。String 是堆分配且可动态增长的字符串,被存储为由字节组成的 vector。

需要注意的是,由 &str 转为 String 的性能开销高于 String 转为 &str,因为前者需要在堆上分配内存去复制 str,而后者只需要栈上复制 String 指针即可。

const data: &str = "data";

fn main() {
    let byte_data: &[u8] = data.as_bytes(); // 不需要检查,直接转换
    let s = std::str::from_utf8(byte_data).unwrap(); // 需要检查
    let s = unsafe {
        std::str::from_utf8_unchecked(byte_data) // 使用 unsafe 方法绕过检查
    };
    let string: String = s.to_string(); // 需要深拷贝
    let s: &str = &string;
}

泛型 & Trait & 反射

Rust 中通过 trait 定义共享的行为,提供继承和默认实现,类似于 Go 中的 interface。值得注意的是,在 Rust 中为 struct 实现不同 trait 的同名默认方法时,不需要强制为 struct 实现同名方法以覆盖,而是可以通过 self 或完全限定语法去完成特定调用。

type testInterface interface {
   TestMethod()
}

var _ testInterface = (*Test)(nil)

type Test struct {
   A string
   B string
}

func (t Test) TestMethod() {
   //TODO implement me
   panic("implement me")
}
trait Stringfy {
    fn tostring(&self) -> String {
        String::from("default method 1")
    }
}

trait Debugfy {
    fn tostring(&self) -> String {
        String::from("default method 2")
    }
}

impl<T, U> Stringfy for Task<T, U> {
}

impl<T, U> Debugfy for Task<T, U> {
}

fn main(){
    let task = Task::new();
    
    // task.tostring() // 编译错误
    Stringfy::tostring(&task) // 编译成功
    Debugfy::tostring(&task) // 编译成功
}

trait Person {
    fn name(&self) -> String;
}

// Person 是 Student 的父 trait。
// 实现 Student 需要你也 impl 了 Person。
trait Student: Person {
    fn university(&self) -> String;
}

Go 虽然有泛型,不过由于历史原因使用还很少。Rust 泛型如下

// 泛型
struct Task<T: Debug, U: Debug> {
    x: T,
    y: U,
}

impl<T: Debug, U: Debug> Task<T, U> {
    // 关联函数  
    pub fn new(x: T, y: U) -> Self {
        Task {x, y}
    }
}

Rust 没有 golang 里面的 interface 结构,自然也没有运行时反射,但Rust 可以使用 Any trailt 实现类似于 golang 中类型断言的效果

var value interface{} = "value"

if str, ok = value.(string); ok {

}else{

}
let value_any = value as &dyn Any;

// Try to convert our value to a `String`. If successful, we want to
// output the String`'s length as well as its value. If not, it's a
// different type: just print it out unadorned.
match value_any.downcast_ref::<String>() {
    Some(as_string) => {
        println!("String ({}): {}", as_string.len(), as_string);
    }
    None => {
        println!("{value:?}");
    }
}

错误处理

enum Option<T> {
    Some(T),
    None,
}

enum Result<T,E> {
    Ok(T),
    Err(E),
}

Rust 是一门新时代的安全性语言,它直接剔除了空值,也即 Go 中的 nil,取而代之的是枚举类型 Option<T>。由于根本没有空值,所有可能产生空值的返回都会被包装成 Option<T> 类型,并要求我们显式解构 Option<T> 获取值,这可在一定程度上保证避免常见错误如假设某一个值不为空而对其进行操作,但值实际为空而造成 panic。

fn main() { 
    let y = String::from("rust");
    let c = y.chars().nth(0).unwrap(); // unwrap 快速解构,当值为 None 时会 panic,应尽量避免使用
}

不同于 Go 错误处理,Rust 通常使用枚举类型 Result<T, E> 作为返回值,再通过对其解构来获取内容是正确或错误,这样一来就可以省掉处理异常时的一些性能开销。同时也要求开发者显示处理异常,而不能简单的忽略

func main() {
    err := callFunc()
    if err != nil {
        fmt.Println(err.Error())
    }
    data, _ := callFunc2() //简单的忽略了异常
}
fn main() -> Result<(), io::Error> {
    let path = "/tmp/data";
    // 可使用 ? 语法糖简化处理,错误时直接向上层返回
    // 相当于
    //let s = match read_file() {
    //    Ok(s) => s,
    //    Err(err) => return err.into(),
    //}
    let s = read_file()?;
    
    Ok(())
}

fn read_file(path: &str) -> Result<String, io::Error> {
    fs::read_to_string(path)
}

资源管理

不同于 Go 的设计理念,Rust 采用 RAII 机制去管理内存,即资源获取就是初始化,资源释放就是析构。Rust 是通过为变量类型实现 Drop trait 来完成自动析构,当变量离开作用域时就自动调用 drop 函数释放资源。RAII 相较于 GC 而言性能和实时性更高,且能够管理所有种类的资源而非只有内存这一种。

func main(){
    l := sync.Mutex
    l.Lock()
    defer l.Unlock()
    ...
}
fn locked_func(m: &Mutex<()>) {
    let _lock = m.lock().unwrap();
}

fn main() {
    let lock = Mutex::new(());
    locked_func(&lock);
    let _lock = lock.lock().unwrap();
    println!("Shouldn't reach here if RAII doesn't work.");
}

模块可见性

默认情况下,模块中的项拥有私有的可见性(private visibility),不过可以加上 pub 修饰语来重载这一行为。模块中只有公有的(public)项可以从模块外的作用域访问。

mod root {
    mod A {
        mod B {
            fn B1() {} // 私有
            pub fn B2() {} // 公有,可被导出
        }
        // `pub(crate)` 使得函数只在当前 crate 中可见
        //   | pub ( self ) 函数只在当前模块可见,与不加 pub 作用相同
        //   | pub ( super ) 函数只在当前模块的父模块可见
        //   | pub ( in SimplePath ) 函数只在制定的 path 内可见, path 一定是该模块的祖先模块
        mod C {
            pub(crate) fn C1() {} // crate内公有,不可导出 
            pub(self) fn C2() {} // 模块内共有,相当于私有
            pub(super) fn C3() {} // 在模块A内共有
            pub(root::A) fn C4() {} // 在模块A内共有
            
            pub(root::B) fn C5() {} // 非法
        }
    }
}

包管理

Rust 中的包管理使用术语 crate;cargo 是包管理工具,类似于 Go 中的 go module。

在 Rust 中允许不同版本的包共存,比如下面的依赖关系图,app 依赖log 0.5.0,other_library依赖log 0.4.*,这在 Rust 中是完全合法的。

学会了这些,大家就来尝试开发吧。

impl<IL, OL, C, Req, Resp, MkE: MkEncoder + 'static, MkD: MkDecoder + 'static, LB>
    ClientBuilder<IL, OL, C, Req, Resp, MkE, MkD, LB>
where
    C: SetClient<Req, Resp>,
    LB: MkLbLayer,
    LB::Layer: Layer<IL::Service>,
    <LB::Layer as Layer<IL::Service>>::Service:
        Service<ClientContext, Req, Response = Option<Resp>> + 'static + Send + Clone,
    <<LB::Layer as Layer<IL::Service>>::Service as Service<ClientContext, Req>>::Error:
        Into<crate::Error>,
    Req: EntryMessage + Send + 'static + Sync + Clone,
    Resp: EntryMessage + Send + 'static,
    IL: Layer<MessageService<Resp, MkE, MkD>>,
    IL::Service:
        Service<ClientContext, Req, Response = Option<Resp>> + Sync + Clone + Send + 'static,
    <IL::Service as Service<ClientContext, Req>>::Error: Send + Into<crate::Error>,
    MkD: MkDecoder + 'static,
    OL: Layer<
        BoxCloneService<
            ClientContext,
            Req,
            Option<Resp>,
            <<LB::Layer as Layer<IL::Service>>::Service as Service<ClientContext, Req>>::Error,
        >,
    >,
    OL::Service: Service<ClientContext, Req, Response = Option<Resp>> + 'static + Send + Clone,
    <OL::Service as Service<ClientContext, Req>>::Error: Send + Sync + Into<crate::Error>,
{
    fn build(){}...
}

image

Rust 自身特质

1.所有权

Rust 在内存管理上引入了所有权这个新概念,这意味着每一个值在任一时刻只能有一个所有者,当所有者离开作用域时会对内存进行释放。在这儿有句非常有趣的话:“我调用了一下别人,我就不属于我自己了”,也就是说,调用时会把变量移动到被调函数的作用域中,导致调用方中的变量失效。当然,如果变量类型实现了 Copy trait(一般都是存储在栈上的变量),则会对值进行复制后传入,在调用方中原变量仍然有效。

fn call_i32(x: i32) {
    println!("{}", x);
}

fn call_string(y: String) {
    println!("{}", y);
}

fn main() {
    let x = 0;
    call_i32(x);
    println!("{}", x); // 编译正确,变量 x 的类型实现了 Copy trait
    let y = String::from("rust");
    call_string(y);
    println!("{}", y); // 编译错误,变量 y 已被移动
}

当然,Rust 中还提供了引用以便我们去操控数据而不获取所有权。引用分为可变引用和不可变引用,且限制在同一时刻只能拥有一个可变引用或多个不可变引用,使得可以在编译器层面限制数据竞争。

fn call_string_ref(y: &String) {
    println!("{}", y);
}

fn call_string_mut_ref(z: &mut String) {
    z.push_str("rust");
    println!("{}", z);
}

fn main() {
    let y = String::from("rust");
    call_string_ref(&y);
    println!("{}", y);
    let mut z = String::from("hello"); // 可变变量
    call_string_mut_ref(&mut z);
    println!("{}", z);
}

2.生命周期

Rust 中变量的生命周期为在作用域中从初始化到销毁的整个过程。为避免悬垂引用的产生,Rust 编译器会强制检查所有引用的生命周期以确保被引用的对象存活时间长于引用者。当 Rust 编译器无法明确推断出某一引用的生命周期时,需要我们显式的进行生命周期标注。

// 编译错误
fn leak() -> &i32 {
    let i = 5;
    &i
}

// 编译错误,返回引用生命周期不明确
fn max_num(x: &i32, y: &i32) -> &i32 {
    if x > y {
        &x
    } else {
        &y
    }
}
// 编译正确,'a 为生命周期标注,会取 x、y 生命周期重叠部分
fn max_num<'a>(x: &'a i32, y: &'a i32) -> &'a i32 {
    if x > y {
        &x
    } else {
        &y
    }
}
fn main() {
    let x = 0;
    let y = 1;
    let max = max_num(&x, &y);
    println!("max: {}", max);
}

3.智能指针

相较于裸指针的操作不安全性,Rust 直接对裸指针进行封装得到智能指针,并通过对其行为进行限制以保证操作的安全性。与引用不同的是,智能指针通常拥有它们所指向的数据。与普通数据结构不同的是,智能指针实现了 Deref 和 Drop 两大 trait,前者让智能指针自动解引用,后者让智能指针自动析构。智能指针的 RAII 机制利用栈的特点,在栈元素作用域结束时自动调用析构函数来释放智能指针所管理的堆内存空间。

Box<T> 用于在堆上去存储数据,并在栈上留下指向堆中数据的指针,其主要特性在于单一所有权,并常用于去封装在编译时未知大小的类型。

enum Recursive {
    Cons(i32, Box<Recursive>), // 用于封装递归类型
    Nil,
}

Rc<T> 用于需要对堆上数据有多个只读访问的情况,支持多所有权,即允许一个值有多个所有者,通过引用计数的形式去保证数据的有效性,因而在此不可避免的引入了运行时开销。值得注意的是:避免引用循环需要使用 Weak<T>,多线程中需要使用 Arc<T> 保证安全。

enum Recursive {
    Cons(i32, Rc<Recursive>),
    Nil,
}

fn main() {
    let a = Rc::new(Recursive::Cons(0, Rc::new(Recursive::Nil)));
    let b = Recursive::Cons(1, Rc::clone(&a)); // 引用计数加一
    let c = Recursive::Cons(2, Rc::clone(&a));
}

Cell<T> 和 RefCell<T> 本质上不属于智能指针,它们主要是提供内部可变性的容器,即允许多个共享引用对其内部值进行改变,弥补了所有权机制在灵活性和某些场景下的不足。Cell<T> 只能用于 T 实现了 Copy trait 的情况,符合所有权规则,没有引入运行时开销。RefCell<T> 则适用于更普遍的情况,在运行时对借用规则进行检查,确保任意运行时刻最多只能有一个可变引用,否则就会产生 panic。

但Cell<T> 和 RefCell<T> 没有实现Sync,是线程不安全的,不能在多线程环境下使用,要在多线程环境下提供内部可变性,就要使用互斥锁等数据结构。

fn main() {
    let v = 1;
    let x = Cell::new(v);
    let y = &x;
    let z = &x;
    x.set(2);
    y.set(3);
    z.set(4);
    println!("{}-{}-{}-{}", x.get(), y.get(), z.get(), v);  // 4-4-4-1
}

// 编译通过,但在运行时因有两个可变引用同时存在而 panic
fn main() {
    let a = RefCell::new(0);
    let b = a.get_mut();
    let c = a.get_mut();
    println!("{}-{}", b, c);
}

在 Rust 中,通过组合 Rc<T> 和 RefCell<T> 可以实现同时存在多个所有者,且每个所有者都可以去修改内容的情形。

fn print_static(x: i32, y: i32) -> Box<dyn View> { // 返回封装 trait 对象
    if x > y {
        return Box::new(Struct1);
    } else {
        return Box::new(Struct2);
    }
}

Rust 还提供了关联类型,其和泛型类似,区别在于泛型 trait 可以多次实现,而关联类型 trait 只能实现一次,故而不需要在方法调用时再加上泛型参数。

trait Iterator {
    type Item;
    fn next(&mut self) -> Option<Self::Item>;
}

trait Iterator<T> {
    fn next(&mut self) -> Option<T>;
}

5.宏

Rust 中的宏不同于 C/C++ 中的宏简单按字符串替换代码,它基于语法树去进行操作,即在宏调用时会被展开成源码嵌入,再由编译器进行编译,故 Rust 中的宏是相对强大且安全的。Rust 中有声明宏和过程宏两种宏,声明宏通过匹配相应模式后再用另外的代码去替换当前代码来实现,而过程宏则通过操作输入的 Rust 代码去输出另外的 Rust 代码实现。宏编程这一部分内容相对复杂,有兴趣的可自行查阅 Rust 宏小册

// 过程宏
#[derive(Error, Debug)]
pub enum DataStoreError {
    #[error("data store disconnected")]
    Disconnect(#[from] io::Error),
    #[error("the data for key `{0}` is not available")]
    Redaction(String),
    #[error("invalid header (expected {expected:?}, found {found:?})")]
    InvalidHeader {
        expected: String,
        found: String,
    },
    #[error("unknown data store error")]
    Unknown,
}

// 声明宏
lazy_static! {
    static ref FIB_100: u32 = {
        fn fib(a: u32) -> u32 {
            match a {
                0 => 0,
                1 => 1,
                a => fib(a-1) + fib(a-2)
            }
        }

        fib(100)
    };
}

6.不安全

Rust 是一门被设计为可以实现操作系统的语言,因底层计算机硬件的固有不安全性,Rust 需要为开发者提供一些不安全的操作。在 Rust 中,不安全代码块需要显式用 unsafe 进行标注,告知编译器这些不安全操作将由开发者来保证安全。在 Rust 1.0 版本之后,安全 Rust 保证一定是安全的,故所有的安全问题只会出现在 unsafe 代码块中。当程序出现安全问题时,开发者们就只需要去排查 unsafe 代码块即可,大大减少 debug 所需排查的范围。值得注意的是,在 unsafe 块中只有某些特定功能如解引用裸指针才不会被编译器去检查内存安全,所以不是说 unsafe 块中的代码一定是不安全的。总结来说,安全 Rust 由编译器保证安全性,不安全 Rust 由开发者保证安全性。

fn main() {
    let x = 5;
    let p1 = &x as *const i32;
    unsafe {
        x = 6; // 编译错误,编译器对非特定功能依旧会有语法检查、借用检查等安全性限制
        println!("{}", *p1); // 不安全操作,解引用裸指针
    }
}

Rust 常用库

1.序列化与反序列化

Serde 是 Rust 中最常用的序列化与反序列化库,它支持将 Rust 中的数据结构方便的序列化成各种格式,比如 JSON, YAML, QueryString等,也支持将他们反序列化成 Rust 中的数据结构,基本上已经成为 Rust 中的事实标准。

use serde::{Serialize, Deserialize};

#[derive(Serialize, Deserialize, Debug)]
struct Point {
    x: i32,
    y: i32,
}

fn main() {
    let point = Point { x: 1, y: 2 };

    // Convert the Point to a JSON string.
    let serialized = serde_json::to_string(&point).unwrap();

    // Prints serialized = {"x":1,"y":2}
    println!("serialized = {}", serialized);

    // Convert the JSON string back to a Point.
    let deserialized: Point = serde_json::from_str(&serialized).unwrap();

    // Prints deserialized = Point { x: 1, y: 2 }
    println!("deserialized = {:?}", deserialized);
}

最后来尝试写一个safe的双向链表吧

参考

The Rust Programming Language - The Rust Programming Language

Introduction - Rust By Example