本篇文章是《Rust程序设计语言》第一~六章笔记
$ rustc hello.rs
$ cargo new demo2 # 此命令用于新建一个cargo项目
Created binary (application) `demo2` package
$ cargo build #此命令用于编译
$ cargo run #此命令可先编译后运行该项目
#build或run的运行要在项目目录或src目录下,
#其中run后面可以空着,也可以跟上项目名或main.rs。
#build命令后面什么都不用跟,跟着反而报错。
总结:
可以使用 cargo build 构建项目。
可以使用 cargo run 一步构建并运行项目。
可以在不生成二进制文件的情况下构建项目并使用 cargo check 检查错误。
有别于将构建结果放在与源码相同的目录,Cargo 会将其放到 target/debug 目录。
可以使用 cargo build --release 来优化编译项目。Cargo 会将其放到 target/release 目录。
首先应声明并初始化一个String类型对象
let mut str =String::new();
核心方法是
io::stdin().read_line(&mut str)//输入
str.trim().parse()//转换:关键在于trim方法,输入的数字字符串结尾处带一个回车,要trim修剪掉
rust是链式编程,在方法后面可直接继续.expect(“xx exception”)进行异常处理
在Cargo.toml文件的[dependencies]标签下写rand的依赖:
rand = “0.8.3”
然后在不改动代码的情况下cargo build构建,首次构建cargo会自动帮下载依赖。
main.rs中引入:use rand::Rng;//todo:vscode能否用快速修复来引入依赖?
核心方法是:
rand::thread_rng().gen_range(1…101);//todo:这里的1…101是什么意思?
和其他语言一样也有while和if else
但如果用while true 实现永远循环会报警告,提示应该使用loop关键字。
todo:除了使用if else实现分支,还可用match
use std::io;
use rand::Rng;
//1. 输入数字,String转int
fn get_number()->i32{
println!("Please input your number:");
let mut guess_number = String::new();
io::stdin().read_line(&mut guess_number).expect("Failed to read line");
println!("your guessed : {} ",guess_number);
let guess_number :i32= guess_number.trim().parse().expect("ClassCastException");//这里运用rust的shadowing特性
return guess_number;
}
fn main() {
println!("Welcome to guess game!");
//2.生成随机数,声明猜测的数
let target = rand::thread_rng().gen_range(1..101);
let mut guess_int :i32;
//3. 判断是否猜对
loop{
guess_int = get_number();
if guess_int>target {
println!("too big");
}else if guess_int<target{
println!("too small");
}else if guess_int==target{
println!("right");
break;
}
}
println!("Game over,bye!")
}
let num = 3;
num = 4;//编译无法通过:num是不可变的变量
let mut str = "hello";//mut关键字让变量可被再次赋值
str = "institution";//可以再次赋值
const i :i32 = 7;
// let mut k = 5;//报错
fn main() {
let mut num = 5;
const j :i32 = 19;
}
常量的数据类型不可被省略。
常量可以放到全局作用域(变量不可),常量不能mut,常量的值在编译期必须是明确的,不允许在运行时赋值。
rust把赋值叫做绑定,所以shadowing重影也可以叫做重新绑定
经常有这样的场景:某个string类型的变量我们要转换为int类型,要为他们分别命名,如price_str 、price_int
在rust中可以用shadowing巧妙地避免这样的冗余。
如
let mut guess = String::new();
let mut guess = 10;
整型、浮点型、布尔、字符四种
有符号:i8-128,无符号:u8-128;范围分别是-2^(n-1) 到[2^(n-1)]-1 和0到(2^n)-1
isize 和usize是取决于计算机硬件架构的类型,可能是32位或64位。
f32和f64两种,默认是f64,因为现代CPU架构大多是64位了。
运算时要注意精度问题。
复合类型有:元组、数组。复合类型存在栈中。(todo:一定是这样的吗?如果元组、数组的元素是引用类型呢?)
let t :(i32,f64,u8)=(90,8.5,2);
let (x,y,z)=t;//解构元组
元组可以被理解为简易的结构体和对象,可用于作为函数的多返回值。
有索引。上一条的例子中可用下标访问元组的元素。如t.0
数组长度不可变。Vector长度可变,更灵活。
let arr :[i32;5]=[1,2,3,4,5];
arr[0];//中括号里的数字是下标
在声明的时候用了mut关键字就是可变的,可重新赋值,如
let mut arr :[i32:3]=[1,2,3];
arr[0] = 98;
println!("{}",arr[0]);//输出98
3.1、rust中的函数可以省略return,直接写一个表达式用于返回
3.2、函数是否支持多个返回值?可以用元组实现多返回值的效果,调用后需要解构。
4.1、打印99乘法表
4.2、打印n阶斐波那契数列
4.3、打印火箭发射倒计时
use std::time::Duration;
use std::thread;
const ci :i32 = 7;//全局作用域允许声明常量,常量必须显式声明其类型
// let mut k = 5;//报错,全局作用域不可声明变量
fn main() {
let mut num = 5;
const j :i32 = 19;//在函数内的常量也必须显式声明其类型
let mut arr :[i32;5]=[6,7,8,9,0];//数组是基本类型中的复合类型,存在栈中,长度不可变
println!("first element of arr is {} ",arr[0]);//可以用下标查询数组中的元素
arr[0]=2;//如果数组是mut,则可以修改数组中的元素值,但数组长度是不可改变的
println!("first element of arr is {} ",arr[0]);
multi_table();
fibonacci(30);
rocket();
}
fn multi_table(){
for i in 1..10{
for j in 1..i+1{
print!("{} × {} = {} ",i,j,i*j);
}
println!();
}
}
fn fibonacci(n :i32){
let mut i:i32 = 0;
let mut j:i32 = 0;
let mut ni:i32 = 1;
while ni <= n{
if i == 0 {
print!("0 , ");
i = 1;
}else if i == 1{
print!("1 , 1 , ");
i = 2;
j = 1;
}else {
print!("{} , ",i);
let k = j;
j = i;
i = i +k;
}
ni += 1;
}
println!();
}
fn rocket(){
for i in (1..11).rev(){
println!("{}",i);
thread::sleep(Duration::from_millis(1000));
}
println!("0 ! rocket lift!");
}
1.1、所有权规则系统要解决的是堆中变量的内存分配和回收的问题。
1.2、对于String这样未知大小的变量,且运行时大小可能会改变的变量,必须在堆中开辟空间进行存储。
其大小改变时,可能还需要重新开辟空间。
1.3、其他语言对于String的处理,主流的做法是两种:
一是由程序员为复杂变量手动请求并分配内存,用完此变量后再释放/回收内存。
二是使用垃圾回收机制,由GC自动回收内存。
1.4、手动回收内存曾经是一个容易导致问题的操作,如果忘记回收会浪费内存,如果过早回收会使变量无效,如果重复回收还会导致bug。
1.5.1、rust的每个值都有一个被称为其所有者的变量。
1.5.2、值在任意时刻有且只有一个所有者。
1.5.3、当所有者离开作用域,这个值将被丢弃。
基本类型变量存在栈上,使用时是值传递。
引用类型变量存在堆上,所有者的本质是个指针,使用时传递了指针。
尝试用代码和图来说明。
//基本类型---------------------------------------
fn main() {
{
let a = 5;
let b = a;
println!("{}",a);//a仍然拥有所有权
}
//println!("{}",a);此处编译报错
}
//-----------------------------------------------
//引用类型---------------------------------------
fn main() {
{
let a = String::from("hello");
let b = a;//a会把所有权移交给b
// println!("{}",a);此处编译报错:a已被moved
println!("{}",b);
}//b被丢弃
}
//-----------------------------------------------
基本类型都实现了Copy trait,直接用=赋值就能实现完全的拷贝,原来的变量不会被丢弃。包括元组和数组。
fn main() {
let a = 5;//基本类型
let b = a;//基本类型实现了Copy trait,在赋值后能直接实现拷贝,原来的a还能继续使用
println!("{}",a);//这里是可以使用的
println!("{}",b);
prt_i(b);
println!("{}",b);
//----------------------------------------------------------------------------------------------------
let a = String::from("hello");
let b = a;//引用类型在赋值操作之后,a移交给了b,a就失效了。
let c = b.clone();//想真的实现拷贝需要用clone方法
// println!("{}",a);//a已经被移交moved,不能再使用
println!("{}",b);
println!("{}",c);
prt_str(b);//这里传参操作之后,b移交给了函数,b 就失效了。
// println!("{}",b);//b已经移交moved,不能再使用
//---------------------------------------------------------------------------------------------------
let arr = [1,2,3];//数组和元组也是存于栈中的,属于基本类型中的复合类型。
let brr = arr;//数组的赋值操作也能实现拷贝
println!("{},{}",arr[0],brr[1]);
}
fn prt_str(str :String){
println!("prt : {}",str);
}
fn prt_i(i:i32){
println!("prt : {}",i);
}
引用就是&,允许使用但不获取所有权的一个指针。
借用就是创建引用的过程。
fn main() {
let str = String::from("hello");
let len_of_str = get_len(&str);
println!("len_of_str is {}",len_of_str);
}
fn get_len(str :&String) -> usize {
str.len()
}
创建引用和传参时使用关键字mut。
2.4.1、一个引用的作用域从声明的地方开始一直持续到最后一次使用为止。
2.4.2、在给定的时间内,要么只有一个可变引用,要么只有多个不可变引用。
2.4.3、引用必须总是有效的。不允许出现悬垂引用(空指针)。
fn main() {
let mut mystr = String::from("hello");
let len_of_str = get_len(&mystr);//借用并传入函数,不移交所有权
println!("len of {} is {}",mystr ,len_of_str);//str的所有权还在,这里能直接使用
let borrow =&mut mystr;//创建可变引用
change(borrow);//把可变引用传入
change(borrow);//把可变引用传入,这里是最后一次使用,borrow的作用域到此为止
println!("after change , str is {}",mystr);//这里是不可变引用,为什么?println!宏实际生成的代码使用了&x,不可变借用
// println!("{}",borrow);//如果最后一次使用在这行会导致同时存在可变和不可变引用,编译错误。
}
fn get_len(mystr :&String) -> usize {
mystr.len()
}
fn change(mystr :&mut String){
mystr.push_str(",world");
}
// fn dangle()-> &String{//返回一个字符串的引用
// let s = String::from("null pointer~~");//生成一个字符串的变量
// return &s//返回字符串变量的引用
// }//这里s离开作用域,将被丢弃,而其引用就会变成悬垂引用/空指针,危险。
fn no_dangle()-> String{//返回一个字符串
let s = String::from("not pointer~~");//生成一个字符串的变量
return s//返回字符串,移交所有权
}
slice是一个集合一部分的引用,slice没有所有权。
let s = String::from("lotus miner");
let s1 :&str = &s[0..5];
let s2 = &s[5..];
println!("{} and {}",s1,s2);
//字符串字面量就是字符串slice,所以默认是不可变的。字符串slice的类型是 &str
fn main() {
let s = String::from("lotus miner");
let s1 :&str = &s[0..5];
let s2 = &s[5..];
println!("{} and {}",s1,s2);
let res = first_word(&s);
println!("first word is {}",res);
}
//字符串字面量就是字符串slice,所以默认是不可变的。字符串slice的类型是 &str
fn first_word(mystr:&String)-> &str{
let bs = mystr.as_bytes();
for (i,&item) in bs.iter().enumerate(){
if item ==b' '{
return &mystr[0..i];
}
}
&mystr[..]
}
fn main() {
let user1 =User{
user_name: String::from("fengbaobao"),
email: String::from("[email protected]"),
active: true,
balance: 10086.9
};
println!("{:?}",user1)//:?告诉println!宏需要按Debug格式打印结构体
}
#[derive(Debug)]//此外部属性允许println!宏打印Debug格式的结构体对象
struct User{
user_name: String,
email: String,
active: bool,
balance: f64,
}
上面的user1是不可变的,用mut令其可变:
let mut user1 =User{//mut关键字使user1可变
user_name: String::from("fengbaobao"),
email: String::from("[email protected]"),
active: true,
balance: 10086.9,
};
user1.balance = 9988.9;//更改其中某个字段的值
fn build_user(user_name: String,email: String)-> User{
User{
user_name,//简写
email:email,//完整写法
active: true,
balance: 0.0,
}
}
let user2 = User{
user_name:String::from("jay chou"),
..user1
};//因为email是引用类型,user1的email赋值给user2之后,user1的所有权就会移交给user2
// println!("{:?}",user1)//所有权已经移交,报错
println!("{:?}",user2);
let user3 = User{
user_name:String::from("zhangchulan"),
email:String::from("[email protected]"),
..user2
};//因为user3的引用类型字段都是新生成的值,其他基本类型字段拷贝了user2的值,所以user2的所有权还在
println!("{:?}",user2);
println!("{:?}",user3);
User中的user_name和email字段,刻意使用了拥有所有权的String类型而非&str类型,这是因为&str是一种slice,是对值的引用,不拥有所有权,可以使用&str但需要标注生命周期,此功能在第十章讨论。
只有字段类型,没有字段名的与元组类似的结构体,叫做元组结构体,例如:
struct Color(i32, i32, i32);
struct Point(i32, i32, i32);
let black = Color(0, 0, 0);
let origin = Point(0, 0, 0);
没有任何字段的结构体称为类单元结构体,可用于实现trait(第十章介绍),实现方法,例如:
struct UserService;
fn main() {
let r1 = Rectangle{
width:9,
height:8,
};
println!("r1.area is {}",r1.get_area());
}
#[derive(Debug)]
struct Rectangle{
width:i32,
height:i32,
}
impl Rectangle{
fn get_area(&self)->i32{
self.width * self.height
}
}
fn main() {
let homeIpKind = IpAddrKind::V4;
let move1 = Message::Move{x:7,y:23};
let s1 = Message::Show(String::from("institution"));
}
enum IpAddrKind{
V4,//枚举的成员
V6,//注意枚举的成员没有数据类型
}//todo:标准库中的IpAddr枚举是怎样的?
enum Message{//枚举成员可以包含任意类型和数量的数据
Quit,//没有关联数据的成员
Move{x:i32,y:i32},//此成员包含了一个匿名结构体
Show(String),//此成员包含一个String
ChangeColor(i32,i32,i32),//此成员包含三个i32
}
标准库中的IpAddr
prelude是标准库中默认导入的crate。Option就是包括在其中的。
Option是标准包中的枚举,包含Some和None两个成员。
Some不能和i8进行运算,必须进行类型转换才可以。
只要一个值不是Option,就可以认定它的值非空。
举个栗子:
从链上获取某个账户的余额,可能获取到了,也可能获取不到。
所以我们可以用Option去接这个余额。
接到之后必须处理可能为空的情况。
let balance: Option<f64> = getBalanceFromChain();
Option 枚举拥有大量用于各种情况的方法:你可以查看它的文档。熟悉 Option 的方法将对你的 Rust 之旅非常有用。
rust中有表达式和语句的概念。
match是一个表达式,可以把整个match视为一个值或一个对象。
match的每个分支的最后一句是一个表达式,中间可以有语句。
match分支可以从枚举成员的绑定数据中取出数据并在match分支内的语句中使用。
enum WorkDay{
Monday(String),
Tuesday,
Wednesday,
Thursdag,
Friday(Lunch),
}
fn value_of_workday(wd: WorkDay)-> u8{
match wd {
WorkDay::Monday(plan) => {
println!("Plan is {} this week!",plan);
1
},
WorkDay::Tuesday => 2,
WorkDay::Wednesday => 3,
WorkDay::Thursdag => 4,
WorkDay::Friday(lunch) => {
println!("Friday is relaxing. Eat some {:?}",lunch);
5
},
}
}
#[derive(Debug)]
enum Lunch{
Noodles,
Rice,
}
match最重要的特性是穷尽,必须考虑所有可能性。例如上面的例子中,必须考虑周一到周五的所有情况,才算穷尽了工作日的所有可能性。下面的代码会报错:
fn value_of_workday(wd: WorkDay)-> u8{
match wd {
WorkDay::Tuesday => 2,
WorkDay::Wednesday => 3,
WorkDay::Thursdag => 4,
}
}
假设空投规则是:从链上获取用户余额,如果有余额就加100,没有就不做操作。
fn air_drop (balance: Option<i32>)->Option<i32>{
match balance{
None => None,
Some(i) => Some(i + 100),
}
}
这样的好处是,因为match会穷尽所有情况,所以必须处理空值,避免出现空指针。
比如跨城通勤,周一要从家里去公司,周五要从公司回家,其他日子都在公司或酒店待着。
fn action(day: WorkDay){
match day{
WorkDay::Monday(_) => go_to_company(),//不需要接收绑定数据,用占位符
WorkDay::Friday(_) => go_home(),
other => work(other),//除了周一周五特殊,其他日子都执行同样操作,用通配符
}
}
fn go_home(){}
fn go_to_company(){}
fn work(w:WorkDay){}
再比如只有周一需要做计划,实现一个计划方法。
fn work_plan(day: WorkDay){
match day{
WorkDay::Monday(plan) => handle_plan(plan),
_ =>(),//通配符
}
}
fn handle_plan(s:String){}
上面的计划方法可以用if let简写,如下所示:
fn work_plan(day: WorkDay){
if let WorkDay::Monday(plan) = day {
handle_plan(plan);
}
}
实现如下需求:定义职业枚举,包含战法牧三种职业结构体。
fn main() {
let sun_shangxiang = new_player(Profession::Doctor);
let guo_jia = new_player(Profession::Master);
let zhang_liao = new_player(Profession::Warrior);
show_player(&sun_shangxiang);//引用,不移交所有权
// reward_player(sun_shangxiang);//移交所有权,下面vector存储报错。
//用vector存储这些角色
let mut player_group = vec![sun_shangxiang,guo_jia,zhang_liao];//发现特点:所有权移交的时候可以把原先不可变的变量变成可变的
//遍历这些角色,给他们buff:hp加5
for p in &mut player_group{
p.hp +=5;
}
//遍历角色,打印
for h in player_group{
println!("{:?}",h);
}
// show_player(&sun_shangxiang);//所有权已经移交,无法再次使用
}
//一、首先定义Player结构体,字段包括hp、mp、攻、防、敏、智等
#[derive(Debug)]
struct Player{
hp:i32,
mp:i32,
attack:i32,
defense:i32,
agility:i32,
intelligence:i32,
}
//二、定义枚举,里面规定了战法牧三种Player
enum Profession{
Warrior,
Master,
Doctor,
}
//三、构造方法可以匹配枚举中的职业,实例化对应的对象
fn new_player(player:Profession)->Player{
match player{
Profession::Warrior => build_player(100, 30, 100, 80, 30, 20),
Profession::Master => build_player(70, 70, 10, 10, 50, 100),
Profession::Doctor =>build_player(80, 40, 60, 50, 50, 70),
}
}
fn build_player(hp:i32,mp:i32,attack:i32,defense:i32,agility:i32,intelligence:i32)->Player{
Player{
hp,//形参与字段同名,可简写
mp,
attack,
defense,
agility,
intelligence,
}
}
fn show_player(p:&Player){
println!("Hello, I am {:?}",p);
}
fn reward_player(p:Player){
print!("{:?} is rewarded",p);
}