cargo new world_hello # 创建rust工程
cd world_hello
cargo run # 运行项目,会自动编译,运行
cargo build # 编译项目,生成target目录,会从Cargo.toml的dependency获取依赖,从create.io下载依赖
./target/debug/test1 # 运行编译好的命令
cargo build --release # release编译,会优化代码
./target/release/test1 # 运行
更多格式化输出,https://course.rs/basic/formatted-output.html#debug-%E7%89%B9%E5%BE%81
使用println!
宏来打印这些数据。花括号{}
用于占位符,将变量的值依次填充进去。
有些类型没有实现 std::fmt::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 永远也不会自动创建数据的“深拷贝”,如果需要用到深拷贝,就调用 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.引用必须总是有效的(变量在离开作用域后,就自动释放其占用的内存,所以不能像另外一个作用域返回引用本作用域的变量)
切片允许你引用集合中部分连续的元素序列,而不是引用整个集合,切片本质也是引用。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.结构体字段如果需要使用引用类型,就必须加上生命周期,否则就会报错。
通过 struct 和 impl 来实现
有两种方法,一种是函数没有 self,成为关联函数,一种是有 self,成为 method
pub struct Brc20 {
pub name: String,
}
impl Brc20 {
pub fn new(name: String ) -> Brc20 { // 这个函数没有self, 只能通过Brc20::new()调用,称为关联函数
Brc20 {
name
}
}
pub fn process(self) -> String { // 直接通过实例brc20.process()调用
let mut result = String::new();
result += &self.name;
result
}
}
fn main() {
let handler = Brc20::new("aaa".to_string());
let res = handler.process();
print!("res:{}", res)
}
self,&self, &mut self
self
表示 Rectangle
的所有权转移到该方法中,这种形式用的较少&self
表示该方法对 Rectangle
的不可变借用&mut self
表示可变借用,如果要修改对象属性,用这个枚举类型是一个类型,它会包含所有可能的枚举成员,而枚举值是该类型中的具体某个成员,类似 C 的 Union 类型,里面的成员可以是不同的类型。
struct A {
number: u8,
}
enum PokerCard {
Clubs(u8), // 关联一个u8
Spades, // 不关联任何值
Diamonds { // 关联一个匿名struct
number: u8,
suit: String,
},
Hearts(A), // 关联一个struct
}
fn main() {
let c1 = PokerCard::Spades;
let c2 = PokerCard::Diamonds { number: 8, suit: String::from("Diamonds")};
let c3 = PokerCard::Hearts(A { number: 7 });
let c4 = PokerCard::Clubs(9);
print_suit(c1);
print_suit(c2);
print_suit(c3);
print_suit(c4);
}
fn print_suit(p: PokerCard) { // 打印枚举变量
match p {
PokerCard::Clubs(value) => println!("Clubs: {}", value),
PokerCard::Spades => println!("Spades"),
PokerCard::Diamonds {number, suit} => println!("Diamonds: {} of {}", number, suit),
PokerCard::Hearts(A { number }) => println!("Hearts: {}", number), // CORRECT CODE
}
}
在 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]
[u8; 3]
和[u8; 4]
是不同的类型,数组的长度也是类型的一部分&[T]
,因为后者有固定的类型大小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 的元素类型是复杂类型,不能直接 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对象
它们底层的数据都存储在内存堆上,然后通过一个存储在栈中的引用类型来访问。
fn add<T>(a:T, b:T)->T{
a+b // 编译会报错,因为不是所有的类型都能相加,需要使用trait对T进行限制,我们称之为特征约束
}
struct Point<T> {
x: T,
y: T,
}
fn main() {
let integer = Point { x: 5, y: 10 }; // 自动识别T为int
let float = Point { x: 1.0, y: 4.0 };// 自动识别T为float
}
enum Result<T, E> {
Ok(T),
Err(E),
}
fn divide(numerator: f64, denominator: f64) -> Result<f64, &'static str> { // 返回值是一个泛型
if denominator == 0.0 {
Err("Cannot divide by zero")
} else {
Ok(numerator / denominator)
}
}
struct Point<T> {
x: T,
y: T,
}
impl<T> Point<T> {
fn x(&self) -> &T {
&self.x
}
}
fn main() {
let p = Point { x: 5, y: 10 };
println!("p.x = {}", p.x());
}
泛型的性能
Rust 是在编译期为泛型对应的多个类型,生成各自的代码,(相当于编译器帮你写了多份代码),因此损失了编译速度和增大了最终生成文件的大小,但是对性能不影响。
匹配对应模式然后以另一部分代码替换当前代码
let v: Vec<u32> = vec![1, 2, 3]; // vec! 是一个声明宏
过程宏接收 Rust 代码作为输入,在这些代码上进行操作,然后产生另一些代码作为输出
类似于面向对象的 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)
}
}
如果你想要为类型 A
实现特征 T
,那么 A
或者 T
至少有一个是在当前作用域中定义的!
定义具有默认实现的方法,这样其它类型无需再实现该方法,或者也可以选择重载该方法
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 作为参数使用,我们使用 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");
}
上面 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
Self
derive
派生特征编译器会自动实现 Trait
#[derive(Debug)]
struct User {
active: bool,
username: String,
id: u64,
}
# 编译器自动实现
impl std::fmt::Debug for User {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(f, "User {{ active: {:?}, username: {:?}, id: {:?} }}", self.active, self.username, self.id)
}
}
生命周期可以定义为一个引用所能持续的范围,是编译器用于预防悬垂引用(在对应的值已经被析构后仍被使用)的方式。这在 Rust 中特别重要,因为 Rust 放弃了垃圾收集器并选择手动管理内存。
大部分情况下,rust 编译器可以自动识别引用的生命周期,判断悬垂引用,但是还有一些情况需要我们使用生命周期标注来告诉编译器。
{
let r;
{
let x = 5;
r = &x;
}
println!("r: {}", r);//r引用了内部花括号中的 x 变量,但是 x 会在内部花括号 } 处被释放,此时r就是悬垂指针
}
&i32 // 一个引用
&'a i32 // 具有显式生命周期的引用
&'a mut i32 // 具有显式生命周期的可变引用
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() {
x
} else {
y
}
}
当结构体中出现引用字段时,就应该标注字段的生命周期和整个结构体的生命周期
结构体引用字段的生命周期应该大于等于结构体的生命周期
struct ImportantExcerpt<'a> {
part: &'a str,
}
&'static
对于生命周期有着非常强的要求:一个引用必须要活得跟剩下的程序一样久,才能被标注为 &'static
。
panic 可以是被动触发或者主动触发,如果是子线程触发,只会终止触发的那个线程,其他程序不受影响
fn main() {
panic!("crash and burn");
}
enum Result<T, E> {
Ok(T),
Err(E),
}
在 Rust 编程语言中,这就是一种惯例,如果一个函数可能会失败,那它应该返回Result
类型而不是直接返回值。
Result
是一种枚举(enum),它被广泛用于错误处理
use std::fs::File;
use std::io;
use std::io::Read;
use std::error::Error;
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)
}
let res = handler.process(); // 返回的是Result
match res {
Ok(value) => {
println!("Success: {}", value);
}
Err(e) => {
eprintln!("Error: {}", e);
}
}
当函数返回值是 Result 时,函数内可以使用宏?来快速传播 Err
fn run(config:&Config) -> Result<(), Box<dyn Error>> {
let content = fs::read_to_string(&config.file_path)?;// 发生错误马上返回
print!("{},{}",config.query,content);
Ok(()) // 函数执行正确没有返回值,使用空元组 ()占位
}
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
}
其他语言一般用 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
Result,Option 有一个 unwrap_or_else 方法,参数是一个闭包函数,当 Result 是 Err 或者 Option 是 None 时,会调用这个闭包
也就是匿名函数
|参数列表| -> 返回值类型 {
// 函数体
}
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 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();
Rust 中最常见的指针就是 引用,引用以 &
符号为标志并借用了它们所指向的值。除了引用数据没有任何其他特殊功能,也没有额外开销。智能指针(smart pointers)是一类数据结构,它们的表现类似指针,但是也拥有额外的元数据和功能
智能指针实现了 Deref 和 Drop trait
fn main() {
let x = 5;
let y = Box::new(x); // y是一个指针,*y才是指针指向的值
assert_eq!(5, x);
assert_eq!(5, *y);
}
仅适合单线程使用
允许变量有多个 owner,指针会记录引用的数量,数量为 0 时释放变量
fn main() {
let five = Rc::new(5);
let shared_five = Rc::clone(&five); // 只会增加引用数值
let another_shared_five = Rc::clone(&five);
println!("{}", *five); // 输出:5
println!("{}", *shared_five); // 输出:5
println!("{}", *another_shared_five); // 输出:5
// 使用 Rc::strong_count 查看引用计数
println!("{}", Rc::strong_count(&five)); // 输出: 3
}
Rust 的静态借用规则:
一个或者多个不可变引用(即 &T)。
仅一个可变引用(即 &mut T)。
它允许你即使在有不可变引用时也可以改变数据,这通常是借用规则所不允许的
use std::cell::RefCell;
struct Container {
inner_value: RefCell<i32>,
}
impl Container {
fn new(value: i32) -> Container {
Container {
inner_value: RefCell::new(value),
}
}
fn increment(&self) {
*self.inner_value.borrow_mut() += 1;
}
fn get_value(&self) -> i32 {
*self.inner_value.borrow()
}
}
fn main() {
let container = Container::new(5); // container是不可变的
println!("Initial value: {}", container.get_value());
container.increment();
println!("After increment: {}", container.get_value());
}
原理
borrow(): 这个方法返回一个 Ref<T>,它是一个智能指针,包裹着 RefCell 的不可变引用。你可以同时借用多个不可变引用.
borrow_mut(): 这个方法返回一个 RefMut<T>,它是一个智能指针,包裹着 RefCell 的可变引用。你只能借用一个活跃的可变引用.