Rust程序语言设计(1-4)
一、Hello world
fn main() {println!("Hello, world!!!");
}
二、猜数游戏 - 目标
- 生成一个 1 到 100 间的随机数
- 提示玩家输入一个猜测
- 猜完之后,程序会提示猜测是太大还是太小了
- 如果猜测正确,那么打印出一个庆祝信息,程序退出
(1)一次猜测
fn main() {println!("猜数!");println!("猜测一个数");// let mut foo = 1;// let bar = foo; //immurable// foo = 2let mut guess = String::new();//String::new() 空字符串实例io::stdin().read_line(&mut guess).expect("无法读取行");//io::Result Ok, Errprintln!("你猜测的数是:{}",guess);
}
(2)生成随机数字
use std::io; //prelude
use rand::Rng; //traitfn main() {println!("猜数!");let secret_number = rand::thread_rng().gen_range(1..101);println!("随机数字是{}",secret_number);println!("猜测一个数");// let mut foo = 1;// let bar = foo; //immurable// foo = 2let mut guess = String::new();//String::new() 空字符串实例io::stdin().read_line(&mut guess).expect("无法读取行");//io::Result Ok, Errprintln!("你猜测的数是:{}",guess);
}
(3)比较猜测数字和随机数字
use std::io; //prelude
use std::cmp::Ordering;
use rand::Rng; //traitfn main() {println!("猜数!");let secret_number = rand::thread_rng().gen_range(1..101); // i32 u32 i64println!("随机数字是{}",secret_number);println!("猜测一个数");let mut guess = String::new();io::stdin().read_line(&mut guess).expect("无法读取行");//shadow 隐藏同名旧变量,从下一行之后,引用的是u32类型guess变量let guess:u32 = guess.trim().parse().expect("Please type a number");// trim() 去掉两端空白 parse() 把字符串转成整数类型println!("你猜测的数是:{}",guess);match guess.cmp(&secret_number) {Ordering::Less => println!("Too small!"), //arm//如果前面成立,执行后面程序Ordering::Greater => println!("Too big!"),Ordering::Equal => println!("You win!"),}
}
(4)多次猜测
use std::io; //prelude
use std::cmp::Ordering;
use rand::Rng; //traitfn main() {println!("猜数!");let secret_number = rand::thread_rng().gen_range(1..101); // i32 u32 i64loop {//loop 循环println!("猜测一个数");let mut guess = String::new();io::stdin().read_line(&mut guess).expect("无法读取行");// shadowlet guess:u32 = match guess.trim().parse() {Ok(num) => num,Err(_) => continue,//错误处理,再次输入};println!("你猜测的数是:{}",guess);match guess.cmp(&secret_number) {Ordering::Less => println!("Too small!"), //armOrdering::Greater => println!("Too big!"),Ordering::Equal => {println!("You win!");break;}}}
}
(5)运行结果
三、通用编程概念
(1)变量与可变性
1.变量
使用 let 来进行声明
fn main() {let x = 5;println!("The value of x is {}",x);x = 6;println!("The value of x is {}",x);
}
不加 mut 无法更改变量
添加 mut 后
fn main() {let mut x = 5;println!("The value of x is {}",x);x = 6;println!("The value of x is {}",x);
}
2.常量(constant)
概念:常量在绑定值以后不可变
- 与不可变变量间的区别
区别 | |
---|---|
1. | 不可以使用mut,常量永远不可变 |
2. | 声明常量使用const关键字,类型必须被标注 |
3. | 常量可以在任何作用域内声明,包括全局作用域 |
4. | 常量只可以绑定到常量表达式,无法绑定到函数的调用结果或只能在运行时才能计算出的值 |
- 在程序运行期间,常量在其声明的作用域内一直有效
- 命名规范:Rust 里常量使用全大写字母,每个单词之间只能用下划线分开,如:
- MAX_POINTS
例如:const MAX_POINTS:u32 = 100_000;
- MAX_POINTS
3.Shadowing(隐藏)
概念:可以使用相同的名字声明新的变量,新的变量就会 shadow (隐藏/覆盖) 之前声明的同名变量
例:
fn main() {let x = 5;println!("{}",x);let x = x + 5;//可以重复命名println!("{}",x);
}
(2)数据类型
Rust 是静态编译语言,在编译时必须知道所有变量的类型
- 基于使用的值,编译器通常能够判断出它的具体类型
- 如果可能的类型比较多(如,字符串转整数),就必须添加类型的标注,否则编译会报错
fn main() {let guess:u32 = "42".parse().expect("Not a number");println!("{}",guess);
}
1.标量类型
- 一个标量类型代表一个单一的值
- Rust 有四个主要的标量类型:
- 整数类型
- 浮点类型
- 布尔类型
- 字符类型
整数类型
- 整数类型没有小数部分
- 例如u32就是无符号的整数类型,占据32位的空间
- 无符号整数类型以u开头
- 有符号整数类型以i开头
- Rust 的整数类型列表如图:
Length | Signed | Unsigned |
---|---|---|
8-bit | i8 | u8 |
16-bit | i16 | u16 |
32-bit | i32 | u32 |
64-bit | i64 | u64 |
128-bit | i128 | u128 |
arch | isize | usize |
- 有符号范围:
−(2nn−1)=>(2n−1−1) -(2nⁿ-1)=>(2ⁿ⁻¹-1) −(2nn−1)=>(2n−1−1) - 无符号范围:
0=> 2n-1 0 => 2ⁿ-1 0=> 2n-1
整数溢出:
例如:u8的范围是 0-255,如果将 u8 的值设为 256,那么: - 调试模式下编译:Rust 会检查整数溢出,如果发生溢出,程序运行时就会 panic
- 发布模式下 (–release) 编译:Rust 不会检查可能导致 panic 的整数溢出
- 如果溢出发生:Rust 会执行 “环绕” 操作:
-256变成0,257变成1 - 程序不会 panic
- 如果溢出发生:Rust 会执行 “环绕” 操作:
浮点类型
- Rust 有两种基础的浮点类型,也就是含有小数部分的类型
- f32,32位,单精度
- f64,64位,双精度
- Rust 的浮点类型使用了 IEEE-754 标准来表述
- f64 是默认类型
布尔类型
- Rust 布尔类型有两个值:true 和 false
- 一个字节大小
- 符号是 bool
字符类型
- char 类型被用来描述语言中最基础的单个字符
- 字符类型的字面值使用单引号
- 4字节大小
- 是 Unicode 标量值:除ASCII外,拼音、中日韩文、零长度空白字符、emoji表情
- U+0000 到 U+D7FF
- U+E000 到 U+10FFFF
- Ubicode 没有“字符”的概念
2.复合类型
Tuple(元组)
- Tuple 可以将多个类型的多个值放在一个类型中
- Tuple 的长度是固定的:一旦声明就无法改变
创建Tuple
- 在小括号里,将值用逗号分开
- Tuple 中每个位置都对应一个类型, Tuple 中各个元素的类型不必相同
fn main() {let tup:(i32,f64,u8) = (500,6.4,1);println!("{},{},{}",tup.0,tup.1,tup.2);
}
获取Tuple的元素值
- 可以使用模式匹配来解构(destructure)
fn main() {let tup:(i32,f64,u8) = (500,6.4,1);let (x,y,z) = tup;println!("{},{},{}",x,y,z);
}
访问Tuple的元素
println!("{},{},{}",tup.0,tup.1,tup.2);
数组
- 数组可以将多个值放在一个类型里
- 数组中每个元素类型必须相同
- 数组的长度是固定的
声明数组
- 中括号内,逗号隔开
let a = [1,2,3,4,5];
数组的用处
- 将数据存放在stack上而不是heap上,保证有固定数量的元素
- 数组没有Vector灵活
- Vector 和数组类似,由标准库提供
- Vector 的长度可以改变
- 如不确定使用哪个时,用Vector
数组的类型
- [类型,长度]
- 例如:let a:[i32,5] = [1,2,3,4,5]
另一种声明数组的方法
- 如果元素值都相等
- 在中括号里指定初始值
- 然后是一个“;”
- 最后是数组长度
- 例:
let a = [3;5]
=let a = [3,3,3,3,3]
访问数组的元素
- 数组是 Stack 上分配的单个块的内存
- 可以使用索引来访问数组的元素
fn main() {let list = [1,2,3,4,5,6,7,8,9,10];let first = list[0];let second = list[1];println!("第一个元素为{},第二个元素为{}",first,second);
}
- 如果访问超出数组范围限制
- 编译会通过
- 运行会报错
(3)函数与注释
函数的声明
针对函数和变量名,Rust使用snakecase命名规范:
- 所有字母都是小写,单词间使用下划线分开
fn main() {println!("First function");another_function();
}fn another_function() {println!("another function");
}
形参\实参
fn main() {another_function(5, 6); // argument
}
fn another_function(x:i32, y:i32) { //parameterprintln!("The value of x is : {}",x);println!("The value of y is : {}",y);
}
函数返回值
- 在 -> 符号后边声明函数返回值的类型,但是不可以为返回值命名
- 在 Rust里面,返回值就是函数体里面的一个表达式的值
- 如果想提前返回,需使用 return 关键字,并指定一个值
fn five() -> i32 {5
}fn main() {let x = five();println!("The value of x is:{}",x);
}
传入参数
fn num(x:i32) -> i32 {x + 5
}fn main() {let x = num(6);println!("The value of x is:{}",x);
}
(4)控制流
if表达式
- if 表达式允许根据条件执行不同的代码分支
- 条件必须是 bool 类型
- if 表达式中,与条件关联的代码块,叫做分支(arm)
- 可以添加 else 表达式
fn main() {let num = 3;if num < 5 {println!("Condition was true");} else {println!("Condition was false");}
}
在 let 语句中使用 if
- if 表达式可以放在 let 语句的等号右边
fn main() {let condition = true;let number = if condition {5} else {6};println!("The value of number is : {}" , number);
}
循环
- Rust 提供了3种循环
- loop、while、for
loop 循环
- 反复执行一块代码,直到喊停
- 可以使用 break 关键字,告知程序何时停止
fn main() {let mut counter = 0;let result = loop {counter += 1;if counter == 10{break counter * 2;}};println!("The result is : {}", result);
}
while 循环
fn main() {let mut num = 3;while num != 0 {println!("{}!",num);num = num - 1;}println!("LIFTOFF!!!");
}
For 循环
fn main() {let a = [10,20,30,40,50];for element in a.iter() {println!("The value is : {}",element);}
}
Range
rev 代表逆向遍历
fn main() {for number in (1..4).rev() {println!("{}!",number);}println!("LIFTOFF!");
}
四、认识所有权
(1)什么是所有权
1.所有权
- Rust 的hexintexing
- 所有程序运行时必须管理自己使用计算机内存方式
- 垃圾处理机制,不断寻找不再使用的内存
- 其他语言,程序员必须显式地分配和释放内存
- Rust 的方式
- 内存通过所有权系统管理,其中包含一组编译器在编译时检查的规则
- 当程序运行时,所有权不会减慢程序的运行速度
Stack & Heap (栈内存和堆内存)
Stack 按值的接收顺序来存储,按相反的顺序移除(先进后出)
- 添加数据叫做入栈
- 移除数据叫做出栈
所有内存在 Stack 上的数据必须拥有已知的固定的大小 - 编译时大小未知或可能发生变化的数据必须存储在 heap 上
Heap 内存组织性差: - 数据存入 heap 时,会请求一定数量的空间
- 操作系统在 heap 中找到足够的空间,进行标记,并返回指针
- 该过程称为分配
指针是已知固定大小,可以存放在 stack 上 - 如想要实际数据,必须使用指针定位
将数据压到 stack 要比分配至 heap 快
访问数据
访问 heap 中的数据速度慢于访问 stack
Stack 中的数据相当于紧密连接着
heap 中的数据相当于分散在各个角落
所有权存在原因
所有权解决的问题
- 跟踪代码的哪些部分正在使用 heap 的哪些数据
- 最小化 heap 上的重复数据
- 清理 heap 上未使用的数据
注:管理 heap 数据,是所有权存在的原因
2.所有权规则、内存与分配
所有权规则
- 每个值都有一个变量,这个变量是该值的所有者
- 每个值同时只能有一个所有者
- 当所有者超出作用域(scope)时,该值将被删除
变量作用域
fn main() {// s 变量不可用let s = "xxxxx"; //自该行起,到本代码块结束可用
}
//超出代码块范围,作用域结束,变量 s 不可用
String 类型
- 字符串字面值:程序里手写的字符串值,不可变
- Rust 还有第二种字符串类型:String
String 类型值的创建 - 使用 from 函数从字符串字面值创建 String 类型
let s = String::from("xxxxx");
- “::” 表示 from 是 String 类型下的函数
fn main() {let mut s = String::from("Hello");// 定义String类型s.push_str(", world!");// push_str :追加内容println!("{}",s);
}
内存和分配
字符串字面值是被硬编码到可执行文件中
- 速度快、高效都是因为他的不可变性
String 类型为了支持可变性,需要在 heap 上分配内存 - 操作系统必须在运行时来请求内存
通过调用 String::from
来实现- 使用完后,需要将内存返还给系统
- 没有GC,就需要自己去识别哪些内存不再使用,并调用代码进行返还
未返还,就是浪费内存
提前返还,变量会变成非法
不能多次进行,一次分配对应一次释放
Rust采用的方法:当某个值的变量走出作用范围时,内存会自动交还个操作系统
变量与数据交互的方式:Move
let x = 5;
let y = x;
因为是固定值,所以这两个 5 就被压到了 stack中
针对String类型
其他语言的拷贝的方式可能会导致 二次释放(double free)
fn main() {let s1 = String::from("Hello");let s2 = s1;println!("{}",s1); //无法调用已经移动的
}
Rust:则采用了废弃的方式来处理,当String类型的 s1 赋值给 s2 后,s1 在该作用域后续的代码中将无法调用
- 浅拷贝(shallow copy)
- 深拷贝(deep copy)
- 由于 Rust 使 前置变量失效了,所以叫做:移动(Move)
变量与数据交互的方式:克隆Clone
fn main() {let s1 = String::from("Hello");let s2 = s1.clone();println!("{},{}",s1,s2);
}
当使用 Clone(克隆) 方法后,两个变量都是有效的
Stack上的数据:复制
- 任何简单标量的组合类型都可以是 Copy 的
- 任何需要分配内存或某种资源的都不是 Copy 的
3.所有权与函数
将值传递给函数和把值赋给变量是类似的
- 将值传递给函数将发生移动或复制
fn main() {let s = String::from("Hello world");take_ownership(s);let x = 5makes_copy(x);println!("x:{}",x);
}fn take_ownership(some_string:String) {println!("{}", some_string);
}fn makes_copy(some_number:i32) {println!("{}",some_number);
}
返回值与作用域
- 函数在返回值的过程中同样也会发生所有权转移
fn main() {let s1 = gives_ownership();let s2 = String::from("hello");let s3 = takes_and_gives_back(s2);
}fn gives_ownership() -> String {let some_string = String::from("hello");some_string
}fn takes_and_gives_back(a_string:String) -> String {a_string
}
变量的所有权遵循同样的模式
- 把值赋给其他变量时会发生移动
- 包含 heap 的数据的变量离开作用域时,它的值会被 drop 清理掉,除非数据的所有权已经转移
如何让函数使用某个值,但不获得所有权
fn main() {let s1 = String::from("hello");let (s2, len) = calculate_length(s1);println!("The length of '{}' is {}.",s2, len);
}fn calculate_length(s:String) -> (String, usize) {let length = s.len();(s, length)
}
(2)引用与借用
参数的类型是 &String 而不是 String
fn main() {let s1 = String::from("hello");let len = calculate_length(&s1);println!("The length of '{}' is {}.",s1, len);
}fn calculate_length(s:&String) -> usize {s.len()
}
& 符号表示引用:允许引用某些值而不取得其所有权
借用
- 把引用作为函数参数这个行为叫做借用
- 借用的内容无法更改,和变量一样,引用默认不可变
可变引用
fn main() {let mut s1 = String::from("hello");let len = calculate_length(&mut s1);println!("The length of '{}' is {}.",s1,len);
}fn calculate_length(s:&mut String) -> usize {s.push_str(",world");s.len()
}
可变引用的限制:
- 在特定作用域内,对某一块数据,只能有一个可变的引用
可变引用的好处: - 编译时,防止数据竞争
数据竞争的产生条件: - 两个或多个指针同时访问同一个数据
- 至少有一个指针用于写入数据
- 没有使用任何机制来同步对数据的访问
可以通过创建新的作用域,来允许非同时的创建多个可变引用
另一个限制 - 不可以同时拥有一个可变引用和一个不可变引用
- 多个不变的引用是可以的
例:
fn main() {let mut s = String::from("Hello");let r1 = &s;let r2 = &s;let s1 = &mut s;println!("{} {} {}",r1,r2,s1)
}
结果:
悬空指针 Dangling References
概念:一个指针引用了内存中的某个地址,但是这块内存可能已经释放并分配给其他人使用了
Rust中,编译器可以保证引用永远都不是悬空引用
- 如果引用了某些数据,编译器将保证在离开作用域前,数据不会离开作用域
fn main() {let r = dangle();
}fn dangle() -> &String { //悬空指针let s = String::from("hello");&s
}
结果:
(3)切片
Rust 的另外一种不持有所有权的数据类型:切片(slice)
例题,编写一个函数:
- 它接收字符串作为参数
- 返回它在这个字符串里找到的第一个单词
- 如果函数没找到任何空格,那么整个字符串就被返回
例:
fn main() {let mut s = String::from("Hello world");let wordIndex = first_word(&s);println!("{}", wordIndex);
}fn first_word(s:&String) -> usize {let bytes = s.as_bytes();//将String类型转换成数组for (i,&item) in bytes.iter().enumerate() {// .iter:返回集合中的每个元素// .enumerate():将 iter 返回的结果进行包装,并将结果作为元组的一部分进行返回if item == b' ' {return i;}}s.len()
}
字符串切片
字符串切片是指向字符串中一部分内容的引用
fn main() {let s = String::from("Hello world");//输出 hellolet hello = &s[0..5];//let hello = &s[..5];//输出 worldlet world = &s[0..11];//let world = &s[0..s.len()];//let world = &s[0..];//输出全部let all = &s[0..11]//let all = &s[0..s.len()]//let all = &s[..]
}
形式:[开始索引…结束索引] (左闭右开)
- 开始索引就是切片的起始位置的索引
- 结束索引是切片终止位置的下一个索引值
注: - 字符串切片的范围索引必须发生在有效的 UTF-8 字符边界内
- 如果尝试从一个多字节的字符中创建字符串切片,程序会报错退出
通过切片重写例题
fn main() {let mut s = String::from("Hello world");let wordIndex = first_word(&s);println!("{}", wordIndex);
}fn first_word(s:&String) -> &str {// &str : 字符串切片的类型let bytes = s.as_bytes();//将String类型转换成数组for (i,&item) in bytes.iter().enumerate() {// .iter:返回集合中的每个元素// .enumerate():将 iter 返回的结果进行包装,并将结果作为元组的一部分进行返回if item == b' ' {return &s[..i];}}&s[..]
}
字符串字面值是切片
fn main() {let s = "Hello world";println!("{}", s);
}
字符串字面值被直接存储在二进制程序中
let s = "Hello,World!";
变量 s 的类型是 &str ,它是一个指向二进制程序特定位置的切片
- &str 是不可变引用,所以字符串字面值是不可变的
字符串切片作为参数传递
fn first_word(s:&String) -> &str{}
也可以将 &str 作为参数类型,这样可以同时接收 String 和 &str 类型的参数
fn first_word(s:&str) -> &str{}
- 使用字符串切片,直接调用该函数
- 使用String,可以创建一个完整的 String 切片来调用该函数
定义函数时使用字符串切片代替字符串引用会使API更加通用,且不损失任何功能
fn main() {let my_string = String::from("Hello world");let wordIndex = first_world(&my_string[..]);let my_string_literal = "hello world";let wordIndex = first_world(my_string_literal);
}fn first_world(s:&str) -> &str {let bytes = s.as_bytes();for (i, &item) in bytes.iter().enumerate() {if item == b' ' {return &s[..i];}}&s[..]
}
列表切片
fn main() {let a = [1, 2, 3, 4, 5];let slice = &a[1..3]// &[i32] 类型切片
}