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 中模块的并集,遵循以下原则:
如果没有显式表明模块,则每个文件就代表一个模块(类似 Python)
如果显式声明模块,则遵循声明(类似命名空间)
模块允许嵌套。只有平级或者更深层次的模块才允许访问私有的函数或者结构体。如果希望外部访问,必须使用 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);
}
容器
没什么好说的,基础语法都差不多,反正有自动导入也不用担心不知道名字是什么