go 开发者的 rust 入门

2021-11-27 17:55:43 浏览数 (1)

包管理和构建

  • cargo 和 go mod 比较类似,不过 cargo 除了是包管理工具,还是构建工具,比如有 cargo new/ build/ run/ test 等命令,分别近似于 go mod init, go build, go run, go test
  • cargo.toml 类似 go.mod 文件,不过由于 cargo 不仅仅是包管理工具,所以 cargo.toml 除了依赖,也包括了编译运行的一些信息
  • 和 golang 的包管理和公私有控制相比,复杂又丑陋, 你需要理解几个概念: Package、Crate、Module、Path, 2018 改过一次,第一次学习建议跳过.

关键字

  • golang 关键字个数 25, golang 开发者对于关键字似乎很节制,增加关键字就是增加复杂度
  • rust 36 个 , 还在增加

基础类型

  • 比较特殊的是 str 类型,和 slice 类型
  • 切片(slice)类型是对一个数组的引用片段, 这点和所有权相关
  • 字符串类型 str,通常是以不可变借用的形式存在,即&str
  • 表达字符串可以用 str, String, CStr, CString ...但是主要是 String 或者 &str, rust 的 string 比较复杂,而 go 语言的字符串处理简单清晰。go 语言的开发者应该很少会遇到在 rust 或者 python 中遇到的字符串处理问题(当然有些问题和所有权和生命周期机制相关).
    • String 有几种表达 bytes, chars ...
    • print! 类似 go 中 fmt.printf
    • format! 类似 go 中 fmt.Sprintf,而且不会影响所有权
    • eprint! 类似 fmt.fprintf(os.stderr, ...)
    • rust 中的 fmt 比 go 中复杂,比如 {} {:?} {2} {last}

可变性和所有权

这点是 rust 和其他语言都不太相同的地方,记住以下几点:

  1. 管理 heap 上的数据是所有权存在的原因
  2. 所有权规则
    • Rust 中的每一个值都有一个被称为其 所有者(owner)的变量。
    • 值有且只有一个所有者。
    • 当所有者(变量)离开作用域,这个值将被丢弃。
  3. 移动 let s1 = String::from("hello"); let s2 = s1; 这句话之后 s1 就失效了. Rust 永远也不会自动创建数据的 "深拷贝", 但是基本类型的组合会被自动拷贝,比如 let x = 5; let y = x, x,y 都是有效的. 如下是一些 Copy 的类型:
    • 所有整数类型,比如 u32。
    • 布尔类型,bool,它的值是 true 和 false。
    • 所有浮点数类型,比如 f64。
    • 字符类型,char。
    • 元组,当且仅当其包含的类型也都是 Copy 的时候。比如,(i32, i32) 是 Copy 的,但 (i32, String) 就不是。
imageimage

引用与借用

  1. & 符号就是 引用,它们允许你使用值但不获取其所有权
  2. 获取引用作为函数参数称为 借用(borrowing)

规则如下:

  1. 不允许修改借用引用的值
  2. 可变引用允许修改,但是定作用域中的特定数据只能有一个可变引用. 可以避免数据竞争(data race)
  3. 也不能在拥有不可变引用的同时拥有可变引用
  4. 一个引用的作用域从声明的地方开始一直持续到最后一次使用为止

即:在任意给定时间,要么 只能有一个可变引用,要么 只能有多个不可变引用。引用必须总是有效的。

函数和宏

  1. 函数关键字为 fn

控制流

  • if 表达式 if x {} else if y {} else {}
  • let 语句可以用 if let a = if x {1} else {2}
  • 循环有三种语法 loop, while, for, 熟悉 go 语言的可能会感觉很多余,在 go 里面实际上一个 for 关键字可以表达所有情况了

结构体

  • 关键子为 struct 和 go 中很类似,例子如下
  • 一旦 struct 是可变的,那么实例中的所有字段都是可变的
  • struct 新建、更新有一些语法糖,和 ts 有点类似
代码语言:txt复制
struct User {
    username: String,
    email: String,
    sign_in_count: u64,
    active: bool,
}

fn build_user(email: String, username: String) -> User {
    User {
        email,
        username,
        active: true,
        sign_in_count: 1,
    }
}

#[derive(Debug)]
struct Rectangle {
    width: u32,
    height: u32,
}

fn area(rectangle: &Rectangle) -> u32 {
    rectangle.width * rectangle.height
}

impl Rectangle {
    fn area(&self) -> u32 {
        self.width * self.height
    }
    
    // impl 块的另一个有用的功能是:允许在 impl 块中定义 不 以 self 作为参数的函数。
    // 这被称为 关联函数(associated functions)
}

枚举和模式匹配

  • rust 中的枚举比较强大,枚举中的选项可以是任意类型,比如常见的 Option<T>
  • 与之对应的是 match,类似 go 语言中的 switch case,其中 "_" 匹配对应 "default"
  • 除了 match 还有一种 if let, 可以看成 match 语法糖,也就是只匹配一种情况
  • 除了 Option<T> 还有一种常用的枚举: Result<T, E> 可以看成把 go 中的常见函数返回 (ret T, err error) 打包成了一个 枚举,可以看下面的例子,这是 rust 常用的错误处理模式;如果只用 match 模式匹配错误,其实这种模式并没有比饱受诟病的 golang 错误处理模式 if (err != nil) 更简单;但是 Result 还有很多方法,比如接受闭包,unwarp, expect 方法, ? 表达式 等会让代码变得更简洁清晰。
    • ?表达式还会自动使用 std::convert::From 上的 from 函数进行返回错误类型转换
代码语言:txt复制
# 使用 match 表达式
fn read_from_file() -> Result<String, io::Error>{
    let f = File::open("hello.txt");
    
    let mut f = match f {
        Ok(file) => file,
        Err(error) => return Err(e),
    };
    
    let mut s = String::new();
    match f.read_to_string(&mut s) {
        OK(_) => Ok(s),
        Err(e) => Err(e),
    }
}


# 使用 ? 表达式   链式表达式 (注意 ? 只用于返回为 Result 的函数内)
fn read_from_file() -> Result<String, io::Error>{
    let mut s = String::new();
    File::open("hello.txt")?.read_to_string(&mut s)?;
    Ok(s)
}

其他复合类型

  • 除了 struct 和 enum,rust 还提供了另两种复合类型,tuple 和 union

常见集合

  • 内置常见的集合类型为 Vector、HashMap、String,其中 Vector、HashMap 对应 golang 中的 slice 和 map,String 没有对应结构(非要对应可能类似 StringBuilder 吧)
代码语言:txt复制
let mut v1 = vec![];
let mut v2 = vec![0; 10];
let mut v3: Vec<i32> = Vec::new();

let mut hmap = HashMap::new();
hmap.insert(1, "a");

错误处理

  • panic! 不可恢复的错误,类似 go 中的 panic

泛型、trait

  • 泛型是 golang (至少 1.7 之前)缺失的,rust 的泛型和其他语言如 c 之类的比较类似,只要记住编译期会被替换成为具体的类型就可以
  • trait 类似 golang 中的 interface,有一些小的区别:
    • trait 需要显式的实现,用 impl SomeTrait for SomeStruct, 而 golang 中不需要
    • rust 中可以给已有的类型实现 trait, 而 golang 中不行,比如 impl SomeTrait for int 【类型或者 trait 二者之一是本地 crate 定义的】
    • rust 中的 trait 支持默认实现, 作为参数支持 指定多个 trait 【要求参数符合哪种 trait 的写法有多种,可以写在参数后,函数签名后,返回值后,这种工程实践没太多好处,反而给开发者心智负担:用一种方法,最好是只有一种方法来做一件事】
    • 返回值是 impl Trait 的时候只能返回一种确定类型,返回不同类型会报错
    • 使用 Trait Bound 可以有条件的为实现了特定 Trait 的类型来实现方法, 也可以为实现了其他 Trait 的任意类似有条件的实现 某另一个 Trait;为满足 Trait Bound 的所有类型上实现 Trait 叫做覆盖实现.
代码语言:txt复制
fn largest<T: PartialOrd   Copy>(list: &[T]) -> T {
    let mut largest = list[0];

    for &item in list.iter() {
        if item > largest {
            largest = item;
        }
    }

    largest
}
let number_list = vec![34, 50, 25, 100, 65];

let result = largest(&number_list);
println!("The largest number is {}", result);

声明周期

生命周期和所有权一样,是 rust 中最特殊的概念

  • 每个引用都有生命周期
  • 生命周期是引用保持有效的作用域
  • 大多数情况下生命周期是隐式的、可被推断的
  • 当引用的生命周期可能以不同的方式相互关联的时候:手动标注生命周期
  • struct 里面如果元素是引用,那么需要增加声明周期标注
  • 所有字符串字面值都有 'static 生命周期,表示整个程序持续时间

生命周期并不总是指明,省略规则如下【规则会变化,生命周期的规则在 rust 的进化过程会不断打补丁】:

  1. 每一个在输入位置省略的生命周期都将成为一个不同的生命周期参数。即对应一个唯一的生命周期参数。
  2. 如果只有一个输入的生命周期位置(无论省略还是没省略),则该生命周期都将分配给输出生命周期。
  3. 如果有多个输入生命周期位置,而其中包含着 &self 或者 &mut self,那么 self 的生命周期都将分配给输出生命周期。
代码语言:txt复制
// 周期 'a 实际生命周期是 x, y 中两个生命周期里面短的那个
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str{
    if x.len() > y.len(){
        x
    }else{
        y
    }
}

测试

  • 在函数上加上 #test, 可以把函数变成测试函数(go 语言里面是使用文件名和函数名的约定,相比而且 go 的做法舒服多了,代码还会更符合工程规范), 用 panic!(assert!) 来表示测试错误,也可以使用 Result<T, E> 来表示测试成功错误
  • tests 目录表示集成测试,下面每个测试文件都是一个单独的 crate
  • cargo test 的使用方式和 go test 非常类似,有很多近似的参数

迭代器和闭包

  • 闭包就是匿名函数(以及相关的引用环境),在 golang 中,大部分开发者都没有意识到 "闭包"的存在,因为他的表现和函数几乎一摸一样
  • rust 中的必报 和 python, java, ts 等中的比较类似,使用单独的语法:|参数|{ 实现} (不要求标注参数和返回值类型,使用编译器自动推断);使用的方法和 golang 大体相同,只有小部分区别:
    • 闭包表达式会由编译器自动翻译为结构体实例,并为其实现 Fn(没有改变环境的能力,可以多次调用)、FnMut(有改变环境的能力,可以多次调用)、FnOnce(没有改变环境的能力,只能调用一次) 三个 trait 中的一个。
    • 如果闭包中没有捕获了移动语义类型的环境变量,不修改,没使用 move 关键字,那么自动实现 FnOnce;如果需要修改,自动实现 FnMut,其他情况实现 Fn
    • 使用 move 关键字来强制让闭包所定义环境中的自由变量转移到闭包中
  • 实现 Interator trait 就是迭代器,要求实现 next 方法。for 循环会自动调用迭代器的 next 方法
    • 迭代器适配器是从一个迭代器转成另一个,比如 Map, Chain, Filter, Enumerate...
    • 消费器是除了主动 for 之外另一种消费迭代器的方法,比如 any, fold, collect, all, for_each..

Crate 和 Crates.io

  • rust 使用 Crates.io 作为官方的包托管平台,类似 python 的 pypi, js 的 npmjs;相比而言 golang 的做法非常简单直接高效,没有官方的包托管平台,因为包路径就是包地址(大部分时候就是一个 github 地址)也就是说知道包名,就知道包的开源代码路径在哪
  • 使用 pub use 可以对包重新导出到最外的层级
  • cargo 还有工作空间的概念,可以在一个空间内放多个 crate

智能指针

  • 智能指针是对指针的一层封装,提供了一些额外的功能,比如自动释放堆内存。智能指针区别于常规结构体的特性在于:它实现了 Deref (解引用, 通过解引用智能指针可以像常规引用一样使用) 和 Drop(析构,和 c 中的析构函数类似) 这两个 trait。
    • Box<T>: Box<T>是指向类型为 T 的堆内存分配值的智能指针。当 Box<T>超出作用域范围时,将调用其析构函数,销毁内部对象,并自动释放堆中的内存。还以用于赋能递归类型. (可以看作就是 golang 里面的 *T)
    • Rc<T>:单线程引用计数指针,非线程安全。Rc::clone(&a): 增加引用计数, Rc::strong_count; Rc::weak_count
    • RefCell<T>: 代表了对数据有唯一所有权;运行时检查借用规则,如果不满足就会 panic; 使用 borrow 返回 Ref<T>, borrow_mut 返回 RefMut<T>

并发

  • 使用 thread::spawn 来新建线程 (1:1 线程,和 python 之类语言的传统线程一回事)
  • 标准库提供了 channel 而 go 语言直接是一个关键字, chan 的实现也比较原始,用起来不是很方便,没有学到 go 语言的精髓
  • 这块没啥好说的,go 语言的并发 和 rust 的并发和不是一个时代的东西

0 人点赞