Rust 概要

基础语法

Rust 是强类型语言,但是其 let 关键字可以充当 C++ auto 的角色。另外,其类型声明是后置类型声明:

let a:i32 = 10;

Rust 中的变量分为三种:

  • 常量:使用 const 声明的变量,不允许更改值

  • 不可变变量:使用 let 声明的变量,只能通过多次声明更改变量值

  • 可变变量:使用 let mut 声明的变量,可以随意更改值

常量必须要指定类型,不可变变量在多次声明的过程中允许改变类型。子作用域中的声明不会更改父作用域变量的值

不允许窄化转换
非零值不能转换为布尔值
没有自增和自减运算符

数据类型

Rust 中内置了诸如 i32, u32 这些宏,另外还添加了 isize, usize 这两个大小与 CPU 字宽度相同的类型

浮点数分为 f32, f64 两种,默认会使用 f64,因为两者处理速度差不多,但是 f64 精度更高

Rust 中的字符大小为 4 字节,只允许 UTF-8 编码

元组使用圆括号声明,数组使用中括号声明:

let a = (510, 1.2, 'c');
let b = [10, 20, 30];
println!("{}", a.2);
println!("{}", b[0]);

注释

注释方式和 C++ 一样

函数

函数的声明方式也是后置类型声明:

fn main() {
   println!("{}", add(1, 2));
}

fn add(a: i32, b: i32) -> i32 {
   return a + b;
}

函数不需要前置类型声明,只要你代码中能找到这个函数就行,写前面还是写后面都行

另外,对于返回值有一种简写形式:

fn add(a: i32, b: i32) -> i32 {
   a + b
}

需要注意的是,后面不能跟分号

条件语句

条件语句中的条件无需加小括号:

fn main() {
   let number = 3;
   if number < 2 {
      println!("number < 2");
   } else if number < 5 {
      println!("number < 5");
   } else {
      println!("number >= 5");
   }
}

另外还能实现类似 C++ 三目运算符的效果:

let number = if number > 2 { number*2} else {0};

条件语句还有一个简写方式,通常用在枚举中:

let i = 0;
if let 0 = i {
   println!("zero");
}

循环

let mut n = 1;
while n != 4 {
   println!("{}", n);
   n += 1;
}
let a = [10, 20, 30];
for i in a {
   println!("{}", i);
}
for i in 0..5 {
   println!("{}", i);
}
let _m = loop {
   if n == 10 {
      break n;
   }
   n += 1;
};

Rust 中没有 for 循环,只有 while 和 for_each 循环。另外 loop 循环相当于 while(true) 。但是不同的是可以使用 break 返回一个值

loop 在单独成块时末尾无需加分号,但是作为初始化语句的一部分时需要加分号(毕竟是语句)

这里之所以将变量命名为 _m 是因为我的 Rust 将 Warning 视为 Error,这里有命名 Warning

所有权

Rust 也有作用域规则,并将表达式分为值和变量两部分,遵循以下规则:

  • 栈中的值默认执行拷贝语义

  • 堆中的值默认执行移动语义

  • 变量可以引用其它变量

例如:

#![cfg_attr(
   debug_assertions,
   allow(dead_code, unused_imports, unused_variables, unused_mut)
)]

fn main() {
   let s_a = 10;
   let s_b = s_a; // 执行拷贝语义
   let h_a = String::from("hello");
   let h_b = h_a; // 执行移动语义,此处 h_a 会失效
   let mut h_c = h_b.clone(); // 执行拷贝
   print_s(h_b); // 执行移动语义,此处 h_b 会失效
   let r_a = &h_c; // r_a 是指向 h_c 的只读引用
   let r_b = &mut h_c; // r_b 是指向 h_c 的可写引用
}

fn print_s(str: String) {
   println!("{}", str);
}

第一行代码是为了关闭 unused warning,不关掉的话代码没法通过编译

Rust 将引用的过程称为租赁

另外,Rust 没有 free 或者 delete,它会在作用域结束时自动为你添加资源清理代码

与 C++ 不同的是,引用不会影响原宿主的生命周期,当原宿主失效时,引用必须重新绑定:

#![cfg_attr(
   debug_assertions,
   allow(dead_code, unused_imports, unused_variables, unused_mut)
)]

fn main() {
   let s1 = String::from("hello");
   let r = &s1;
   let s2 = s1; // 移交 s1 所有权, r 必须重新绑定
   let r = &s2;
}

悬垂引用在编译期就会被发现。

切片

在切片之前需要区分 str 和 String 类型。str 是 Rust 内置的基本类型,代码中使用 str 初始化的变量均为 str 的引用。String 是一种容器类型。

  • 切片的结果是只读的

  • 切片的类型为 &str

fn main() {
   let s1 = "hello"; // s1 是 &str 类型
   let mut s2 = String::from("hello"); // s2 是 String 类型
   let slice1 = &s2[0..3]; // slice1 是 &str 类型
   let slice2 = &s2[3..];
   let slice3 = &s2[..3];
   println!("{}", slice1);
   println!("{}", slice2);
   println!("{}", slice3);
}

结构体

struct Student {
   id: u32,
   name: String,
}

fn main() {
   let stu = Student {
      id: 1234,
      name: String::from("小明"),
   };
   let stu2 = Student {
      id: 2345,
      ..stu // stu 中非 id 字段被移动
   };
   // println!("{}", stu.name); // 此处不允许,stu.name 已经被移动
   println!("{}", stu2.name);
}

Rust 中的结构体无需加分号结尾,stu2 展示了另一种语法:当新的结构体 至少 有一个字段不同时,可以简化语法,这种语法我认为可以被成为更新

另外还有一个类元组的结构体:

struct Color(u32, u32, u32);

fn main() {
   let black = Color(0, 0, 0);
}

这种结构体的使用方式与元组相同,但是元组结构体是一个语句,末尾需要添加分号

还有两种绑定到结构体上的函数:

  • 如果有 &self 函数,称为方法

  • 如果没有 &self 函数,可以简单地将其理解为 C++ 中的静态成员函数

struct Int {
   data: u32,
}

impl Int {
   fn create(data: u32) -> Int {
      return Int { data }; // 这里 return 写不写都行
   }
   fn bigger(&self, b: &Int) -> bool {
      return self.data > b.data;
   }
}

fn main() {
   let a = Int::create(10);
   let b = Int::create(11);
   println!("{}", a.bigger(&b));
}

impl 可以写任意次,总的效果相当于他们的并集

还可以使用 特性 限制函数的签名,这与 C++ 中的纯虚函数类似:

struct Person {
   name: String,
   age: u8,
}

trait Descriptive {
   fn describe(&self) -> String {
      return String::from("[Object]");
   }
}

impl Descriptive for Person {
   fn describe(&self) -> String {
      return format!("{} {}", self.name, self.age);
   }
}

fn output(obj: &impl Descriptive) {
   println!("{}", obj.describe());
}

fn main() {
   let a = Person {
      name: String::from("小明"),
      age: 20,
   };
   output(&a);
   println!("{}", a.describe());
}

特性允许有默认实现,这点和 C++ 相似。一个 impl 块只能实现一个特性

另外,接受特性的函数还有一种简便的表示方式:

fn output<T: Descriptive>(obj: &T) {
   println!("{}", obj.describe());
}

特性可以进行组合:

fn output<T: Display + Clone>(obj: &T) {
   println!("{}", obj.describe());
}

此时类型 T 必须同时实现了 Display 和 Clone

还能使用 where 简化函数签名:

fn output<T, U>(obj: &T)
where
   T: Display + Clone,
   U: Clone + Debug,
{
   println!("{}", obj);
}

枚举

枚举的三种写法:

enum Book {
   Papery,
   Electronic,
}

enum Anmial {
   Dog(String),
   Cat(String),
}

enum Pair {
   Number { number: u32 },
   String { str: String },
}

fn main() {
   let book = Book::Papery;
   let dog = Anmial::Dog(String::from("a"));
   let pair = Pair::String {
      str: String::from("b"),
   };
   match pair {
      Pair::Number { number } => {
            println!("Number {}", number);
      }
      Pair::String { str } => {
            println!("String {}", str)
      }
   }
}

枚举中可以储存字段,访问这些字段只能使用 match 语法

泛型

简单的泛型语法:

struct Number<T> {
   data: T,
}

impl<T> Number<T> {
   fn get_data(&self) -> &T {
      return &self.data;
   }
}

fn main() {
   let a = Number { data: 10 };
   let b = Number { data: 11 };
   println!("{}", a.get_data());
}

和 C++ 一样,函数也允许有泛型,这是 impl 中也需要添加相应的类型,而且泛型的具体类型可以被推断出来。推断时不会发生类型转换

错误处理

Rust 将错误分为可恢复的错误和不可恢复的错误。

不可恢复的错误类似 C++ 中断言失败的宏:

fn main() {
   panic!("failed");
}

可恢复错误通过 Result<T,E> 枚举 表示,可能产生异常的函数的返回值都是 Result 类型的:

fn main() {
   let f = File::open("hello.txt");
   match f{
      Ok(file) =>{
            println!("file opened");
      },
      Err(err) =>{
            println!("Failed");
      }
   }
}

提示

实际上报错时显示类型是 tuple

使用 if let 语法可以简化处理流程:

fn main() {
   let f = File::open("hello.txt");
   if let Ok(file) = f {
      println!("File opened successfully.");
   } else {
      println!("Failed to open the file.");
   }
}

直接对 Result 调用 unwrap/expect 会导致系统直接挂掉

因为异常只是一个类型,所以传递时直接返回就行了,异常还有一个简单语法:

fn g(i: i32) -> Result<i32, bool> {
   let t = f(i)?;
   Ok(t) // 因为确定 t 不是 Err, t 在这里已经是 i32 类型
}

这里函数 f 的返回值是 Result,其 E 的类型必须和 g 的 E 的类型相同,? 运算符用来将异常取出,如果有异常直接返回,否则向下继续执行

生命周期参数

来看一个返回引用的函数的例子:

fn longer(s1: &str, s2: &str) -> &str {
   if s1.len() > s1.len() {
      return s1;
   }
   return s2;
}

fn main() {
   let s1 = "he";
   let s2 = "she";
   let r = longer(&s1, &s2);
   print!("{}", r);
}

遗憾的是 Rust 不会允许编译的,根本原因在于 longer 返回的是一个引用,但是引用的值的生命周期不知道。可以通过生命周期参数解决这个问题

下面我会将术语生命周期进行加粗,而和 C++ 中生命周期相同的那个术语不会

生命周期参数的语法是一个单引号后跟一个小写字母,习惯上以 'a 表示。生命周期注解描述了多个引用生命周期相互的关系,而不影响其生命周期。

现在使用生命周期参数来纠正这个函数:

fn longer<'a>(s1: &'a str, s2: &'a str) -> &'a str {
   if s1.len() > s1.len() {
      return s1;
   }
   return s2;
}

这个函数的签名分为了三个部分:

  • fn longer<'a> 中使用泛型的语法将生命周期参数引入函数

  • s1: &'a str 表明 s1 的生命周期至少与 'a 的生命周期一样长

  • &'a str 表示返回值的生命周期至少与 'a 一样长

'a 的生命周期是 s1 和 s2 中生命周期的较小值

下面无法通过编译的代码,从侧面证实了生命周期参数不会延长值的生命周期:

fn longer<'a>(s1: &'a str, s2: &'a str) -> &'a str {
   if s1.len() > s1.len() {
      return s1;
   }
   return s2;
}

fn main() {
   let r;
   {
      let s1 = String::from("he");
      let s2 = String::from("she");
      r = longer(s1.as_str(), s2.as_str());
   }
   print!("{}", r);
}

生命周期注释有一个特别的:’static 。所有用双引号包括的字符串常量所代表的精确数据类型都是 &’static str ,’static 所表示的生命周期从程序运行开始到程序运行结束,因此下面的代码可以通过编译:

let r;
{
   let s1 = "he";
   let s2 = "she";
   r = longer(s1, s2);
}
print!("{}", r);

备注

生命周期参数从语法上可以简单地认为就是一个特殊的模板参数

模块、包和箱

简单地来讲,含有 Cargo.toml 文件的项目就是一个包,项目编译后生成的二进制文件就是箱

Rust 中的模块类似于 C++ 中命名空间和 Python 中模块的并集,遵循以下原则:

  1. 如果没有显式表明模块,则每个文件就代表一个模块(类似 Python)

  2. 如果显式声明模块,则遵循声明(类似命名空间)

模块允许嵌套。只有平级或者更深层次的模块才允许访问私有的函数或者结构体。如果希望外部访问,必须使用 pub 公开

模块的使用方式与命名空间类似,同样是使用 :: ,但是导入运算符使用了 use 而不是 using

面向对象

封装可以使用模块,继承只能通过特性完成接口继承,多态也可以使用特性。但是类似 C++ 中的继承是不存在的,只能通过组合的方式

终端 IO

例如:

use std::io::stdin;
use std::io::BufRead;

fn main() {
   let args = std::env::args();
   for arg in args {
      println!("{}", arg);
   }
   // echo
   let mut buf = String::new();
   stdin().read_line(&mut buf).unwrap();
   println!("{}", buf);
}

文件 IO

先看一个单向读写的例子:

use std::fs;

fn main() {
   fs::write("1.txt", "hello").unwrap();
   let content = fs::read_to_string("1.txt").unwrap();
   println!("{:?}", content);
}

再看一个双向读写的例子:

use std::{
   fs,
   io::{Read, Seek, Write},
};

fn main() {
   let mut file = std::fs::OpenOptions::new()
      .write(true)
      .read(true)
      .open("1.txt")
      .unwrap();
   file.write(b"hello,world\n").unwrap();
   // 刷新缓冲区
   file.flush().unwrap();
   file.seek(std::io::SeekFrom::Start(0)).unwrap();
   // 读取数据
   let mut buf = String::new();
   file.read_to_string(&mut buf).unwrap();
   println!("{}", buf);
}

容器

没什么好说的,基础语法都差不多,反正有自动导入也不用担心不知道名字是什么