Rust语言学习
Rust语言学习
- 通用编程概念
- 所有权
- 所有权
- 引用和借用
- slice
- struct(结构体)
- 定义并实例化一个结构体
- 使用结构体
- 方法语法
- 枚举 enums
- 定义枚举
- match控制流运算符
- if let 简单控制流
- 使用包、Crate和模块管理不断增长的项目(模块系统)
- 包和crate
- 定义模块来控制作用域与私有性
- 路径用于引用模块树中的项
- 使用use关键字将名称引入作用域
- 将模块分割进不同文件
- 常用集合
- vrctor<T>
- 使用字符串存储utf-8编码的文本
- 哈希map存储键值对
- 错误处理
- panic!与不可恢复的错误
- Rusult 与可恢复的错误
- 泛型、trait、生命周期
- 泛型数据类型
- trait :定义共享的行为
- 生命周期和引用有效性
- 测试
- 编写测试
通用编程概念
- let定义一个不可变的值,需要可变则 加mut;
- 使用const 声明常量,常量不仅是不可变的,它们始终是不可变的。常量只能设置为常量表达式,而不能设置为函数调用的结果或只能在运行时计算的任何其他值。
- 遮蔽,使用let对关键字进行阴影处理,例如let x = 5; let x = x + 1 ;
- 静态类型,Rust是一种静态类型的语言,必须在编译器知道所有变量的类型;
- 标量类型:整数、浮点数、布尔值、字符
- 复合类型:元组、数组
- 元组: 将多种类型的多个值组合为一个复合类型的一般方法。元组的长度是固定的:声明后,它们就无法增长或缩小。例如:let tup = (500, 6.4, 1); let (x, y, z) = tup;
- 数组:数组具有固定长度,例如:let a = [1, 2, 3, 4, 5]; let a: [i32; 5] = [1, 2, 3, 4, 5];
- 函数:蛇形大小写作为函数和变量名的常规样式。在蛇形情况下,所有字母均为小写,并在下划线之间使用单独的单词。
- 功能实体包含语句和表达式
- 语句:语句不返回值;如:let a= 5;
- 表达式:**{ let x=3; x+1 }**表达式不包括结尾分号。如果在表达式的末尾添加分号,则将其变成一条语句,然后该语句将不返回值。
- 具有返回值的函数:函数的返回值与函数主体块中最终表达式的值同义。您可以通过使用return关键字并指定一个值从函数中提前返回,但是大多数函数隐式返回最后一个表达式。例如:fn five() -> i32 { 5 }
- if bool { } else if bool {} else {}:if的条件不加() ,结果可赋值给变量
- 循环重复 :loop、while、for
- loop { // 逻辑 } :Rust提供了另一种更可靠的突破循环的方法。您可以将break关键字放在循环中,以告知程序何时停止执行循环;
let mut counter = 0;
let result = loop {
counter += 1;
if counter == 10 {
break counter * 2;
}
};
- while bool表达式 {}
```javascript
let mut a = 0;
let aa = [1,2,2,3,4];
while a<aa.len() {
println!("{}",aa[a]);
a = a+2;
}
```
所有权
所有权
- 所有权规则
- Rust中的每个值都有一个称为其所有者的变量。
- 值在任一时刻有且只有一个所有者。
- 当所有者(变量)离开作用域,该值将被删除。
- 所有权的存在为了管理堆数据
- String 类型存储在堆上,当离开作用域时,Rust会调用一个特殊函数,这个函数叫drop 放置释放内存的代码。
- Rust 永远也不会自动创建数据据的“深拷贝”,
- 变量与数据的交互方式:
- 移动 let s1 = s2; (主要是存储在堆里的数据,如果作为参数传入到新的函数中,之后不可使用,参考其下代码二)
- 克隆 clone()
- 只在栈上的拷贝 (区分上述的移动和拷贝)。Rust 有一个叫做 Copy trait 的特殊注解,。如果一个类型拥有 Copy trait,一个旧的变量在将其赋值给其他变量后仍然可用。
- 所有整数类型,比如 u32。
- 布尔类型,bool,它的值是 true 和 false。
- 所有浮点数类型,比如 f64。
- 字符类型,char。
- 元组,当且仅当其包含的类型也都是 Copy 的时候。比如,(i32, i32) 是 Copy 的,但 (i32, String) 就不是。
- 返回值也可以转移所有权
- 转移返回值的所有权(变量的所有权总是遵循相同的模式:将值赋给另一个变量时移动它。当持有堆中数据值的变量离开作用域时,其值将通过 drop 被清理掉,除非数据被移动为另一个变量所有。)
- 返回参数的所有权 (引用)
//代码二
fn main() {
//s值移入到函数中 之后不在有效
let s = String ::from("hello");
takes_ownership(s);
//返回值转移所有权
let s2 = String::from("abc");
let s3 = takes_and_give_back(s2);
println!("s3:{}",s3);
//转移返回值的所有权
let s1 = String::from("word");
let (s2, len) = calculate_length(s1);
println!("s2:The length of '{}' is {}.", s2, len);
let x =5; //x是copy的,所以后面可继续使用
makes_copy(x);
}
fn takes_ownership(some_string:String){
println!("{}",some_string);
}
fn makes_copy(some_integer:i32){
println!("{}",some_integer);
}
fn takes_and_give_back(str:String) -> String {
str
}
fn calculate_length(s: String) -> (String, usize) {
let length = s.len(); // len() 返回字符串的长度
(s, length)
}
引用和借用
- 引用就是符号&。 允许使用所有权但不获取所有权。
- 我们将获取引用作为函数参数称为借用。
- 正如变量默认是不可变的,引用也一样。(默认)不允许修改引用的值。
- 可变引用:&mut
- 在特定作用域中的特定数据中只能有一个可变引用,好处是可以在编译时就避免数据竞争,可以通过大括号创建一个新的作用域,以允许拥有多个可变引用,只是不能同时拥有。
- 引用的规则:在任意给定时间,要么只能有一个可变引用,要么只能有多个不可变引用。 引用必须总是有效的。
slice
- 允许引用集合中一段连续的元素序列,而不引用整个集合。
- 没有所有权的数据类型。
- 引用 &string[1…2] ; &string[…1]; &string[1…];(部分 或起止末尾的引用)。
- 所有权、借用和slice让rust在编译时确保内存安全。
struct(结构体)
自定义数据类型,允许命名和包装多个相关的值,形成一个有意义的组合。
定义并实例化一个结构体
- 变量与字段同名的字段初始化简写语法。
- 可使用结构体更新语法从其他实例创建实例。
- (元组结构体):使用没有命名字段的元组结构体来创建不同的类型。
- (类单元结构体):没有任何字段的结构体(因为它们类似于 (),即unit 类型。类单元结构体常常在你想要在某个类型上实现 trait 但不需要在类型中存储数据的时候发挥作用)。
- 结构体数据的所有权,(如果结构体存储被其对象拥有数据的引用,需要用上生命周期(lifetimes))
使用结构体
- {} 中加入 :? 指示符告诉 println! 我们想要使用叫做 Debug 的输出格式。Debug 是一个 trait,它允许我们以一种对开发者有帮助的方式打印结构体,以便当我们调试代码时能看到它的值。(参考代码如下)
- #[derive(Debug)] 注解
let user = User {
user_name: String::from("yyn"),
age: 18,
live: true
};
println!("user:{:#?}",user);
#[derive(Debug)]
struct User{
user_name:String,
age:u16,
live:bool,
}
方法语法
- 方法和函数类型 使用fn关键字,可以拥有参数和返回值 ,不同点:在结构体的上下文中被定义(或枚举或trait对象的上下文),并且第一个参数总是self,代表调用该方法的结构体实例。
- 自动引用和解引用: 方法调用时rust中少数几个拥有这种行为的地方:当使用 object.something() 调用方法时,Rust 会自动为 object 添加 &、&mut 或 * 以便使 object 与方法签名匹配。
- 方法有一个明确的接收者———— self 的类型。在给出接收者和方法名的前提下,Rust 可以明确地计算出方法是仅仅读取(&self),做出修改(&mut self)或者是获取所有权(self)。事实上,Rust 对方法接收者的隐式借用让所有权在实践中更友好。
- 关联函数(不以self作为参数的函数):impl的另一个有用的功能,它们与结构体相关联,他们仍然是函数,而不是方法,因为它们并不作用于一个结构体的实例,例如:String::from
- 每个结构体都允许拥有多个impl块。
- 结构体不是创建自定义类型的唯一方法,还有枚举。
fn main(){
let mut r = Rectangle{
width:20,
height:20
};
r.setHeight(300);
println!("r.area:{}",r.area());
let square = Rectangle::square(300);
println!("square:{}",square.height);
}
#[derive(Debug)]
struct Rectangle {
width:u32,
height:u32,
}
impl Rectangle{
fn square(size:u32)-> Rectangle{
return Rectangle{ width:size,height:size};
}
fn setHeight(&mut self,height:u32){
self.height = height;
}
fn area(&self) -> u32{
self.height* self.width
}
}
枚举 enums
定义枚举
- 可以将任意类型的数据放入到枚举成员中。例如:字符串、数字类型、或者结构体,还可以是另一个枚举。
- 结构体和枚举还有另一个相似点:使用impl来为结构体定义方法那样,也可以在枚举上定义方法。
- Option枚举:为了拥有一个可能为空的值,你必须要显式的将其放入对应类型的 Option 中。接着,当使用这个值时,必须明确的处理值为空的情况。只要一个值不是 Option 类型,你就 可以 安全的认定它的值不为空。这是 Rust 的一个经过深思熟虑的设计决策,来限制空值的泛滥以增加 Rust 代码的安全性。
- match 表达式就是这么一个处理枚举的控制流结构:它会根据枚举的成员运行不同的代码,这些代码可以使用匹配到的值中的数据。
match控制流运算符
- 允许我们将一个值与一系列的模式相比较,并根据相匹配的模式执行相应代码。
- ** _ 通配符** :会匹配所有的值,通过将其放在其他分支之后,_将会匹配所有之前没有指定的可能的值。
- match 在只关心 一个 情况的场景中可能就有点啰嗦了。为此 Rust 提供了if let。
if let 简单控制流
- if let 获取通过等号分隔一个模式和一个表达式,失去match强制要求的穷尽性检查。
- if let 包含一个else 。else块中代码与match表达式中的_分支块中的代码相同。
#[derive(Debug)]
enum IpAddKind{
V4,
V6(String)
}
#[derive(Debug)]
struct IpAddr{
kind :IpAddKind,
address:String,
}
fn main(){
//使用结构体和枚举定义数据
let home = IpAddr{
kind:IpAddKind::V4,
address:String::from("127.0.0.1")
};
let loopback = IpAddr{
kind:IpAddKind::V6(String::from("::1")),
address:String::from("::1")
};
//使用match
let b = value_in_ip_add_kind(home.kind);
println!("v4:{}",b);
let a = value_in_ip_add_kind(loopback.kind);
println!("v6:{}",a);
//Option<T>
//let five = Some(5);
//let six = plus_one(five);
//let none = plus_one(None);
//if let 使用
let some_and_value = Some(4);
if let Some(3) = some_and_value{
println!("three");
} else {
println!("qita");
}
}
fn value_in_ip_add_kind(ip_add_kind:IpAddKind) -> u8 {
match ip_add_kind{
IpAddKind::V4 => 1,
IpAddKind::V6(str)=>{
println!("str:{}",str);
return 2
},
}
}
fn plus_one(x:Option<i32>) -> Option<i32>{
match x {
None => None,
Some(i) => Some(i+1)
}
}
使用包、Crate和模块管理不断增长的项目(模块系统)
一个包可以包含多个二进制crate项和一个可选的crate库。伴随着包的增长,你可以将包中的部分代码提取出来做成独立的crate,这些crate则作为外部依赖项。
作用域
模块系统:
1、包(Packages): Cargo 的一个功能,它允许你构建、测试和分享 crate。
2、Crates :一个模块的树形结构,它形成了库或二进制项目。
3、模块(Modules)和 use: 允许你控制作用域和路径的私有性。
4、路径(path):一个命名例如结构体、函数或模块等项的方式
cargo new --lib 模块名
Rust 提供了将包分成多个 crate,将 crate 分成模块,以及通过指定绝对或相对路径从一个模块引用另一个模块中定义的项的方式。你可以通过使用 use 语句将路径引入作用域,这样在多次使用时可以使用更短的路径。模块定义的代码默认是私有的,不过可以选择增加 pub 关键字使其定义变为公有。
包和crate
- Cargo 遵循的一个约定:src/main.rs 就是一个与包同名的二进制 crate 的 crate 根。Cargo 知道如果包目录中包含 src/lib.rs,则包带有与其同名的库 crate,且 src/lib.rs 是 crate 根。Cargo提供了工作区间这一功能。
- crate 是一个二进制项或者库。crate root 是一个源文件,Rust 编译器以它为起始点,并构成你的 crate 的根模块.
- 包(package) 是提供一系列功能的一个或者多个 crate。一个包会包含有一个 Cargo.toml 文件,阐述如何去构建这些 crate。
- 包中所包含的内容由几条规则来确立。一个包中至多 只能 包含一个库 crate(library crate);包中可以包含任意多个二进制 crate(binary crate);包中至少包含一个 crate,无论是库的还是二进制的。
- 如果一个包同时含有 src/main.rs 和 src/lib.rs,则它有两个 crate:一个库(main)和一个二进制项,且名字都与包相同。通过将文件放在 src/bin 目录下,一个包可以拥有多个二进制 crate:每个 src/bin 下的文件都会被编译成一个独立的二进制 crate。
- cargo new name创建一个可执行工程 src/main.rs
- cargo new --lib name 创建一个库工程 src/lib.rs
定义模块来控制作用域与私有性
- paths(允许你命名项的路径);use(将路径引入作用域);pub(使项变为公有);as,外部包,glob运算符;
- 模块让我们可以将一个 crate 中的代码进行分组,以提高可读性与重用性。模块还可以控制项的 私有性,即项是可以被外部代码使用的(public),还是作为一个内部实现的内容,不能被外部代码使用(private)。
- ** 定义一个模块**:是以 mod 关键字为起始,然后指定模块的名字,并且用花括号包围模块的主体。在模块内,我们还可以定义其他的模块。模块还可以保存一些定义的其他项,比如结构体、枚举、常量、特性、或者函数。
路径用于引用模块树中的项
- 路径分为绝对路径和相对路径:绝对路径(absolute path)从 crate 根开始,以 crate 名或者字面值 crate 开头。相对路径(relative path)从当前模块开始,以 self、super 或当前模块的标识符开头。
- 绝对路径和相对路径都后跟一个或多个由双冒号(::)分割的标识符。
- 模块还定义了Rust的私有性边界,Rust默认所有项都是私有的,父模块中的项不能使用子模块中的私有项,但是子模块中的项可以使用父模块中的项。
- 使用pub关键字创建公共项,使子模块的内部部分暴露给上级模块。
- 使用super起始的相对路径。
- 使用pub 可以设计公有的结构体和枚举,我们还可以决定每个结构体的字段是否公有。
- 如果结构体中具有私有字段,我们需要提供公共的关联函数来构造结构体的实例。
- 与结构体相反**,如果把枚举设置为公有,它的成员都将变为公有。**
使用use关键字将名称引入作用域
- 使用use关键字将路径一次性引入作用域,然后调用该路径的项。
- 使用use 将两个同名类型引入同一个作用域这个问题还有另一个解决办法 使用as关键字指定一个新的本地名或者别名。 例如:use std::fmt::Result; use std::io::Result as IoResult;
- 使用 pub use 重导出名称,将项引入作用域,并同时使其可供其他代码引入自己的作用域。
- 使用外部包 Cargo.toml 加入 [dependence] rand=“0.5.5” ,加入一行use起始的包名。
- crates.io 上有很多 Rust 社区成员发布的包,将其引入你自己的项目都需要一道相同的步骤:在 Cargo.toml 列出它们并通过 use 将其中定义的项引入项目包的作用域中。 例如:[dependencies] rand = “0.5.5”
- 嵌套路径来消除大量的use行。 例如:use std::{cmp::Ordering,io}
- 通过glob运算符将所有的公有定义引入作用域 ** *,glob运算符 **,glob运算符经常用于测试模块tests中,后面讲解。
将模块分割进不同文件
- mod *** 后使用分号,而不是代码块,这将告诉Rust在另一个与模块同名的文件中加载模块的内容。
常用集合
vrctor
- 新建
- let v:Vec = Vec::new();
- let v = vec![1,2,3];
- 更新 push
- 读取 索引语法(&v[下标]) 或者 get()(返回一个Option<&T>)
- (执行所有权和借用规则)不能在相同作用域中同时存在可变和不可变引用的规则
- 遍历 **for i in & (mut) v {} **
- 更多参考文档
使用字符串存储utf-8编码的文本
- Rust 的核心语言中只有一种字符串类型:str,字符串 slice,它通常以被借用的形式出现,&str;
- 创建字符串 String::new(); String::from(“abc”); “abc”.to_string();
- 更新字符串push_str();push(); 使用+运算符 (let s3 = s1 + &s2; // 注意 s1 被移动了,不能继续使用)或format! 宏拼接字符串
- 索引字符串 s1[0]
- slice 字符串
- for c in “字符串”.chars(){}
哈希map存储键值对
- 新建 HashMap::new(); HashMap<,> 可以使用下划线占位,而 Rust 能够根据 数据的类型推断出 HashMap 所包含的类型。
- insert(插入、覆盖) get(获取) entry(是否存中)
错误处理
panic!与不可恢复的错误
- Rust 有 panic!宏。当执行这个宏时,程序会打印出一个错误信息,展开并清理栈数据,然后接着退出。
- 当出现 panic 时,程序默认会开始 展开((unwinding),这意味着 Rust 会回溯栈并清理它遇到的每一个函数的数据,不过这个回溯并清理的过程有很多工作。另一种选择是直接 终止(abort),这会不清理数据就退出程序。**panic 时通过在 Cargo.toml 的 [profile] 部分增加 panic = ‘abort’,可以由展开切换为终止。例如,如果你想要在release模式中 panic 时直接终止 [profile.release] panic = ‘abort’ **
- RUST_BACKTRACE 环境变量来得到一个 backtrace ** RUST_BACKTRACE=1 cargo run**
Rusult 与可恢复的错误
- Result<T,E>
- 失败时panic的简写:unwrap(unwrap 会为我们调用 panic!)和expect( 输出失败信息)
- 传播错误:当编写一个其实现会调用一些可能会失败的操作的函数时,除了在这个函数中处理错误外,还可以选择让调用者知道这个错误并决定该如何处理。
- 传播错误的简写:?运算符
泛型、trait、生命周期
泛型数据类型
- Rust 通过在编译时进行泛型代码的 单态化(monomorphization)来保证效率。单态化是一个通过填充编译时使用的具体类型,将通用代码转换为特定代码的过程。
trait :定义共享的行为
- trait 告诉 Rust 编译器某个特定类型拥有可能与其他类型共享的功能。可以通过 trait 以一种抽象的方式定义共享的行为。可以使用 trait bounds 指定泛型是任何拥有特定行为的类型。**注意:trait 类似于其他语言中的常被称为 接口(interfaces)的功能,虽然有一些不同。 **
- trait 定义是一种将方法签名组合起来的方法,目的是定义一个实现某些目的所必需的行为的集合。
- 例如:pub trait Summary { fn summarize(&self) -> String; }; 实现 使用 impl trait名称 for 目标{fn… }
- 实现 trait 时需要注意的一个限制是,只有当 trait 或者要实现 trait 的类型位于 crate 的本地作用域时,才能为该类型实现 trait。
- 但是不能为外部类型实现外部 trait
- trait中的方法可有默认实现。
- trait作为参数: fn 方法名(item: impl trait名){ … }
- trait bound 语法糖 fn 方法名 <T : trait名>( item:T){ … }
- 在<T : trait名> 还可使用 + 或者使用 where fn some_function<T, U>(t: T, u: U) -> i32 where T: Display + Clone, U: Clone + Debug { … }
- 也可在返回值中使用 impl Trait语法 ,返回一个只是指定了需要实现的 trait 的类型的能力在闭包和迭代器场景十分的有用
- 通过使用带有 trait bound 的泛型参数的 impl 块,可以有条件地只为那些实现了特定 trait 的类型实现方法。
生命周期和引用有效性
- Rust 中的每一个引用都有其 生命周期(lifetime),也就是引用保持有效的作用域。
- 生命周期避免了悬垂引用。它会导致程序引用了非预期引用的数据。
- 生命周期注解语法
- &i32 // 引用
- &'a i32 // 带有显式生命周期的引用
- &'a mut i32 // 带有显式生命周期的可变引用
- 泛型生命周期
- 当从函数返回一个引用,返回值的生命周期参数需要与一个参数的生命周期参数相匹配。如果返回的引用 没有 指向任何一个参数,那么唯一的可能就是它指向一个函数内部创建的值,它将会是一个悬垂引用,因为它将会在函数结束时离开作用域。
- 生命周期语法是用于将函数的多个参数与其返回值的生命周期进行关联的。一旦他们形成了某种关联,Rust 就有了足够的信息来允许内存安全的操作并阻止会产生悬垂指针亦或是违反内存安全的行为。
- 结构体定义中的生命周期注解
- 我们将定义包含引用的结构体,不过这需要为结构体定义中的每一个引用添加生命周期注解。
- 被编码进 Rust 引用分析的模式被称为 **生命周期省略规则 **
- 第一条规则是每一个是引用的参数都有它自己的生命周期参数。换句话说就是,有一个引用参数的函数有一个生命周期参数:fn foo<'a>(x: &'a i32),有两个引用参数的函数有两个不同的生命周期参数,fn foo<'a, 'b>(x: &'a i32, y: &'b i32),依此类推。
- 第二条规则是如果只有一个输入生命周期参数,那么它被赋予所有输出生命周期参数:fn foo<'a>(x: &'a i32) -> &'a i32。
- 第三条规则是如果方法有多个输入生命周期参数并且其中一个参数是 &self 或 &mut self,说明是个对象的方法(method) ,那么所有输出生命周期参数被赋予 self 的生命周期。第三条规则使得方法更容易读写,因为只需更少的符号。
- **静态生命周期 **
- ** 'static **,其生命周期能够存活于整个程序期间。
测试
编写测试
- #[test]注解 方法上使用
- assert_eq!宏 相等 assert_ne! 不相等
- 使用 assert! 宏来检查结果
- assert!、assert_eq! 和 assert_ne! 宏传递一个可选的失败信息参数,可以在测试失败时将自定义失败信息一同打印出来
- 使用 #[should_panic] 检查 panic ,检查方法打印值是否相同
- 将 Result<T, E> 用于测试作为返回值