认识基础结构
// 定义参数
fn main () {
// main 函数是程序开始执行的地方
println!("Hello, world!");
// println! 宏用于打印文本到控制台
}
// rustc进行编译
// 编译后生成exe可执行文件和pdb调试文件
认识cargo
fn main() {
println!("Hello, world!");
}
cargo new helloWorld,创建一个新项目
// cargo 构建和npm类似,它是一个命令行工具,用于管理Rust项目的构建和依赖项。
// cargo check 它的作用是检查项目是否能够成功编译,但不会生成可执行文件,执行速度比较快。相当于执行了rustc --check命令。
// cargo build 它的作用是编译项目,但不会运行生成的可执行文件。相当于执行了rustc命令。发布时要加--release参数。可以进行优化,生成更小的可执行文件。
// cargo run 它的作用是编译并运行由当前项目。如果没修改过Cargo.toml文件,那么cargo run会默认调用rustc来编译项目,然后运行编译好的可执行文件。
// cargo clean 它的作用是删除项目中的所有构建产物,包括生成的可执行文件和所有中间文件。相当于执行了rustc --clean命令。
// cargo doc 它的作用是生成项目的文档,但不会生成可执行文件。相当于执行了rustc --document-private-items命令。
// cargo test 它的作用是运行项目的测试,但不会生成可执行文件。相当于执行了rustc --test命令。
// cargo bench 它的作用是运行项目的基准测试,但不会生成可执行文件。相当于执行了rustc --bench命令。
// cargo fmt 它的作用是格式化项目的代码,但不会生成可执行文件。相当于执行了rustc --emit=fmt命令。
// cargo install 它的作用是安装一个Cargo包,但不会生成可执行文件。相当于执行了rustc --install命令。
// cargo uninstall 它的作用是卸载一个Cargo包,但不会生成可执行文件。相当于执行了rustc --uninstall命令。
// cargo update 它的作用是更新Cargo.lock文件中的依赖项版本,但不会生成可执行文件。相当于执行了rustc --update命令。
// cargo publish 它的作用是发布一个Cargo包,但不会生成可执行文件。相当于执行了rustc --publish命令。
// cargo login 它的作用是登录Cargo.io,但不会生成可执行文件。相当于执行了rustc --login命令。
// cargo search 它的作用是搜索Cargo.io上的包,但不会生成可执行文件。相当于执行了rustc --search命令。
一个猜数游戏
// 调用io标准库
use rand::Rng;
use std::cmp::Ordering;
use std::io;
fn main() {
println!("这是一个猜数游戏!");
// 生成一个随机数
let secret_number = rand::thread_rng().gen_range(1..=100);
print!("这个数为{}", secret_number);
// 循环
loop {
println!("猜测一个数!");
// rust默认所有变量都是不可变的
// 如果不使用的变量,可以加下划线忽略,如let mut _foo = 1;
let mut foo = 1;
println!("{}", foo);
// foo=3;这和js不太一样,rust不允许这样做
// js对于原始数据类型(如 number、string、boolean、undefined、null、symbol 和 bigint),它们是存储在栈中的。
// 当你执行 let foo = 123; 时,123 这个数值直接存储在栈中。当你之后执行 foo = 3;,这是直接在栈上改变了 foo 指向的值,即把 foo 这个变量的引用改为了新的数值 3。
// 因此,对于原始数据类型,我们说是改变了栈值。
// 而对于引用数据类型(如 object、array、function 等),情况就不同了。这些类型的值实际是存储在堆中,而变量中存储的是指向堆中对象的引用(这个引用本身存放在栈中)。
// 当你改变这类值的内容时(比如修改数组的一个元素),你不是在改变引用本身的值(这个引用依然指向堆中的同一块内存),而是在改变堆中对象的状态。
// 如果是js,在上面例子中,foo 最初指向一个原始数据类型(数字),所以改变 foo 的值实际上是直接在栈上替换了这个值,并没有涉及到堆。
// 但是rust的栈值不能随便改动,所以rust不允许你直接改动原始数据类型的值。
// rust如果想改变变量,需要加一个mut关键字,这样就相当于js的let了
foo = 2;
let bar = foo;
println!("{}", bar);
// 定义一个字符串
// ::是rust表示关联函数,相当于js中的prototype,java中的static静态方法
let mut guess = String::new();
// 读取用户输入,将其放到 guess 变量的引用里,也就是赋值
io::stdin()
// 应为guess是随着用户的输入来修改,是可变的,所以要加mut,&表示引用,类似于react里的ref概念,一个是保持值不变,一个保持该值的栈地址不变
// read_line会返回一个io::Result,io::Result是一个枚举类型,它有两个成员Ok和Err,分别表示成功和失败。类似js的Promise
.read_line(&mut guess)
// 如果出错,就抛错
.expect("无法读取输入");
// 把字符串转换成数字
let guess: u32 = match guess.trim().parse(){
Ok(num) => num,
Err(_) => {
println!("输入无效,请输入一个数字!");
continue;
}
};
// {}是rust的占位符,类似于js里的模板字符串
println!("你猜测的数是:{}", guess);
// cmp比较的作用
match guess.cmp(&secret_number) {
Ordering::Less => println!("太小了!"),
Ordering::Greater => println!("太大了!"),
Ordering::Equal => {
println!("恭喜你,猜对了!");
break;
}
}
}
}
通用的编程概念
// 调用io标准库
// use rand::Rng;
// use std::cmp::Ordering;
// use std::io;
fn main() {
// 变量和可变性
// 默认变量是不可变的,其实rust里的变量更应该叫一次性赋值量,不能再赋值,除非特殊情况
let x = 5;
// x=10;这样的话编译器会直接报错
println!("x值是{}", x);
let mut y = 6;
y=y*2; //这样就是可以的,let mut也就和js里的let一样
println!("y值是{}", y);
// shadowing 隐藏
// 使用相同的名字声明一个新的变量,这个新的变量会隐藏掉之前的变量
// 效果类似js的var
let a =5;
let a = a+1;//其实就是开个新地址,将上个值赋给新地址,再换上新名字
println!("a的值是{}", a);
// 使用场景,减少变量重命名
let spaces = "jakshds";
let spaces = spaces.len();
println!("spaces的值从字符串变成了字符串长度:{}", spaces);
// 常量:所有的字母都是大写的,用下划线来连接
// 常量在绑定值之后不能再改变,也就是说let后面是可以改成let mut的,但是const不行
// 类型必须指定,而且必须指定初始值
// 常量只可以绑定到常量表达式,无法绑定到函数调用的结果或只有运行时才能确定的值
const Z_CONST:i32 = 100;
println!("z的值是{}", Z_CONST);
}
基本数据类型
// 数据类型
// rust是静态类型语言,编译时确定类型,所以变量必须声明类型
// rust有自动类型推断,但是类型推断只适用于let
fn main() {
// 标量类型,也就是基础数据类型,有四大类
// 整数类型
// 浮点类型
// 布尔类型
// 字符类型
// 整数类型
// 无符号类型
// u32占据32位空间,范围为0~2^32-1
let u32_var: u32 = 10;
// 有符号类型
// i32占据32位空间,范围为-2^31~2^31-1,i32是默认类型
let i32_var: i32 = 10;
// 整形范围,后面的数字表示它占的位数,size表示其位数随系统架构
// u8,u16,u32,u64,u128,usize
// i8,i16,i32,i64,i128,isize
// 整形字面量,数值里可以加下划线,方便阅读,rust特有
// 十进制
let decimal = 98_222;
// 十六进制
let hex = 0xff;
// 八进制
let oct = 0o77;
// 二进制
let binary = 0b1111_0000;
// 整数溢出
// 在开发中,如果遇到整数溢出,rust会报错,不会自动溢出
// 在发布版中,rust会自动溢出,形成整数环流,参考时钟
// 浮点类型
// f32,f64,f64是默认类型
let f32_var: f32 = 10.0;
let f64_var: f64 = 10.0;
// 数值计算
let sum = 5 + 10;
let difference = 95.5 - 4.3;
let product = 4 * 30;
let quotient = 56.7 / 32.2;
let remainder = 43 % 5;
// 布尔类型
let t = true;
let f: bool = false;
// 字符类型用单引号表示,占用四个字节,包括ASCII和Unicode,空白字符,emoji表情
let c = 'z';
let z: char = 'ℤ';
}
复合数据类型
fn main() {
// 复合数据类型,也就是引用数据类型
// 可以将多个值组合成一个复合值
// 包括元组,数组,结构体,枚举,向量,哈希映射
// 元组tuple
// 元组中的元素可以是不同的类型
// tuple的长度是固定的,一旦声明,就无法修改
let tup: (i32, f64, u8) = (500, 6.4, 1);
// 对元组结构赋值,就把(),理解成js的{}就好
let (x, y, z) = tup;
println!("x,y,z is: {} {} {}", x, y, z);
// 访问tuple的元素
let five_hundred = tup.0;
let six_point_four = tup.1;
let one = tup.2;
println!(
"five_hundred, six_point_four, one is: {} {} {}",
five_hundred, six_point_four, one
);
}
// 数组,和tuple类似,但是数组中的元素类型必须相同
// 数组的长度也是固定的,类型声明时,第一个是数组元素类型,第二个是数组长度
// 数组的声明,有两种
let a: [i32; 5] = [1, 2, 3, 4, 5];
// 如果数组中的元素类型相同,可以省略类型声明,类似js的full
let b = [3; 5];
// 访问数组的元素
let first = a[0];
let second = a[1];
println!("first, second is: {} {}", first, second);
// 如果索引超出,编译时是不会报错的,但是运行时会报错
// 后面还会学一个可变长度的数组,叫做向量vector,它的长度是可变的,更灵活
// 如果不确定使用哪个,建议使用向量vector
函数
// 函数
// main 函数是 Rust 程序的入口点
fn main() {
// 函数是以关键字 fn 开头,后跟函数名和一对圆括号
// 函数体被一对花括号包围
// 命名规范是小写,用下划线分割
// 函数的参数
// parameter 形参
// arguments 实参
// 函数参数必须注明类型
fn another_function(name: String, age: i32) {
println!("你的名字是:{}, 年龄是:{}", name, age);
}
// 调用函数
another_function(String::from("张三"), 18);
// 语句是来执行的
// 表达式是有返回值的,哪怕是返回空的
// Rust 是一门注重表达式的语言、
// 函数默认就是表达式,函数体中最后一个表达式会作为函数的返回值
// 语句不能赋值给变量,表达式可以,想想也能明白,都没有返回值哪来的赋值
let x = {
let y = 5;
y + 1
};
println!("x的值是:{}", x);
//会返回6
// 这段代码相当于js的立即执行函数和不写return的箭头函数结合,注意y+1表示返回时,后面不能有分号的
// 函数的返回值,把rust的函数当js的不写return的箭头函数理解最好
// 函数返回值使用 -> 符号后跟返回值的类型
// 函数体中最后一个表达式会作为函数的返回值
// 如果函数体中最后一个表达式没有分号,那么它的值就是函数的返回值
// 如果函数体中最后一个表达式有分号,那么函数的返回值就是 ()
// 如果想提前返回就使用return关键字
fn add(x: i32, y: i32) -> i32 {
x + y
}
println!("add函数的返回值是:{}", add(1, 2));
}
流程控制
// 控制流,流程语句
fn main() {
if_code();
match_code();
if_condition();
loop_code();
while_code();
for_code();
range_code();
break_code();
continue_code()
}
fn if_code() {
// if表达式,和js的区别在于,它不用给判断条件加小括号
let number = 6;
if number % 3 == 0 {
println!("{} 是3的倍数", number);
} else if number % 2 == 0 {
println!("{} 是2的倍数", number);
} else {
println!("{} 既不是3的倍数也不是2的倍数", number);
}
}
fn match_code() {
// match表达式,类似于js中的switch
let number = 4;
match number {
1 => println!("One!"),
2 => println!("Two!"),
3 => println!("Three!"),
4 => println!("Four!"),
5 => println!("Five!"),
_ => println!("Something else!"),
}
}
fn if_condition() {
// if条件,rust的if是一个表达式,可以赋值给别人,类似于js中的三元运算符
let age = 20;
let number = if age < 18 {
1
} else if age < 24 {
2
} else {
3
};
match number {
1 => println!("未成年!"),
2 => println!("青年!"),
_ => println!("成年!"),
}
}
// 循环
fn loop_code() {
// loop循环,类似于js中的while(true)
let mut counter = 0;
loop {
println!("loop!{}", counter);
counter += 1;
if counter == 5 {
break;
}
}
}
fn while_code() {
// while循环,类似于js中的while
let mut number = 3;
while number != 0 {
println!("while{}!", number);
number -= 1;
}
}
fn for_code() {
// for循环,类似于js中的for
let a = [10, 20, 30, 40, 50];
for element in a.iter() {
println!("for {}!", element);
}
}
// range循环,类似于js中的for(let i=0;i<10;i++)
fn range_code() {
for number in (1..4).rev() {
// 1<=number<4
println!("range {}!", number);
}
}
// break和continue,类似于js中的break和continue
fn break_code() {
let a = [1, 2, 3, 4, 5];
for element in a.iter() {
if element == &3 {
break;
// 表示运行到这结束123
}
println!("break {}!", element);
}
}
fn continue_code() {
let a = [1, 2, 3, 4, 5];
for element in a.iter() {
if element == &3 {
continue;
// 表示忽略这次循环1245
}
println!("continue {}!", element);
}
}
所有权
// 所有权
// 内存管理的三种方式
// 手动释放 如:从 c,c++
// GC释放 如:java,js
// Rust的所有权系统通过所有权规则来防止内存泄漏
// stsck和 heap
// stack: 存储编译时已知大小的变量,比如i32,f64,char,bool,以及所有其他基本数据类型
// stack的变量在离开作用域时会被自动丢弃,
// stack不用分配内存,因为大小已知,挨个放就好
// stack压栈,后进先出,每次找最上面的值就好,就像弹夹,每次出去都是最上面的,每次都找所以速度很快
// heap: 存储在编译时大小未知或大小可能变化的数据,比如:String,Vec<T>,
// heap的变量需要手动释放,否则会造成内存泄漏,也就是说只有堆上的数据需要手动释放
// heap是在使用时找到足够大的空间,标记再用,并返回地址值来引用
// heap是要分配地址和寻址的,所以速度慢,但是可以存储大小未知或大小可能变化的数据
// 1. Rust 中的每一个值都有一个被称为其所有者(owner)的变量
// 2. 值在任一时刻只能有一个所有者
// 3. 当所有者(变量)离开作用域,值会被丢弃
// 4. 这是几乎是零开销,不会影响编译时的性能
// 作用域的例子(也就是常说的生命周期)
fn main() {
string_code();
string_heap();
}
// 简单通用的例子
fn string_code() {
// s不可用,还没声明,也就是还没出生
let s = "hello";
// s 声明和赋值,进入作用域
println!("{}", s);
// s 在这里可用,可以进行各种操作
}
// // s 离开作用域,被丢弃,内存被释放,s不可用,也就是死亡了
// 复杂点的例子
fn string_heap() {
// 基础数据类型是存在栈上面,栈弹出,这数据就释放了
// String是放在堆上面的引用数据类型,就像是字符数组的样式
// 字符串分字符串字面值和字符串对象
// 字符串字面值是不可变的,就是程序里直接写出来的字符串
// 字符串对象是可变的,是通过String::from()方法创建的,也就是堆上的数据
let mut s = String::from("hello"); // s 在这里可用,可以进行各种操作
s.push_str(" world"); // s 在这里使用push,在原字符串后面添加
println!("{}", s); // 输出
}
// 内存分配
// 字符串字面值是存储在栈上的,因为大小已知,所以可以直接分配,所以也不能乱变,就像是排队,你不能突然让你朋友站你后面插队
// 字符串对象是个变量,因为大小未知,是存储在堆上的操作,就像去饭店吃饭,多个人就在空位再找一个位子就好
// 变量和数据的交互方式就是Move(移动)
// 当变量离开作用域,Rust会自动调用drop函数,释放内存,就像离开饭店,你吃完饭,服务员会自动帮你收拾桌子
fn code() {
let a = 5;
let b = a;
// a 在这里仍然可用,因为栈中 a 被拷贝给了栈中 b,a 并没有被“移动”或“丢弃”
// 对于基本数据类型(如整数、浮点数、布尔值等),Rust 编译器会进行值拷贝。
// 这意味着当你将 a 的值赋给 b 时,b 会获得 a 值的完全独立的副本。因此,a 和 b 在内存中各自存储了相同的值,但它们并不共享这个值的内存位置。
let s1 = String::from("hello");
let s2 = s1;
// s1 在这里不再可用,因为 s1 的值被“移动”给了 s2
// print!("{} {}", s1, s2); 编译器会报错,因为 s1 在这里已经被丢弃
// 一个字符串由三个部分组成,一个指向堆地址的指针,一个长度,一个容量,当 s1 被移动给 s2 时,s1 的指针、长度和容量都被移动给了 s2,s1 就被丢弃了,所以 s1 不再可用
// 为啥要用移动,而不是引用
// js举例
// let obj1 = {a: 1, b: 2};
// let obj2 = obj;
// 为了举例的假设情况下。要是释放内存,会先释放obj2,再释放obj1,因为obj2是obj1的引用
// 但是obj2释放了,obj1堆地址的指针还在指向这段位置,如果这时候这里被写入了值,就会二次释放造成野指针问题
// 释放obj1,就会把别的数据给释放了
// 所以rust直接就让 obj1不可用,这样这堆的地址值只会被一个变量所拥有,就不会造成野指针问题
// 类似js
// let obj1 = {a: 1, b: 2};
// let obj2 = obj;
// let obj1=null;
// 这样obj1就提前释放了,上面也就没有野指针了
// 上面说的Move,会让人们想到浅拷贝(shallow copy),深拷贝(deep copy)
// 浅拷贝就像是一个房子,夫妻两有两个不一样的钥匙,但是都是用一个房子里的东西
// 深拷贝就像是两个一摸一样的房子,夫妻两有两个不一样的钥匙,各住各的
// 所以浅拷贝和深拷贝的区别就在于,浅拷贝是共享内存,深拷贝是独立内存
// 但是现在还是房子,夫妻就剩一把钥匙,谁用给谁,另一个人就进不了门,这就是rust的Move
// rust其实也是有clone的
let s3 =s2.clone();
// 在这时候就类似js的深拷贝了,深拷贝就像是两个一摸一样的房子,夫妻两有两个不一样的钥匙,各住各的
// 非常消耗性能的,毕竟要购买一个新的一摸一样的房子
}
// Copy trait
// 基本数据类型和能深度确定大小的引用数据类型都是Copy trait的
// 整型,布尔,数值,字符
// 基础数据类型组成的引用数据类型,如
// 纯数组,上面四个基础类型组成的元组
// 不能Copy trait的,典型的包含String对象类型的
函数和所有权
fn main (){
// 所有权和函数
// 在语义上把值传递给函数和把值给变量是相似的,因为这两者都转移了所有权。
// 把值传给函数要不发生移动,要不发生复制
let s1 = String::from("hello");
take_ownership(s1); // s1 被移动到函数里
// println!("{}", s1); //s1 在这里不再有效,它手里的值已经给了函数,就理解成它的房间钥匙已经给了函数的形参
let x = 5; // x 进入作用域
make_copy(x); // x是一个基本值,所以是被复制到函数里
println!("{}", x); // x 在这里依然有效
}
fn take_ownership(s: String) { // s 进入作用域
println!("{}", s);
}
fn make_copy(x: i32) { // x 进入作用域
println!("{}", x);
}
参数的返回和数据的引用
fn main() {
code1();
code2();
code3();
code4();
code5();
code6();
let mut s = String::from("hello world");
let world_index = code8(&mut s);
println!("{}", world_index);
}
fn code1() {
let _s1 = gives_ownership();
// 无参函数的返回
// 调用 gives_ownership 函数后,some_string的值的所有权被转移给了s1,所以s1拥有字符串 "hello" 的所有权
fn gives_ownership() -> String {
let some_string = String::from("hello");
some_string
}
}
fn code2() {
let s2 = String::from("hello");
let _s3 = takes_and_gives_back(s2);
// 有参函数的返回
// takes_and_gives_back函数接收一个String类型的参数,并返回一个String类型,即s2将值的所有权转移给了函数的形参,函数再返回给s3
fn takes_and_gives_back(a_string: String) -> String {
a_string
}
}
fn code3() {
let s4 = String::from("返回所有权");
let (s4, len) = use_value_in_function(s4);
println!("s4 = {}, len = {}", s4, len);
// 函数使用变量,并返回所有权
fn use_value_in_function(some_string: String) -> (String, usize) {
let lenght: usize = some_string.len();
// 原理就是把值再返回回去,有一个同名的变量再接受
(some_string, lenght)
}
}
// 变量的引用
// Rust中的引用允许你使用值而不获取其所有权,Rust中的引用有三种:不可变引用、可变引用、悬垂引用
// 不可变引用:引用一个值,但不允许修改它
// 可变引用:引用一个值,并允许修改它
// 悬垂引用:一个已经被释放的变量的引用,也就是野指针,大bug!
// 还是夫妻住房问题,还是只有一把钥匙,现在两个都想能进门,但是妻子没钥匙,所以妻子直接喊丈夫回来开门,这就是引用
// 所以这样妻子就能进入房间,但是它不拥有这把钥匙,也就是说,哪怕妻子离婚出走,丈夫都能打开门,这就是引用
// 引用者离开作用域时,只会清除本身的引用,不会清除引用的值
fn code4() {
let s5 = String::from("借用的字符串");
let len = use_value_in_function1(&s5);
println!("s5={},len = {}", s5, len);
// 引用就在前加上一个&符号,例如let s1: &String = &s2;,这样s1就引用了s2
// 借用就是函数参数对变量的引用,两个几乎是一个东西,使用了&符号,其实就很贴近平时的编程习惯了
fn use_value_in_function1(some_string: &String) -> usize {
some_string.len()
}
}
// 上面是不可变引用,下面是可变引用
fn code5() {
let mut s6 = String::from("可变引用");
use_value_in_function2(&mut s6);
println!("s6 = {}", s6);
// 可变引用:引用一个值,并允许修改它
// 其实和可变变量一个道理,一个房子不让装修,你就不能动。如果让装修,你就可以动
fn use_value_in_function2(some_string: &mut String) {
some_string.push_str(", world");
}
}
// 为了避免数据竞争,Rust在编译时对可变引用进行了严格的限制,即在同一作用域内,只能有一个可变引用,或者多个不可变引用
fn code6() {
let mut s7 = String::from("可变引用");
let _r1 = &mut s7;
let _r2 = &mut s7;
// println!("r1 = {}, r2 = {}", r1, r2);
// 不可同时存在,否则报错,如果只是声明是没事的,只要一进行读写操作就会报错,就像上面这个打印,就会报错
// 就好比房子请施工队装修,只能找一个施工队负责,不然两队各有各的风格和进度,就会一团糟
}
// 一定要注意是同一个作用域不可以!!!!!不同作用域就可以,因为不同作用域互不影响
fn code7() {
let mut s8 = String::from("可变引用");
{
let r1 = &mut s8;
// 不能直接打印r1,但我们可以打印它所引用的字符串
println!("r1引用的字符串 = {}", *r1);
// 或者如果你只是想看看是否能进入这个作用域而不做任何操作,上面的println就足够了
}
// r1的作用域已结束,现在我们可以安全地再次借用s8
let r2 = &mut s8;
// 同样地,我们不能直接打印r2,但可以打印它所引用的内容
println!("r2引用的字符串 = {}", *r2);
}
切片和字符串的切片的应用
fn main() {
let mut s = String::from("hello world");
let hello=&s[0..5];
// 表示0<=index<5的切片,其实像这种0开始的,可以省略前面的0
// let hello=&s[..5];与上面一样的效果
let world = &s[6..11];
// 如果是到最后其实就是字符串的长度,可以写成
// let world = &s[6..s.len()];
// let world = &s[6..];
// 由上可得,如果我想要全部长度的字符串
// let world = &s[0..s.len()];
// let world = &s[..];
let world_index = code1(&mut s);
println!("{}", world_index);
let world_index_2= code2(&s);
// s.clear();
println!("{}", world_index_2);
}
// 切片
// 切片是对集合中一部分的引用,而不是整个集合的引用,切片是引用,所以它没有所有权
// 没有使用切片,返回的索引和原数据没有任何关联,哪怕字符串已经被清空,返回的数据依旧是在
// 就导致期间如果字符串发生变动,再利用返回的索引就会去查,就会出错
fn code1(s: &String) -> usize {
let bytes = s.as_bytes();
for (i, &item) in bytes.iter().enumerate() {
if item == b' ' {
return i;
}
}
s.len()
}
// 下面就不返回字符串的索引,而是返回一个切片
// 这样返回的是对string的切片的引用,所以在切片作用域内,原字符串不能被清空和改变
fn code2(s: &String) -> &str {
let bytes = s.as_bytes();
for (i, &item) in bytes.iter().enumerate() {
if item == b' ' {
return &s[0..i];
}
}
&s[..]
}
// 其实切片最经常看到的例子就是字符串字面值
// 字符串字面量的类型就是&str,它就是一种切片的表现,它指向了程序可执行文件中的某个位置
// 因为在切片引用生命周期结束之前,源数据不能改变
// 所以字符串字面值是不可变的
// 之前的函数中的字符串参数都是用的&String,其实也可以用&str,这样就可以即接收&String,也可以接收&str
// fn code3(s: &str) -> &str { s },如果参数是&str,那么函数的参数就只能是字符串字面值了,如果这时候传入&String,那就是会对其进行切片
// 所以&str是&String的特化,&String是&str的泛化;
struct
fn main() {
struct Person {
name: String,
age: u8,
is_male: bool,
}
// 结构体
// 就是类似一个ts的type,java的抽象类,多种数据的集合包
// 其中一开始就只有c的结构体,c++就是将其抽象成了类
// 正常情况下,结构体定义几个属性,那创建实例就要有几个属性
// 读实例的属性,就是一个person.name这样的小点
// rust的结构体有个特点就是如果其中一个属性是可变的,那所有的都是可变的,如果都不变,那都是不可变的
let person = Person {
name: String::from("张三"),
age: 18,
is_male: true,
};
let sex = if person.is_male { "男" } else { "女" };
println!(
"这个人名字叫{} , 年龄是{} , 性别是{}",
person.name, person.age, sex
);
// 和js一样,属性名如果和值是一样的,那就可以简写
// 可以作为参数传递给函数,也可以作为返回值
// 结构体的更新,先高呼js牛逼
let _person1 = Person {
name: String::from("李四"),
age: person.age,
is_male: person.is_male,
};
// 更新语法,其实就是js展开符
let _person2 = Person {
name: String::from("李四"),
..person
};
// tuple struct
// 结构体和元组类似,但是元组没有名字,结构体有名字
// 结构体可以包含多个不同类型的值,元组只能包含相同类型的值,一个类似对象,一个类似数组
// 结构体和元组都可以作为函数的参数和返回值
// 但是rust的struct可以和tuple结合,变成一个有名字的数组
// 这里的名字是指定数组中每个元素的名称,对象是键值对,有键有值;数组只有值
struct Color(i32, i32, i32);
println!(
"{} {} {}",
Color(255, 0, 0).0,
Color(255, 0, 0).1,
Color(255, 0, 0).2
);
let _color = Color(255, 0, 0);
// 空结构
// 当我们需要定义一个结构体,但是暂时不需要任何字段时,可以使用空结构体
struct Empty;
let _empty = Empty {};
// struct数据的所有权,如Person,name是String,所以name的所有权在person中,其他属性又是基本数据类型
// 所以该结构体的实例拥有其所有属性的所有权
// 只要struct实例还在,那么其所有字段的所有权也会一直存在
// struct里面也可以放引用数据类型,但是在rust的这门语言中,当然也要考虑该结构体实例引用的引用数据的声明周期
// 一个结构体来表示长方形,来计算长方形的面积
#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}
let rectangle = Rectangle {
width: 30,
height: 50,
};
fn rectangle_area(rectangle: &Rectangle) -> u32 {
rectangle.width * rectangle.height
}
println!("长方形面积:{}", rectangle_area(&rectangle));
println!("长方形面积的宽:{}", rectangle.width);
// 如果我们要看一个完整的结构体,并不像js那么容易
// 只可以使用debug格式打印,在结构体上面加#[derive(Debug)],其实就是ts的注解
// 不能使用{}号了,要使用{:?},表示结构未知。如果想要更好地格式展示数据,可以用{:#?}
println!("长方形数据:{:?}", rectangle);
}
struct方法
// 函数和方法的区别
// 函数:定义在模块或包中,可以访问模块或包中的变量和函数
// 方法:定义在结构体或枚举中,可以访问结构体或枚举中的变量和函数
fn main() {
struct Person {
name: String,
age: u8,
is_male: bool,
}
// 方法,和别的语言不同的就是,结构体里面只能存属性,不能存方法
// 要想存方法要用impl(扩展)关键字,第一个参数一定是&self,这个就是js的this
impl Person {
fn get_age(&self) -> u8 {
self.age
}
}
let p = Person {
name: "zhangsan".to_string(),
age: 18,
is_male: true,
};
println!("age:{}", p.get_age());
// 关联函数,就是静态方法
// 使用是通过::来调用,他是不用要实例化就可以调用的
// 下面通过静态方法,创建一个构造函数
impl Person {
fn new(name: String, age: u8, is_male: bool) -> Self {
Person { name, age, is_male }
}
}
let p = Person::new("zhangsan".to_string(), 18, true);
println!("age:{}", p.get_age());
}
枚举和模式匹配
代码有点问题,有几个警告没能解决,等以后理解透了,再来看看
// 枚举
fn main() {
main2();
main3();
main4();
main5()
}
fn main2() {
// 枚举的简单使用
// 声明枚举类型,类似类的静态属性
enum IPAddrKind {
V4,
V6,
}
// 定义枚举变量
let four = IPAddrKind::V4;
let six = IPAddrKind::V6;
// 匹配枚举类型
fn route(ip_type: IPAddrKind) {
match ip_type {
IPAddrKind::V4 => println!("ip_type: IPv4"),
IPAddrKind::V6 => println!("ip_type: IPv6"),
}
}
route(four);
route(six);
}
fn main3() {
#[derive(Debug)]
enum IPAddrKind {
V4,
V6,
}
// 枚举作为结构体的字段使用,字段就是只有声明,没有定义的的属性
#[derive(Debug)]
struct IPAddr {
kind: IPAddrKind,
address: String,
}
let home: IPAddr = IPAddr {
kind: IPAddrKind::V4,
address: String::from("127.0.0.1"),
};
let loopback: IPAddr = IPAddr {
kind: IPAddrKind::V6,
address: String::from("::1"),
};
println!("Home: {:?}", home);
println!("Loopback: {:?}", loopback);
}
fn main4() {
// 每个枚举可以有不同的类型
#[derive(Debug)]
enum IpAddr {
V4(u8, u8, u8, u8),
V6(String),
}
// 注意:IPv4 地址的每个部分都需要是 u8 类型的字面量或变量
let home = IpAddr::V4(127, 0, 0, 1);
let loopback = IpAddr::V6(String::from("::1"));
// 打印这两个 IP 地址
println!("Home: {:?}", home);
println!("Loopback: {:?}", loopback);
}
fn main5() {
enum Message {
Quit,
Move { x: i32, y: i32 }, //Move是个匿名的结构体,包含x和y两个属性
Write(String),
ChangeColor(i32, i32, i32),
}
impl Message {
fn call(&self) {
// 在这里实现方法体
}
}
let q = Message::Quit;
let m= Message::Move { x: 10, y: 20 };
let w = Message::Write(String::from("hello"));
let c = Message::ChangeColor(0, 160, 255);
m.call();
}
option枚举
// 枚举
fn main() {
// option枚举定义在标准库中
// 在prelude(预导入模块)中,所以可以直接使用
// Option枚举有Some和None两个变体,表示一个值可能存在或者不存在
// 在其他语言中,这种类型通常被称为“可能为空”或者“可能为null”的类型
// 因为null值并不安全,就好比一个对象,假设它的值为null,那么调用它的方法就会报错
// 所以Rust语言没有null值,但是提供了Option枚举来处理可能为空的情况,表示一个因为为某种原因无效或缺失的值
// 本质上就是一个枚举,有两个变体,Some和None
// enum Option<T> {
// Some(T),
// None,
// }
main2();
}
fn main2() {
// Option枚举可以用于任何类型,包括结构体、元组、枚举等
// Option枚举有两个状态,Some和None,分别表示一个值存在或者不存在
let some_number = Some(5);
let some_string = Some("a string");
let absent_number: Option<i32> = None;
// 这里的absent_number是一个Option<i32>类型的变量,它的值是None,表示这个变量没有值
// 这就相当于一个空值,相当于ts的<number|null>
// 但是它比null更安全,因为Option枚举可以明确地表示一个值可能存在或者不存在
println!("some_number: {:?}", some_number);
println!("some_string: {:?}", some_string);
println!("absent_number: {:?}", absent_number)
}
枚举和匹配
// 枚举
fn main() {
// 模式匹配
let coin = Coin::Penny;
value_in_cents(coin);
// 枚举的值中还能传入参数
let coin2 = Coin::Quarter(UsState::Alaska);
value_in_cents(coin2);
// 匹配可能为空的值
let five = Some(5);
let six = plus_one(five);
let none = plus_one(None);
// 只匹配某些值,剩余的值用 _ 来匹配
let some_u8_value = 0u8;
match_numbers(some_u8_value);
}
#[derive(Debug)]
enum UsState {
Alabama,
Alaska,
}
// 枚举的值中还能传入参数
enum Coin {
Penny,
Nickel,
Dime,
Quarter(UsState),
}
fn value_in_cents(coin: Coin) {
match coin {
Coin::Penny => println!("1 cent"),
Coin::Nickel => println!("5 cent"),
Coin::Dime => println!("10 cent"),
Coin::Quarter (state) => {
println!("25 cent from {:?}", state)
}
}
}
fn plus_one(x: Option<i32>) -> Option<i32> {
match x {
// 就是说如果x是Some,那么就返回Some(i + 1),如果是null,就返回null
// match必须覆盖所有可能的值,否则编译器会报错
Some(i) => {
println!("Some(i) = {:?}", i);
Some(i + 1)}
None => {
println!("None");
None
},
}
}
fn match_numbers(x:u8) {
match x {
1 => println!("one"),
3 => println!("three"),
5 => println!("five"),
7 => println!("seven"),
_ => println!("other"),
}
}
组织结构
fn main() {
// rust组织结构
// 1.顶层包 Package 也就是js的中常说的项目,就好比一个一个学校
// 2.以体系树区分的单元 Crate ,也就是js中的包,就好比学校里的各个学院
// 3.单元中以功能模块 Module ,也就是js的模块,就好比学院里的各个专业
// Crate有两种类型:二进制binary crate和库library crate
// 一个Package里有:
// Cargo.toml文件:定义了包的元数据,比如包的名称、版本、作者、依赖项等,以及如何构建和打包包的指令,描述了如何构建这个包
// Crate Root: 一个crate的根模块,它定义了crate的公有API
// rust编译器开始编译的入口,也就是main.rs
// 只能包含零个或一个library crate,因为作为库开发时,lib.rs文件是library crate是包的入口,且只能有一个
// 能包含多个binary crate,因为binary crate是可执行文件,是是包的依赖包,在src/bin下面
// module对代码进行分组,控制作用域,方便代码的复用,增加可读性,隐藏实现细节,提高安全性。关键字:mod
// 创建了模块,如果想使用,就要通过路径来访问,路径分为绝对路径和相对路径
// 绝对路径从crate root开始,相对路径从当前模块开始
// 相对路径从当前模块开始,使用关键字self、super来相对定位
}
路径
mod front_of_house {
pub mod hosting {
pub fn add_to_waitlist() {}
pub fn seat_at_table() {}
}
pub mod serving {
pub fn take_order() {}
// 如果这个函数前面没有 pub,那么它就是私有的private,只能在当前模块中使用
// 私有只能在当前模块中使用,父模块无法访问,但是子模块可以访问
// 如果两个模块都在一个文件的根级上,那么这两个模块是兄弟模块,兄弟模块之间可以互相访问
// 就像front_of_house和eat_at_restaurant,front_of_house虽然是私有的,但是eat_at_restaurant可以访问
fn serve_order() {
// super关键字,表示父级,::表示子级
super::hosting::add_to_waitlist()
}
}
}
pub fn eat_at_restaurant() {
// 绝对路径
crate::front_of_house::hosting::add_to_waitlist();
// 相对路径
front_of_house::hosting::seat_at_table();
}
模块
mod front_of_house {
pub mod hosting {
pub fn add_to_waitlist() {}
pub fn seat_at_table() {}
pub fn serve_order() {}
}
}
// 通过use关键字来导入模块,
use crate::front_of_house::hosting;
// 使用as关键字来给导入的模块起别名
use front_of_house::hosting::seat_at_table as seat;
// 嵌套导入
use std::{
cmp::Ordering,
io::{
self,//就表示导入std::io自身
Write,
}
};
// 全部引入,但是不推荐使用,一般在测试时使用
use std::collections::*;
pub fn eat_at_restaurant() {
// 绝对路径
crate::front_of_house::hosting::add_to_waitlist();
// 相对路径
front_of_house::hosting::seat_at_table();
// 使用use关键字来导入模块
hosting::add_to_waitlist();
seat()
}
// 将模块放在文件中
// main.rs
mod front_of_house;
// front_of_house.rs
pub mod hosting {
pub fn add_to_waitlist() {}
pub fn seat_at_table() {}
pub fn serve_order() {}
}
对比js的模块系统
//js的模块
module.exports = {
add_to_waitlist: function() {}
}
//rust的模块
pub mod hosting {
pub fn add_to_waitlist() {}
}
// 在js中,使用import导入模块
import { add_to_waitlist } from './hosting.js'
// 在rust中,使用use导入模块
use crate::hosting::add_to_waitlist;
// js的暴露引用
export waitlist from './hosting.js'
// rust的暴露引用
pub use crate::front_of_house::hosting;
Vector集合
fn main() {
// 集合之Vector
// 用new创建一个空的Vector
let _v: Vec<i32> = Vec::new();
// 用vec!宏创建一个包含初始值的Vector
let mut v1 = vec![1, 2, 3];
// 向Vector中添加元素
v1.push(1);
// 删除Vector中的元素
v1.remove(0);
// 访问Vector中的元素的方法
let third = &v1[2];
println!("The third element is {}", third);
match v1.get(2) {
Some(third) => println!("The third element is {}", third),
None => println!("There is no third element."),
}
// 遍历Vector中的元素
for i in &mut v1 {
*i += 50;
// 修改Vector中的元素
}
for i in v1 {
println!("{}", i);
}
}
fn main2() {
// Vector是可变的,所以需要mut
let mut v1 = vec![1, 2, 3];
// 有定义了不可变的Vector切片
let first = &v1[0];
// 修改Vector中的元素,会报错,因为first是Vector的不可变引用
v1.push(4);
println!("The first element is: {}", first);
}
fn main3() {
// vector和emun的结合就能使用枚举来存储多种类型的数据
enum SpreadsheetCell {
Int(i32),
Float(f64),
Text(String),
}
let row = vec![
SpreadsheetCell::Int(3),
SpreadsheetCell::Text(String::from("blue")),
SpreadsheetCell::Float(10.12),
];
}
String
fn main() {
// 字符串
// rust中字符串是UTF-8编码的,只有一个字符串类型,切片str或&str
// 字符串字面量,是存储在二进制文件中,字符串字面量是不可变的
// 而String类型,则是来自于标准库的,是可变的,可增长,可修改的,可拥有的,也是UTF-8编码的
// 在其他库中,有着更多的字符串类型,比如:OsString,OsStr,PathBuf,Path等
// String本质上是bytes的集合,所以集合Vec<T>的很多方法都是可以用的,String可以看做是Vec<u8>的封装
// 定义
let mut _s = String::new();
// 创建的两种方法
// 字面量创建,from函数创建
let mut s1 = String::from("hello");
// to_string方法
let data = "world";
let s2 = data.to_string();
let s3 = "hello".to_string();
// 字符串拼接
s1.push_str("World");
println!("{}", s1);
// s1.push('!'); // push方法只能接受一个字符
s1.push_str("!");
println!("{}", s1);
// 加号拼接,前面是String类型,后面是&str类型,后面是一个字符串的引用
// 使用加号拼接,会创建一个新的String,并复制数据,所以性能较低。所以s1的值会被丢弃
// 类似fn add(self, s: &str) -> String
let s4 = s1 + &s2;
// format!宏,性能比加号拼接好,因为format!宏会创建一个String,并直接将数据写入,不会丢弃s1
// format!宏的用法和println!宏类似,不过它不会打印到控制台,而是返回一个String,类似模板字符串
// 它不会获得所用参数的所有权,所以s3和s2的值不会被丢弃,后面还能继续使用
let s5 = format!("{}-{}", s3, s2);
// String不能直接使用索引访问,因为字符串是UTF-8编码的,一个字符可能占用多个字节
// 中文一个字符占用3个字节,英文一个字符占用1个字节
let len = s2.len();
println!("len: {}", len);
// rust看待字符:字节,标量值,字形簇(字形)
// 字节,每个字节是个二进制的数值
for b in s4.bytes() {
println!("{}", b);
}
// 标量,二进制数值表示的字符
for c in s4.chars() {
println!("{}", c);
}
// 字形簇,标准库没这方法
// 字符串的切片,如果一个字母占用多个字节,切片恰好切在它的中间,那就会在编译时报错
let s6=&s5[0..5];
println!("{}", s6);
}
HashMap
use std::collections::HashMap;
fn main() {
// HashMap<K,V>:键值对集合,通过键来查找数据,而不是索引
// 创建一个空的HashMap,空的HashMap会报错,因为HashMap是空的,所以无法确定它的类型
// 解决办法是使用类型标注,或者添加数据,这样HashMap就会知道它的类型
let mut scores:HashMap<String, i32> = HashMap::new();
scores.insert(String::from("Blue"), 10);
scores.insert(String::from("Yellow"), 50);
// HashMap用的比较少,所以他不在标准库中,而是放在了collections模块中
// HashMap的数据是保存在堆上的,所以可以存储任何类型的数据,但是是同构的,即所有的键和值必须是相同类型,相当于是变体的数组
// 另一个创建HashMap的方法collect,collect方法可以将迭代器中的元素收集到HashMap中
let teams = vec![String::from("Blue"), String::from("Yellow")];
let initial_scores = vec![10, 50];
let mut scores1:HashMap<_, _> = teams.iter().zip(initial_scores.iter()).collect();
// zip方法可以将两个迭代器组合成一个迭代器,每次迭代时返回一个元组,元组的第一个元素来自第一个迭代器,第二个元素来自第二个迭代器
// collect方法能返回很多种类型的数据结构,所以我们要指明想要的数据类型
// HashMap的所有权
// 对于实现了Copy trait的类型,它们会被复制到HashMap中,对于没有实现Copy trait的类型,如String,它们的所有权会被转移
let field_name = String::from("Favorite color");
let field_value = String::from("Blue");
let mut scores2 = HashMap::new();
scores2.insert(field_name, field_value);
// 如果把值的引用插入到HashMap中,值本身就不会移动
// 访问HashMap中的值
let team_name = String::from("Blue");
let score = scores.get(&team_name);
// get方法返回一个Option,因为可能不存在对应的键,所以需要使用unwrap方法来获取值,或者使用if let来处理Option
match score {
Some(s) => println!("The score is {}", s),
None => println!("Team not found"),
}
// 遍历HashMap,这里是对HashMap的引用,因为HashMap后面一般是要继续使用的
for (key, value) in &scores {
println!("{}: {}", key, value);
}
// 更新HashMap
// 1.覆盖一个值
scores.insert(String::from("Blue"), 100);
// 2.只插入一个值,如果键已经存在,则不插入,entry会检查键是否存在
scores.entry(String::from("Yellow")).or_insert(50);
// 3.根据旧值更新一个值
let text = "hello world wonderful world";
let mut map = HashMap::new();
for word in text.split_whitespace() {
let count = map.entry(word).or_insert(0);
*count += 1;
}
println!("{:?}", map);
// HashMap函数
// 默认情况下,HsahMap使用加密功能强大的Hash函数来哈希你的键,可以抵御服务Dos攻击,
// 它不是最快的,但是很安全的,你可以选择自己定义哈希函数
}
错误处理
use std::{
fs::File,
io::{ErrorKind, Read},
};
fn main() {
// 错误处理
// rust大部分错误是能在编译时处理的,比如类型错误,rust编译器会直接报错
// 但是有些错误只能在运行时才能发现,比如文件不存在,网络连接失败等,这些错误rust提供了Result和Option两种类型来处理
// 这种错误分两类,可恢复错误:如文件没找到,和不可恢复错误,如访问超出索引,可恢复错误使用Result类型,不可恢复错误使用panic!宏
// 在别的语言中,错误通常使用异常处理,但是rust中异常处理是不推荐的,因为异常处理会导致程序的控制流混乱,难以调试
// 应对panic,既不可恢复的错误
// Rust会展开调用栈,沿着栈往回走,释放资源,然后终止程序,如果想要在panic时打印错误信息
// 还可以立即终止程序,不进行展开调用栈,释放资源,直接终止程序,内存需要OS自行释放
// 如果想要文件体积更小,可以使用std::process::abort函数,不进行展开调用栈,不释放资源,直接终止程序,内存需要OS自行释放
// 终止错误
// 在Cargo.toml中添加panic = 'abort',就可以在panic时立即终止程序,不进行展开调用栈,不释放资源,直接终止程序,内存需要OS自行释放
// [dependencies]
// [profile.release]
// panic = 'abort'
// 命令行 set RUST_BACKTRACE=1 && cargo run 能够打印调用栈,显示出具体的错误位置及原因,等号两边不能有空格
// 对于可以恢复的错误,就像读取文件,如果没有文件,我们希望创建文件,而不是报错,这时候可以使用Result枚举类型
// Result枚举有两个变体,Ok和Err,Ok表示成功,Err表示失败,Err中可以包含具体的错误信息
// enum Result<T, E> {
// Ok(T),
// Err(E),
// }
// Result类型是泛型的,T表示成功时的返回值类型,E表示失败时的错误类型
// 使用Result类型时,需要使用match语句来处理,match语句可以匹配Result枚举的变体,然后分别处理成功和失败的情况
test3();
}
fn _test() {
// 相当于throw
// panic!("这里出错了");
let mut v = vec![1, 2, 3];
v[4]; // 这里会panic,因为索引越界
}
fn _test2() {
// let f=File::open("test.txt").expect("文件打开失败");
let f = File::open("test.txt");
let f = match f {
Ok(file) => {
println!("文件打开成功");
// 这里可以处理文件
file;
}
Err(e) => {
// 这里可以处理错误
panic!("文件打开失败,错误信息:{:?}", e);
}
};
}
// 判断错误类型,根据不同的错误类型进行不同的处理,感觉有点像是Promise
fn test3() {
let f = File::open("test.txt");
let f = match f {
Ok(file) => {
println!("文件打开成功");
// 这里可以处理文件
file;
}
Err(e) => match e.kind() {
ErrorKind::NotFound => match File::create("test.txt") {
Ok(file) => {
println!("文件创建成功");
file;
}
Err(e) => {
// 这里可以处理错误
panic!("文件创建失败,错误信息:{:?}", e);
}
},
other_error => {
// 这里可以处理其他错误
panic!("文件打开失败,错误信息:{:?}", other_error);
}
},
};
}
// unwrap_or_else和上面的作用差不多,不过就有点类似 then,它只管处理错误,不处理成功的情况,如果出错,就执行后面的,如果成功,就返回成功的结果
// 如果只是简单的抛错,那就使用 let f = File::open("test.txt").unwrap(),但是这样是没法指定抛错信息的
// 这个时候就可以使用 let f = File::open("test.txt").expect("文件打开失败"),这样就可以指定抛错信息了
fn test4() {
let f = File::open("test.txt").unwrap_or_else(|e| {
if e.kind() == ErrorKind::NotFound {
File::create("test.txt").unwrap_or_else(|e| {
panic!("文件创建失败,错误信息:{:?}", e);
})
} else {
panic!("文件打开失败,错误信息:{:?}", e);
}
});
// 这个时候就可以使用 let f = File::open("test.txt").expect("文件打开失败"),这样就可以指定抛错信息了
let f1 = File::open("test.txt").expect("文件打开失败");
}
// 错误传递:一段程序里有多种可能的报错,但是你只想在最后处理一次,这个时候就可以使用错误传递,将错误传递给下一个函数,直到最后处理
fn test5() -> Result<String, std::io::Error> {
let mut f = match File::open("test.txt") {
Ok(file) => file,
Err(e) => return Err(e),
};
let mut s = String::new();
match f.read_to_string(&mut s) {
Ok(_) => {
println!("文件内容:{}", s); // 可以在这里打印,但不影响函数返回值
Ok(s) // 返回读取成功的字符串
}
Err(e) => return Err(e),
}
}
// 等同于
// ?号和from函数
// Trait std::convert::From上的from函数,它可以把一个错误类型转换成另一个错误类型,用于错误间的转换
// 被?的错误,会自动调用from函数,把错误转换成另一个错误类型
// ?号运算符只能返回Result类型,如果返回的是其他类型,那么?号运算符会报错
fn test6() -> Result<String, std::io::Error> {
let mut f = File::open("test.txt")?;
let mut s = String::new();
f.read_to_string(&mut s)?;
Ok(s)
}
// Box<dyn Error>:Box<dyn Error>是一个动态分发的错误类型,它可以表示任何实现了Error trait的错误类型
// 这样就可以在main函数中处理任何类型的错误了
// fn main () ->Result<(), Box<dyn std::error::Error>> {
// let f = File::open("test.txt")?;
// Ok(())
// }
泛型
fn main() {
// 泛型就是类型的占位符
// 编译器会在编译时,根据传入的参数类型,来推导出具体的类型
// 泛型可以减少代码重复,增加代码的复用性
}
// 函数中使用泛型
// 在比较大小时,要注意,不是所有的类型都可以比较大小,所以需要限制泛型
// 暂时不修改错误,这下面是个错误的例子,只是为了演示泛型
// fn largest<T>(list: &[T]) -> &T {
// let mut largest = &list[0];
// for item in list {
// if item > largest {
// largest = item;
// }
// }
// largest
// }
// 结构体中使用泛型
fn test1() {
struct Point<T> {
x: T,
y: T,
}
// 将泛型放在结构体名后面,表示在类型T上实现方法
impl<T> Point<T> {
fn x(&self) -> &T {
&self.x
}
}
// 在指定类型上实现方法
impl Point<f32> {
fn distance_from_origin(&self) -> f32 {
(self.x.powi(2) + self.y.powi(2)).sqrt()
}
}
let integer = Point { x: 5, y: 10 };
struct PointTwo<T, U> {
x: T,
y: U,
}
let both_integer = PointTwo { x: 5, y: 1.0 };
}
// 枚举中使用泛型
fn test2() {
enum Option<T> {
Some(T),
None,
}
enum Result<T, E> {
Ok(T),
Err(E),
}
}
trait
use std::fmt::Display;
fn main() {
// trait
// trait的作用是定义一个接口,让不同的类型实现这个接口,从而实现多态
// 只有方法签名,没有具体实现
// 可以有好多个方法
// 实现trait的方法,必须实现trait中定义的所有方法,有点像接口,又有点像抽象类
pub trait Animal {
fn name(&self) -> &str;
fn age(&self) -> u32;
// 默认实现
fn name_call(&self) -> String {
format!("{} is a animal", self.name())
}
}
struct Dog {
name: String,
age: u32,
}
impl Animal for Dog {
fn name(&self) -> &str {
&self.name
}
fn age(&self) -> u32 {
self.age
}
}
let dog = Dog {
name: String::from("dog"),
age: 3,
};
println!("name: {}, age: {}", dog.name(), dog.age());
println!("sound: {}", dog.name_call());
}
fn main2() {
// 定义一个动物类型
pub trait Animal {
fn name(&self) -> &str;
fn age(&self) -> u32;
fn name_call(&self) -> String {
format!("{} is a animal", self.name())
}
}
// 定义一个狗结构
struct Dog {
name: String,
age: u32,
}
// 实现狗这动物类型的行为
impl Animal for Dog {
fn name(&self) -> &str {
&self.name
}
fn age(&self) -> u32 {
self.age
}
}
// 构建一个狗实例
let dog = Dog {
name: String::from("dog"),
age: 3,
};
// 定义一个猫结构
struct Cat {
name: String,
age: u32,
}
// 实现猫这动物类型的行为
impl Animal for Cat {
fn name(&self) -> &str {
&self.name
}
fn age(&self) -> u32 {
self.age
}
}
// 构建一个猫实例
let cat = Cat {
name: String::from("cat"),
age: 2,
};
// 介绍动物,一般的trait作为参数类型
pub fn introduce(animal: impl Animal) {
println!("sound: {}", animal.name_call());
}
// 作为尖括号泛型来使用
pub fn introduce1<T: Animal>(animal: T) {
println!("sound: {}", animal.name_call());
}
pub fn introduce2<T: Animal>(animal: T, animal2: T) {
println!("sound: {}", animal.name_call());
}
// trait表示"且"的连接
pub fn introduce3<T: Animal + Display, U: Clone + Display>(animal: T, animal2: U) {
println!("sound: {}", animal.name_call());
}
// 使用where子句来简化
pub fn introduce4<T, U>(animal: T, animal2: U)
where
T: Animal + Display,
U: Clone + Display
{
println!("sound: {}", animal.name_call());
}
// trait还能作为返回的类型限制
pub fn get_animal() -> impl Animal {
Cat {
name: String::from("cat"),
age: 2,
}
}
}
生命周期标记
use std::fmt::Display;
fn main() {
test1();
test2();
test3();
test4();
test5();
}
fn test1() {
{
let r;
// let b=r;这是错的,因为r还没有被初始化,所以不能被赋值给b
{
let x = 5;
// r = &x;这样也是错的,因为x的声明周期比r短,当x离开作用域时,r指向的内存会被释放,所以r指向的内存是无效的
r = x; //这样是对的,因为x的声明周期比r长,当x离开作用域时,r指向的内存不会被释放,所以r指向的内存是有效的
// 不难看出,在借用检查器的约束下,借用是不能超出其作用域的。但是直接等就等于值移动给r了,r就是有效的
}
println!("The value of r is: {}", r)
}
}
// 借用的生命周期
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() {
x
} else {
y
}
}
fn test2() {
let string1 = String::from("abcd");
let string2 = "xyz";
// 在 Rust 中,所有引用都必须遵守借用规则,这意味着引用的生命周期不能超出它们所引用的数据的生命周期
// 在当前的代码中,longest 函数返回了一个指向 x 或 y 的引用,但是我们不知道那个会返回被接着用,也就是不知道哪个生命周期更长
// 这可能会导致一个悬垂引用(dangling reference),即返回的引用指向了已经不再有效的内存。也就没办法返回
// 所以我们要用泛型来延长生命周期,所以用&'a str来表示,帮助编译器来计算生命周期
// 单个借用参数的生命周期标注没啥意义,标注是用来告诉编译器多个引用的生命周期参数之间的关系,意思返回的'a就是借用参数生命周期最短的那个,对比test2和3的例子
let result = longest(string1.as_str(), string2);
println!("The longest string is {}", result);
}
fn test3() {
// 这两个生命周期都是比较大的
let string1 = String::from("abcd");
let result;
{
// 这个生命周期比较小
let string2 = "xyz";
// 如果没有'a这个标记,那result可能会是string2的引用
// 因为string2的生命周期比较短,但是string2的生命周期已经结束了,所以result指向的内存已经无效了,所以需要标注生命周期
// 它取最小的生命周期,这样编译器就能给我们做检查了
// 就好比爸妈在房间里,你每天在门外喊一个人借钱,但是现在房间灯熄了,那就不要去敲门喊了,因为你可能在喊你爸,但是你爸睡着了,这时就会打你一顿。这个作为提醒的灯就是'a
// 那我不想
result = longest(string1.as_str(), string2);
}
println!("The longest string is {}", result);
}
// 为什么会出来'a,其他语言就没有呢?因为别的语言是大部分语言是GC,它会保留生命周期,语言后台帮你管理。
// 而C和C++是手动管理,所以容易空指针。Rust是手动管理内存,为了确定谁的生命周期更短,所以需要标注生命周期
// 结构体的生命周期标注
// 表示part字段的引用不能比整个ImportantExcerpt实例活得更长
// 可能part没有,ImportantExcerpt活着,但是ImportantExcerpt死了,port肯定也没了
// 生命周期标注能省略的三个场景
// 1.引用的每一个参数都有生命周期,那么就省略,因为它们的生命周期是明确的
// 2.如果只有一个输入生命周期,那么就省略,因为它的生命周期就那一个,只要盯着他一个就好,不怕找到已经"死"的 参数
// 3.如果函数没有输入生命周期,那么就省略,因为函数没有输入,所以它没有生命周期,他肯定会"死"的
// 一般来说,生命周期省略规则能覆盖大部分场景,所以你不需要标注生命周期,等到编译器报错的时候,再标注生命周期
// 结构体以及方法中的生命周期标注
struct ImportantExcerpt<'a> {
part: &'a str,
}
impl<'a> ImportantExcerpt<'a> {
// 只有一个引用,所以省略生命周期标注
fn level(&self) -> i32 {
3
}
// 有两个引用,所以需要标注生命周期,但是第一个引用是self,也就是说它的生命周期是明确的
fn announce_and_return_part(&self, announcement: &str) -> &str {
println!("Attention please: {}", announcement);
self.part
}
}
fn test4() {
let novel = String::from("Call me Ishmael. Some years ago...");
let first_sentence = novel.split('.').next().expect("Could not find a '.'");
let _i = ImportantExcerpt {
part: first_sentence,
};
}
// 静态生命周期
// 所有的字符串字面值都拥有'static生命周期,因为它们在编译时就被确定下来,并且保存在程序的二进制文件中,直到程序结束才被销毁
// 在指定”static“生命周期时,要注意他是否需要引用在整个生命周期存活
fn test5() {
let s: &'static str = "I have a static lifetime.";
}
// 最后的小例子
// 生命周期标记也是泛型的一种,不过他是来辅助确定引用类型生命周期的,一般泛型是来确定类型的
fn test6<'a, T>(x: &'a str, y: &'a str, ann: T) -> &'a str
where
T: Display,
{
println!("Announcement! {}", ann);
if x.len() > y.len() {
x
} else {
y
}
}
函数测试
pub fn add(left: usize, right: usize) -> usize {
left + right
}
// 测试函数需要test注解
// #[test]
// 加上了test注解,函数会自动被测试
// 测试函数使用cargo test运行,rust自动构建test Runer可执行文件,会自动运行所有被test注解的函数,并报告其运行是否成功、
// 当使用cargo构建Library时,会自动生成test Module,该模块有一个test函数
// 我们可以添加任意数量的test Module,每个模块中可以包含任意数量的test函数
// rusult 表示成功
// passed 表示测试通过
// failed 表示测试失败
// ignored 表示测试被忽略
// measured 表示测试被测量性能
// filtered 表示测试被过滤
// 测试函数一旦出现了panic,测试就会失败
// 所以,测试函数不能包含任何panic的代码
// 每个测试都是一个独立的线程,因此测试函数之间不能共享状态
// 测试函数不能有参数,也不能返回值
// 一旦某个线程挂掉,那个测试就算是失败的
// 所以在测试中一般使用断言assert宏,而不是panic宏
// assert宏会在断言失败时panic,但不会导致整个测试失败
// assert宏会返回一个布尔值,如果为false,则panic
#[derive(Debug)]
pub struct Rectangle {
length: u32,
width: u32,
}
impl Rectangle {
pub fn can_hold(&self, other: &Rectangle) -> bool {
self.length > other.length && self.width > other.width
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn larger_can_hold_smaller() {
// 判断一个矩形是否可以容纳另一个矩形
let larger = Rectangle {
length: 8,
width: 7,
};
let smaller = Rectangle {
length: 5,
width: 1,
};
assert!(larger.can_hold(&smaller));
}
}
// assert! 宏判断是否为真
// assert_eq! 宏判断是否相等
// assert_ne! 宏判断是否不相等
// 这两个断言失败会自动打印两个参数的值
// 使用Debug宏和PartialEq打印出参数的值(绝大多数的基本类型都实现了Debug和PartialEq)
pub fn add_two(a: i32) -> i32 {
a + 2
}
#[cfg(test)]
mod tests1 {
use super::*;
#[test]
fn it_adds_two() {
assert_eq!(4, add_two(2));
}
}
// 这三个断言是可以添加自定义的错误信息
// 添加了自定义的错误信息是可以让测试失败时,打印出更多的信息,帮助调试
// assert!(larger.can_hold(&smaller), "The larger rectangle cannot hold the smaller one.");
// assert_eq!(5, add_two(2), "Icarus");
// 用should_panic!宏来测试代码是否panic
// 如果panic了,测试就会通过,但是如果你预测会抛错,但是没有测试就会失败。这就说明程序的逻辑有错
pub struct Guess {
value: i32,
}
impl Guess {
pub fn new(value: i32) -> Guess {
if value < 1 || value > 100 {
panic!("Guess value must be between 1 and 100, got {}.", value);
}
Guess { value }
}
pub fn value(&self) -> i32 {
self.value
}
}
#[cfg(test)]
mod tests2 {
use super::*;
#[test]
// #[should_panic]直接写是概括全部的恐慌(没被预知的的错误),还可以像下面那样进行更细致的判断
#[should_panic(expected = "Guess value must be between 1 and 100")]
fn greater_than_100() {
Guess::new(200);
}
}
// 在测试中使用Result<T, E>,因为它也可以通过返回Ok或者Err来表示测试是否通过
// 好处就是它是常用标准库的,不需要再引入 use super::*;
// 坏处就是不能写should_panic!宏了,因为它会返回错误,而不会引发恐慌
#[cfg(test)]
mod tests3 {
#[test]
fn it_works() -> Result<(), String> {
if 2 + 2 == 4 {
Ok(())
} else {
Err(String::from("two plus two does not equal four"))
}
}
}
// 控制测试的运行
// 默认的就是并行运行,所有的测试,成功的测试不显示print输出,失败的会显示print输出
// 可以通过改变cargo test命令的参数来控制测试的运行
// cargo test --help可以查看所有参数
// --test <name> 只运行指定的测试
// --test add 其实会测试add1 ,add2...
// --test add div 运行所有以add或div开头的测试
// 在 #[test]下面写#[ignore] 就可以忽略这个测试
// -- --include-ignored 可以运行所有测试,包括被标记为ignored的测试
// -- --ignored 可以运行被标记为ignored的测试
// -- --nocapture 可以让测试的输出显示在终端上,而不是被隐藏起来
// -- --show-output 测试成功也可以让测试的输出显示在终端上,而不是被隐藏起来
// -- --no-run 可以只生成测试的输出,而不运行测试
// -- --quiet 可以关闭测试的输出
// -- --color <when> 可以设置测试的输出颜色,默认是auto
// -- --nocolor 可以关闭测试的输出颜色
// -- --no-fail-fast 可以让测试在遇到第一个失败后继续运行,而不是立即停止
// -- --test-threads <n> 可以设置测试的线程数,默认是1。如果是1就是单线程
// -- --message-format <format> 可以设置测试的输出格式,默认是pretty
// -- --verbose 可以让测试的输出更详细
// 并行测试要求:相互之间没有依赖,不会依赖于某一个共享的状态
// 测试分类
// 单元测试和集成测试
// 单元测试,小,专注,独立,快速,可重复,一次对一个模块进行隔离测试,可以测试private的函数
// 在注解#[cfg(test)]的函数就是单元测试,只有运行cargo test才能编译和运行
// 集成测试,大,复杂,依赖,慢,不可重复, 一次对多个模块进行测试,只能测试public的函数
// 在不同的文件夹中,不需要注解#[cfg(test)]标注,但是要导入被测试函数,还要#[test]标注
// 集成测试一般是为了验证多个测试库的多个部分在一起能不能工作
// 要创建tests目录,和src同级
// tests目录下的每一个文件都是一个单独的包crate
//integration_test.rs
use adder::add_two;
#[test]
fn it_adds_two() {
assert_eq!(4, add_two(2));
}
// 执行命令:cargo test 会将所有测试函数都执行了,包括tests文件下的所有文件里的函数
// 执行命令:cargo test -- --test integration_test 测试单个文件
// 那如果tests目录下的help函数也会被执行
// 这个时候我们就要在tests目录下建立common目录,这个是rust默认的不会在测试中执行的目录
// 这个时候我们我们就能够使用common目录下的帮助函数了
use adder::add_two;
mod common;
#[test]
fn it_adds_two() {
common::setup();
assert_eq!(4, add_two(2));
}
// 如果不是binary crate文件,只是main.rs那就没办法创建集成测试
// 因为没办法将main.rs的函数导入作用域