Rust快速入门
Rust快速入门
打印数据
更多格式化输出,course.rs/basic/formatted-output.h...
使用println!
宏来打印这些数据。花括号{}
用于占位符,将变量的值依次填充进去。
有些类型没有实现std::Display,就要用{:?}占位符
fn main() {
let name = "Alice";
let age = 30;
let height = 1.65;
println!("Name: {}, Age: {}, Height: {}", name, age, height);
}
打印结构体,需要在定义struct加上#[derive(Debug)]
#[derive(Debug)]
struct User {
active: bool,
username: String,
id: u64,
}
fn main() {
let user1 = User{
active:true,
username:String::from("aaa"),
id:1
};
print!("{:?} \n", user1);
}
语句和表达式
语句会执行一些操作但是不会返回一个值,需要分号结尾;
表达式会进行求值,然后返回一个值,不能有分号;
调用一个函数,调用宏,调用{}语句块,都是一个表达式,会有返回值
let x = x + 1; // 语句
let y = y + 5; // 语句
x + y // 表达式
数据类型和赋值
基本数据类型
所有整数类型,比如 u32
布尔类型,bool,它的值是 true 和 false
所有浮点数类型,比如 f64
字符类型,char
元组,当且仅当其包含的类型也都是 Copy 的时候。比如,(i32, i32) 是 Copy 的,但 (i32, String) 就不是
不可变引用 &T ,例如转移所有权中的最后一个例子,但是注意: 可变引用 &mut T 是不可以 Copy的
复合类型
字符串与切片
元祖
结构体
枚举
数组
Rust 基本类型都是通过自动拷贝的方式来赋值的。
Rust复杂类型的赋值,可以称为所有权转移或者移动(move),就像是浅拷贝,同时让第一个变量失效
let s1 = String::from("hello");
let s2 = s1; // 移动,
println!("{}, world!", s1); // 编译错误,use of moved value: `s1`
println!("{}, world!", s2);
- Rust 中每一个值都被一个变量所拥有,该变量被称为值的所有者
- 一个值同时只能被一个变量所拥有,或者说一个值只能拥有一个所有者
- 当所有者(变量)离开作用域范围时,这个值将被丢弃(drop)
克隆(深拷贝)
Rust 永远也不会自动创建数据的 “深拷贝”,如果需要用到深拷贝,就调用clone()
函数的传值与返回
和变量赋值一样,函数传参也会出现move(复杂类型)或者copy(基本类型)的现象
let s1 = String::from("hello");
show(s1);
print!("{}\n",s1);// 编译错误,use of moved value: `s1`
引用与解引用(引用通常也叫借用)
常规引用是一个指针类型,指向了对象存储的内存地址。
fn main() {
let s1 = String::from("hello");
show(&s1); // 参数是一个引用,并没有发生转移
print!("{}\n",s1);
}
fn show(s:&String){
print!("{}\n",s.len());
}
可变引用
fn main() {
let mut s1 = String::from("hello"); // 声明一个可变变量
add_str(&mut s1); // 可变引用
print!("{}\n",s1);
}
fn add_str(s:&mut String){
s.push_str(" world"); // 只有可变引用,才能修改引用的值
print!("{}\n",s.len());
}
引用的特点
1.同一时刻,你只能拥有要么一个可变引用, 要么任意多个不可变引用
2.引用必须总是有效的(变量在离开作用域后,就自动释放其占用的内存,所以不能像另外一个作用域返回引用本作用域的变量)
复合类型
字符串与切片(与go的切片很不一样)
切片允许你引用集合中部分连续的元素序列,而不是引用整个集合,切片本质也是引用。rust有两种切片,分别是字符串切片&str,数组切片
切片只有两个字段
ptr
:这是一个指向切片首元素的原始指针。它的类型是*const T
。len
:这是切片的长度。它的类型是usize
。
特点:切片只是对数据的部分引用,而且长度固定,可以通过切片获取新的切片。
let s1 = String::from("hello");
let s2: &str = &s1[1..3]; // 字符串切片的类型标识是&str, s2是一个切片,切片对象包含指针和len,指针指向第二个元素,长度是2
print!("{} \n",s2) // el
字符串字面量也是切片
let s: &str = "hello";//编译器在编译的时候直接将字符串字面量以硬编码的方式写入程序的二进制文件中,当程序被加载时,字符串字面量保存中Read Only Memory 字段中。如果有两个相同的字面量,他们的地址相同
String
Rust 在语言级别,只有一种字符串类型: str
,它通常是以引用类型出现 &str
,但是在标准库里,还有多种不同用途的字符串类型,其中使用最广的即是 String
类型。str
类型是硬编码进可执行文件,也无法被修改,但是 String
则是一个可增长、可改变且具有所有权的 UTF-8 编码字符串,String类型是变长的,所以需要在堆上分配。
pointer
:heap中值的内存地址length
:当前值的长度、当前元素个数。capacity
:当前缓冲区的容量,可以容纳元素的个数,当前字符串的长度超过当前分配的capacity
会重新分配内存,会将当前字符串拷贝到新分配的内存中。
String与&str相互转换
// 字符串切片转String
let s = String::from("hello,world");
let s1 = "hello,world".to_string();// 当我们调用 &str 的 to_string 方法时,实际上就是创建一个新的 String 对象,其内容是 &str 的深拷贝。
// String转&str
let s = String::from("hello,world");
print!("String={}\n", s);
print!("&str={} \n", &s); // 所有元素
print!("&str={} \n", &s[1..3]); // 只要下标1-2的元素
元组
元组是由多种类型组合到一起形成的,元组的长度是固定的,元组中元素的顺序也是固定的。
fn main() {
let s1 = String::from("hello");
let (s2, len) = calculate_length(&s1); // 使用模式匹配解构元组
let res = calculate_length(&s1); // 使用.下标来访问
println!("The length of '{}' is {} \n", s2, len);
print!("The length of '{}' is {} \n",res.0, res.1);
}
fn calculate_length(s: &str) -> (&str, usize) { // 函数返回一个元组
let length = s.len();
(s, length)
}
结构体
由多个类型组合在一起,有结构体名称,有字段
如果要修改结构体字段,必须声明为可变类型
实例化结构体,必须为每个字段赋值(不然编译报错)
struct User {
active: bool,
username: String,
id: u64,
}
fn main() {
let mut user1 = User{ // 实例化结构体,必须为每个字段赋值
active:true,
username:String::from("aaa"),
id:1
};
user1.username = String::from("bbb"); // 修改结构体
print!("{},{},{} \n", user1.active, user1.username, user1.id);
}
结构体所有权
1.如果是整个struct发生move,则user1不能再使用
let user1 = User{
active:true,
username:String::from("aaa"),
id:1
};
let user2 = user1;
print!("{},{},{} \n", user1.active, user1.username, user1.id); // 报错
2.如果只是struct某个字段发生move, user1除了发生move的字段不能使用,其他字段还可以使用
let user1 = User{
active:true,
username:String::from("aaa"),
id:1
};
let user3 = User{
active:user1.active,
username:user1.username, // 发生move
id:user1.id
};
print!("{},{} \n", user1.active,user1.id); // 正常
3.结构体字段如果需要使用引用类型,就必须加上生命周期,否则就会报错。
枚举-enum
枚举类型是一个类型,它会包含所有可能的枚举成员, 而枚举值是该类型中的具体某个成员,类似C的Union类型,里面的成员可以是不同的类型。
enum PokerCard { // 定义枚举类型,里面有4个成员
Clubs(u8), // 这个成员关联一个u8类型的值
Spades(u8),
Diamonds(char), // 这个成员关联一个char类型的值
Hearts(char),
}
fn main() {
let c1 = PokerCard::Spades(5); // 实例化一个枚举成员,并且关联5
let c2 = PokerCard::Diamonds('A');
print_suit(c1); // 处理枚举变量
print_suit(c2);
}
fn print_suit(p: PokerCard) { // 传入一个枚举类型的变量
match p {
PokerCard::Clubs(value)=> println!("Clubs: {}", value), // 得到枚举成员关联的值
PokerCard::Spades(value)=> println!("Spades: {}", value),
PokerCard::Diamonds(value)=> println!("Diamonds: {}", value),
PokerCard::Hearts(value)=> println!("Hearts: {}", value),
}
}
数组
在 Rust 中,最常用的数组有两种,第一种是速度很快但是长度固定的 array
,第二种是可动态增长的但是有性能损耗的 Vector
array
- 长度固定
- 元素必须有相同的类型
- 依次线性排列
let arr1:[u8,3] = [1,2,3];
let arr2 = ["hello", "world"]
println!("{:?}",arr1); // 1,2,3
println!("{:?}",arr2); // 0,0,0
数组切片
let arr1: [i32; 3] = [1,2,3]; // 数组类型[T,len]
let s1: &[i32] = &arr1[1..2]; // 数组切片&[T]
println!("{:?}",arr1);// [1,2,3]
println!("{:?}",s1);// [2]
- 数组类型容易跟数组切片混淆,[T;n]描述了一个数组的类型,而[T]描述了切片的类型, 因为切片是运行期的数据结构,它的长度无法在编译期得知,因此不能用[T;n]的形式去描述
[u8; 3]
和[u8; 4]
是不同的类型,数组的长度也是类型的一部分- 在实际开发中,使用最多的是数组切片[T],我们往往通过引用的方式去使用
&[T]
,因为后者有固定的类型大小
动态数组-Vector
Vector,HashMap 都是标准库封装的类型,它们底层的数据都存储在内存堆上,然后通过一个存储在栈中的引用类型来访问,可以动态扩展
跟结构体一样,Vector
类型在超出作用域范围后,会被自动删除,内部的元素也会被删除
let mut arr1 = Vec::new(); // 实例化vector对象,
let mut arr2 = vec![1,2,4]; // 使用宏实例化对象,并且同时初始化元素
arr2.push(3) // 添加元素
// 读取元素,下标或者.get()
fn main() {
let arr2 = vec![1,2,4]; // 使用宏实例化对象,并且同时初始化元素
let arr3 = vec!["welcome","hello"];
let first = arr2[0];
let first_s = arr3[0];
match arr2.get(1) { // 通过get()访问,不会越界报错
Some(k) => println!("第二个元素是 {k}"),
None => println!("去你的第二个元素,根本没有!"),
}
}
vector元素的所有权
如果Vector的元素类型是复杂类型,不能直接arr[0]来赋值
let mut arr1 = vec![String::from("hello"), String::from("world")];
let a = arr1[0]; // 会编译错误,有两种解决1.使用引用 2.通过remove(),pop发生所有权转移
let a = arr1.remove(0);// 会把元素所有权转移给a,同时元素会移动填充0下标,元素多的时候会消耗性能,vec长度也会变化
let b = &arr1[1];// 直接引用, 如果需要得到String, b.to_string()会生成新的Stringd对象
KV 存储 HashMap
它们底层的数据都存储在内存堆上,然后通过一个存储在栈中的引用类型来访问。
Method
Rust 的对象定义和方法定义是分离的
struct Circle{
x:f64,
y:f64,
}
impl Circle {
fn area(&self)->f64{
self.x* self.y
}
}
fn main() {
let c = Circle{x:22.0,y:10.0};
let a = c.area();
println!("a={}",a)
}
self,&self, &mut self
self
表示Rectangle
的所有权转移到该方法中,这种形式用的较少&self
表示该方法对Rectangle
的不可变借用&mut self
表示可变借用,如果要修改对象属性,用这个
关联函数
这种定义在 impl
中且没有 self
的函数被称之为关联函数: 因为它没有 self
,不能用 f.read()
的形式调用,因此它是一个函数而不是方法,它又在 impl
中,与结构体紧密关联,因此称为关联函数,只能通过T::fn()来调用
struct Circle{
x:f64,
y:f64,
}
impl Circle {
fn new(x:f64,y:f64)->Circle{
Circle { x: x, y: y }
}
}
fn main() {
let c = Circle::new(1.0,2.0 ); // 调用关联函数
println!("x={},y={}",c.x, c.y)
}
泛型
fn add<T>(a:T, b:T)->T{
a+b // 编译会报错,因为不是所有的类型都能相加,需要使用trait对T进行限制,我们称之为特征约束
}
struct Point<T,U> { // struct中使用泛型
x: T,
y: U,
}
impl<T,U> Point<T,U> { // 给泛型struct添加method
fn new(x: T, y: U) -> Point<T,U> {
Point {
x,
y
}
}
}
fn main() {
let res = add(1,2); // 对a赋值时,T就被确定为整数类型
print!("a+b={}", res);
}
泛型的性能
Rust 是在编译期为泛型对应的多个类型,生成各自的代码,(相当于编译器帮你写了多份代码),因此损失了编译速度和增大了最终生成文件的大小,但是对性能不影响。
特征 Trait
类似于面向对象的interface{},定义了一组可以被共享的行为,只要实现了特征,你就能使用这组行为
如果不同的类型具有相同的行为,那么我们就可以定义一个特征,然后为这些类型实现该特征。
定义Trait
pub trait Person {
fn say(&self)->String;
}
Trait的关联类型
trait除了可以定义方法,还可以定义类型,实现Trait特征时,也需要实现关联类型,像泛型的T,需要指明类型
pub struct Puppy;
trait Animal {
type Baby; // 关联类型
fn have_baby(&self) ->Self::Baby;
}
impl Animal for Dog {
type Baby = Puppy; // 确认关联类型
fn have_baby(&self) -> Self::Baby {
println!("A puppy is born.");
Puppy
}
}
实现Trait
struct User {
id:i32,
name:String,
}
// 实现trait
impl Person for User {
fn say(&self)->String{
format!("id is {}, name is {}",self.id, self.name)
}
}
trait的孤儿规则
如果你想要为类型 A
实现特征 T
,那么 A
或者 T
至少有一个是在当前作用域中定义的!
trait的默认实现
定义具有默认实现的方法,这样其它类型无需再实现该方法,或者也可以选择重载该方法
pub trait Person {
fn author(&self)->String;
fn say(&self) { // 有具体的实现,其他类型可以不实现或者重载这个方法
print!("{} is saying", self.author());
}
}
trait作为参数使用
fn notify(item: &impl Person) { // 参数是trait, 写法就是 impl trait
item.say();
}
fn notify2(item: &(impl Person+Display) { // 多重约束,参数必须实现Person和Display特征
item.say();
}
fn main() {
let u1 = User{name:String::from("aa")};
notify(&u1)
}
特征约束(trait bound)
trait作为参数使用,我们使用impl trait其实是一个语法糖,本质是这样:
fn notify3<T: Person>(user:&T){ // 对T类型,必须实现Person特征进行限制,T: Person就是特征约束
user.say();
}
多重约束
fn notify3<T: Person+Display>(user:&T){ // 对T类型,必须实现Person特征进行限制,T: Person就是特征约束
user.say();
}
函数返回中的 impl Trait
可以通过 impl Trait
来说明一个函数返回了一个类型,该类型实现了某个特征。
但是这种写法有个缺点,只可以返回一种具体的类型,这种类型实现了 Person
trait
fn createp(n:&str)->impl Person{
User{
name:String::from(n)
}
}
fn main() {
let u1 = createp("aa");
let u2 = createp("bb");
}
Trait对象
上面User 实现了Person, 如果又有一个Child 实现了Person.
Trait对象指向实现了Person特征的类型的实例,也就是指向了 User
或者 Child 的实例,这种映射关系是存储在一张表中,可以在运行时通过特征对象找到具体调用的类型方法。
特征对象:Box,当成一个引用即可,只不过它包裹的值会被强制分配在堆上。
fn createp(n:&str, b:bool) -> Box<dyn Person>{ // 返回一个特征对象(类似智能指针,当做一个引用即可)
if b {
Box::new(User{
name:String::from(n)
})
}else{
Box::new(Child{ // 通过Box::new()创建特征对象
name:String::from(n),
age:1
})
}
}
fn notify(item: Box<dyn Person>) { // 参数是特征对象
item.say();
}
fn main() {
let u1 = createp("aa",true);
let u2 = createp("bb",false);
notify(u1);
notify(u2);
}
特征对象原理
泛型是在编译期完成处理的:编译器会为每一个泛型参数对应的具体类型生成一份代码,这种方式是静态分发(static dispatch),因为是在编译期完成的,对于运行期性能完全没有任何影响。
与静态分发相对应的是动态分发(dynamic dispatch),在这种情况下,直到运行时,才能确定需要调用什么方法。之前代码中的关键字 dyn
正是在强调这一“动态”的特点。
Box, 包含了两个指针
ptr: 指向实现了特征 Person的具体类型的实例,比如类型 User 的实例、类型Child 的实例
vptr指向一个虚表
vtable,保存了实例对于可以调用的实现于特征 Person 的方法
trait对象的限制
不是所有的trait都有trait对象,必须满足一定条件的trait
- 方法的返回类型不能是
Self
- 方法没有任何泛型参数
生命周期
生命周期可以定义为一个引用所能持续的范围,是编译器用于预防悬垂引用(在对应的值已经被析构后仍被使用)的方式。这在 Rust 中特别重要,因为 Rust 放弃了垃圾收集器并选择手动管理内存。
大部分情况下,rust编译器可以自动识别引用的生命周期,判断悬垂引用,但是还有一些情况需要我们使用生命周期标注来告诉编译器。
生命周期标注语法
生命周期的基本标记是 'a
,其中 a
可以是任何有效的 Rust 标识符。此标记用于注解有生命周期的元素。比如引用类型的生命周期,如 &'a T
。这表示这个引用的生命周期至少和 'a
一样长。
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() {
x
} else {
y
}
}
&’static
&'static
对于生命周期有着非常强的要求:一个引用必须要活得跟剩下的程序一样久,才能被标注为 &'static
。
错误处理
panic
panic可以是被动触发或者主动触发, 如果是子线程触发,只会终止触发的那个线程,其他程序不受影响
fn main() {
panic!("crash and burn");
}
Result枚举用于处理函数返回
在Rust编程语言中,这就是一种惯例,如果一个函数可能会失败,那它应该返回Result
类型而不是直接返回值。
Result
是一种枚举(enum),它被广泛用于错误处理
use std::fs::File;
use std::io;
use std::io::Read;
fn main() {
_ = read_content();
}
fn read_content() ->Result<String, Box<dyn Error>> { // 注意Result有两个成员Ok,Err,成员可以携带数据,比如这个String就是Ok的
let mut f = File::open("test.txt")?;
let mut s = String::new();
f.read_to_string(&mut s)?;
Ok(s)
}
快速处理Result
宏?,发生错误直接返回
当函数返回值是Result时,函数内可以使用宏?来快速传播Err
fn run(config:&Config) -> Result<(), Box<dyn Error>> {
let content = fs::read_to_string(&config.file_path)?;// 发生错误马上返回
print!("{},{}",config.query,content);
Ok(()) // 函数执行正确没有返回值,使用空元组 ()占位
}
unwrap,发生错误,直接panic
Result是枚举,要读取里面的数据,需要用match处理,但是也有快速的方法
use std::fs::File;
fn main() {
let f = File::open("hello.txt").unwrap(); // 如果成功,直接返回Ok(T)关联的值,如果失败,直接panic
let f = File::open("hello.txt").expect("Failed to open hello.txt");// 和unwarp一样,但是会打印自定义panic日志
let f = File::open("hello.txt").unwarp_or_else(闭包) // 成功发挥Ok关联的值,失败执行闭包,不会Panic
}
Option 枚举用于处理空值
其他语言一般用null处理空值,
enum Option<T> {
Some(T),
None,
}
let maybe_value = Some(42); // 实例化Option实例,绑定一个值
let value = maybe_value.unwrap_or_else(|| 0);// 如果是None就返回0
println!("{}", value); // Prints 42
unwrap_or_else 处理Result,Option
Result,Option有一个unwrap_or_else方法,参数是一个闭包函数,当Result是Err或者Option是None时,会调用这个闭包
闭包 Closure
也就是匿名函数
|参数列表| -> 返回值类型 {
// 函数体
}
let add = |x, y| -> i32 { // 有返回值
x + y
};
let run = |x| { // 没有返回值
}
let result = add(1, 2);
println!("{}", result); // 打印: 3
闭包中捕获作用域中的值
fn main() {
let s = String::from("Hello, world!");
let run = ||{
print!("{}", s); //闭包函数可以使用作用域内的值,而不用当做参数传进去
};
run();
}
迭代器 Iterator
实现了 Iterator trait 的类型,就可以在 Rust 中被视为迭代器
pub trait Iterator {
type Item;
fn next(&mut self) -> Option<Self::Item>;
// 方法with default implementations elided
}
常见的集合类型,例如Vector、Array、HashSet、HashMap, 字符串String类型虽然不是迭代器,但是可以通过.iter()
和 .into_iter()
方法,生成迭代器。
into_iter
会夺走所有权iter
是借用iter_mut
是可变借用
fn main() {
let arr = vec![1,3,6,8,9,10,11,12,13,14,15];
let arr_iter = arr.iter(); // 借用
for v in arr_iter{
println!("{}", v) // v就是借用每一个元素
}
}
fn main() {
let arr = vec![1,3,6,8,9,10,11,12,13,14,15];
let arr_into_inter = arr.into_iter(); // 元素所有权转移
for v in arr_into_inter{
println!("{}", v) //
}
}
fn main() {
let arr = vec![1,3,6,8,9,10,11,12,13,14,15];
let arr_mut_iter = arr.iter_mut(); // 可变借用
for v in arr_mut_iter{
*v += 1; // 修改借用元素的值
}
}
消费者适配器
只要迭代器上的某个方法 A
在其内部调用了 next
方法,那么 A
就被称为消费性适配器:因为 next
方法会消耗掉迭代器上的元素,所以方法 A
的调用也会消耗掉迭代器上的元素。
let arr = vec![1,3,6,8,9,10,11,12,13,14,15];
let arr_ter = arr.iter();
let s:i32 = arr_ter.sum(); // 会把迭代器中的所有元素相加,执行sum后,会转移所有的元素所有权
迭代器适配器
迭代器有些方法会返回一个新的迭代器,方便链式调用,结尾一定要有消费者适配器返回数据
let v1: Vec<i32> = vec![1, 2, 3];
let v2: Vec<_> = v1.iter().map(|x| x + 1).collect();
包和模块
- 包(Crate):一个由多个模块组成的树形结构,可以作为三方库进行分发,也可以生成可执行文件进行运行,它编译后会生成一个可执行文件或者一个库。
cargo new my-project // 创建一个二进制 Package,src/main.rs 是二进制包的根文件
cargo new my-lib --lib // 不能独立运行,只能作为三方库被其它项目引用,根文件是 src/lib.rs
包名就是创新项目的目录名称,在Cargo.toml也会有
[package]
name = "minigrep"
version = "0.1.0"
edition = "2021"
- 模块(Module):可以一个文件多个模块,也可以一个文件一个模块,模块可以被认为是真实项目中的代码组织单元
模块路径
绝对路径,以包名或者 crate
作为开头
use minigrep::Config; // minigrep就是package名称
模块可见性
父模块完全无法访问子模块中的私有项,但是子模块却可以访问父模块、父父..模块的私有项。
类型转换
本作品采用《CC 协议》,转载必须注明作者和本文链接
未完待续。。。