通过一猜数游戏代码开发过程的学习,我们完成了猜数游戏的程序编写,但是对一些代码的语法细节却没有展开讲解。本章节将对猜数游戏涉及的Rust基础语法进行一次梳理。
1、变量声明
Rust局部变量(可以理解为声明在函数内部的变量)声明的形式是:
let mut(可选) 变量名称:数据类型(可选) = 变量值;
其中mut变量说明该变量的值可以重新绑定,而没有mut时,表示该变量值只能绑定一次,否则编译异常,如下代码所示:
fn main() {
let x = 1; //1
x = 2; //2
let mut y: i32 = 0;//3
y = 123;//4
let x = y + 1;//5
}
第一行:声明变量x,类型为i32(通过=右侧的值可以推断x的数据类型为i32因此: i32数据类型声明省略,与语句let x: i32 = 1;完全相同;
第二行:再次对x绑定新值,编译出错,因为该变量未使用mut关键字修饰;
第三行:声明变量y为mut并绑定初始值为0;
第四行:再次对y绑定新值,因为有mut关键字修饰,表示这个变量可以多次绑定值,正常编译;
第五行:再次声明变量x,与第一行声明变量x重名,这时在第一行声明的x变量将不再有效。
2、常量声明
声明常量使用关键字const替换let,其他形式相同:
const TEST_VALUE: i32 = 1;
const修饰的常量,必须声明数据类型(例如本例中的: i32就是声明常量的代码)并同时绑定常量值。let和const都可以声明常量,两者的区别如下:
对比项 | const | let |
---|---|---|
声明时必须绑定值 | 是 | 否 |
声明时必须声明数据类型 | 是 | 否 |
可定义相同名称的变量或常量 | 否 | 是 |
在实际开发中,开发人员根据实际需要确定使用哪个关键字。
3、数据类型
Rust是静态类型(statically typed)语言, 也就是说在编译时就必须知道所有变量的类型。总的来说,Rust数据类型分为数值型、bool型、元组类等。
数值型
数据类型 | 长度(字节数) | 取值范围 | 备注 |
---|---|---|---|
i8 | 8 | 从-128到127 | 有符号 |
i16 | 16 | 从-32768到32767 | 有符号 |
i32 | 32 | 从-2147483648到2147483647 | 有符号 |
i64 | 64 | 从-9223372036854775808到9223372036854775807 | 有符号 |
i128 | 128 | 有符号 | |
isize | 依赖与计算机架构。64 位架构上它们是 64 位的, 32位架构上它们是 32 位的 | ||
u8 | 8 | 从0到255 | 无符号 |
u16 | 16 | 从0到65535 | 无符号 |
u32 | 32 | 从0到4294967295 | 无符号 |
u64 | 64 | 无符号 | |
u128 | 128 | 无符号 | |
usize | 依赖于计算机架构。64 位架构上它们是 64 位的, 32位架构上它们是 32 位的 | ||
f32 | 32 | ||
f64 | 64 | 默认f64,例如:let x = 2.0;等同于let x: f64 = 2.0 |
bool型
bool型取值只有true或者false二者之一。
元组类型
元组是一种将多个不同类型的值组合进一个复合类型的主要方式。 元组长度固定: 一旦声明, 其长度不会增大或缩小。我们使用包含在圆括号中的逗号分隔的值列表来创建一个元组。 元组中的每一个位置都有一个类型, 而且这些不同值的类型也不必是相同的。 如下代码所示:
fn main() {
let tup: (i32, f64, u8) = (500, 6.4, 1);//定义元组
let (x, y, z) = tup;//解构元组
println!("{}", y);
let five_hundred = tup.0;//通过索引访问元组中的数据
let six_point_four = tup.1;
let one = tup.2;
println!("打印数据:{}, {}, {}", five_hundred, six_point_four, one);
}
数组类型
另一个包含多个值的方式是 数组( array) 。 与元组不同, 数组中的每个元素的类型必须相同。 Rust 中的数组与一些其他语言中的数组不同, 因为 Rust 中的数组是固定长度的: 一旦声明, 它们的长度不能增长或缩小。数组使用示例:
fn main() {
let a = [1, 2, 3, 4, 5];//等价于let a: [i32; 5] = [1, 2, 3, 4, 5];
let first = a[0]; //通过下标访问元素
let second = a[1];
for one in a { //遍历数组元素
println!("{}", one)
}
}
字符串
字符串类型是现代编程语言的重要数据类型,C/C++语言之所以被认为比较难学习,在C/C++语言中要操作字符串会非常的复杂也可能是原因之一。Rust语言中字符串有两种表现形式,一种是基本类型,表示字符串的切片,以&str表示;另一种是可变的String类型。
fn main() {
let s1 = "Hello,String";//定义字符串切片
println!("{}",s1.len());//输出字符串长度
let s2 = "Hello,String".to_string(); //定义可变字符串
//println!("{}", s1.eq(s2));//编译报错,字符串切片和可变字符串是不同的数据类型,不能比较
println!("{}", s1.eq(s2.as_str()));//字符串切片比较
println!("{}", s1.to_string().eq(&s2));//可变字符串比较,记住这里&s2的写法,这个将在后续的完整中解释
}
4、函数
通过重构猜数游戏的代码,我们可以看到函数就是将一段特定功能的代码封装到函数中,这样可大大降低主流业务逻辑的代码复杂度,使得程序更加便于维护。Rust中函数定义模式代码如下:
fn 函数名称(参数1: 数据类型, 参数2: 数据类型……) -> 返回数据类型 {
//函数体
}
如果函数定义在模块(mod)中,关键字fn前面还可以使用pub关键字表明其他模块是否有权限调用这个函数。其中:函数参数和返回数据类型根据实际场景可以省略。
除了函数定义,还需要理解函数调用,函数调用模式代码如下:
let result_value = 函数名称(参数1, 参数2……);
函数调用时,没有fn关键字。如果调用的函数有返回值,则一般用let关键字声明一个变量来保存函数调用后的结果。
函数必须先定义,后调用。
fn main() {
let secret_num = generate_secret_num();//调用生成秘密数字的函数,将秘密函数保存到secret_num中备用
//……
}
fn generate_secret_num() -> u32 { //定义生成秘密函数的函数,该函数返回u32类型的数据
let mut rng = thread_rng();//生成ThreadRng struct的实例
rng.gen_range(0..101) //调用ThreadRng实例rng的成员函数gen_range生成随机数。没有return关键字和分号;,表达式的值就是函数的返回值
}
5、程序流程控制
流程控制分为选择和两种。
if语句是当条件满足时,执行某一段代码,不满足时则不执行这段代码。例如猜数游戏中的一段代码:
if input_num < secret_num {//如果输入值小于秘密数字,则将变量result_str的值绑定为“太小了”字符串切片
result_str = "太小了!";
} else if input_num > secret_num {//如果输入值不小于秘密数字且输入值大于秘密数字,则将变量result_str的值绑定为“太大了”字符串切片
result_str = "太大了!";
} else {//输入值等于秘密数字(等同于:输入值不小于秘密数字且输入值不大于秘密数字)
result_str = "恭喜您,猜对了!";
is_equ_flag = true;
}
注意这里else if,是先进行input_num < secret_num判断,如果input_num < secret_num条件不成立,再进行input_num > secret_num条件判断。最后的else是input_num < secret_num和input_num > secret_num两个条件都不成立时,才执行,两个条件都不成立,可不就等同于input_num == secret_num了吗?对于两个数字之间的关系,不就是要么大于,要么小于,要么等于这三种情况之一吗?
Rust中没有其他大部分语言都支持的switch语句,而是采用了match语句,和switch语句差不多,但是match语句没有switch缺少break关键字而引起程序逻辑错误的坑。我们可以采用了match改写猜数游戏中的关键逻辑:
use std::cmp::Ordering;//引入数字比较结果的枚举,有三种关系:大于、小于及等于
//……
fn check_input(secret_num: u32, input_num: u32, records: &mut Vec<String>) -> bool {
let mut is_equ_flag = false;
let mut result_str = "";
match input_num.cmp(&secret_num) {
Ordering::Less => result_str = "太小了",//input_num < secret_num条件成立时执行,没有break关键字
Ordering::Greater => result_str = "太大了",//input_num > secret_num条件成立时执行
Ordering::Equal => {//input_num == secret_num条件成立时执行,因为有多条语句需要执行,需要用{}包裹
result_str = "恭喜您,猜对了!";
is_equ_flag = true;
}//Ordering::Equal结束
}
let now_time = Local::now();
let now_str = now_time.format("%Y-%m-%d %H:%M:%S");
let one_record = format!("时间:{},输入值:{},结果:{}", now_str, input_num, result_str);
records.push(one_record);//6
print_info(result_str);
is_equ_flag
}
循环,是重复执行一段代码。例如猜数游戏中的代码:
loop {//循环开始
print_info("请输入0到100的数字:");
let input_num = get_input_num();
if check_input(secret_num, input_num, &mut his_records) { //函数check_input返回值为true则执行break
break;//中断循环,或者叫循环出口
}//函数check_input返回false,则跳转到循环开始处继续运行
}
该循环语句等同于:
loop {//循环开始
print_info("请输入0到100的数字:");
let input_num = get_input_num();
if check_input(secret_num, input_num, &mut his_records) {
break;
}
continue;//函数check_input返回false,则跳转到循环开始处继续运行
}
这个continue也是一个关键字,当循环中执行到continue语句时,将跳转到循环开始处执行,而continue之后的语句则不会被执行。
for循环语句:
for i in 1..5 {
println!("{}", i);
}
运行程序后,会输出1、2、3、4。1..5时一个序列表达式,会生成1、2、3、4这个序列。可以将序列理解成数组,每个元素的值分别是:1、2、3、4。
for循环一般用于具有固定循环次数的场景。同样在for循环中也可以使用break关键字终止循环,continue语句继续下一次循环。
while条件循环语句中的条件表达式与if中的条件表达式相同,就是条件成立时,则执行循环体中的代码,例如:
let mut number = 3;
while number != 0 {
println!("{}!", number);
number = number - 1;
}
println!("LIFTOFF!!!");
循环开始时,number的值是3,number != 0条件成立,所以先输出了3!,然后将3-1的值绑定number,程序跳转到while处,判断条件是否成立……,所以最后程序将输出3!、2!、1!、LIFTOFF!!!
可以将猜数游戏中的loop改写为while循环:
let mut stop_loop_flag = false;
while stop_loop_flag == false {//当终止循环标志为false,则循环
print_info("请输入0到100的数字:");
let input_num = get_input_num();//获取用户输入
stop_loop_flag = check_input(secret_num, input_num, &mut his_records);//输入值与秘密数字相等,函数返回true,跳转到while语句是,条件不成立,不再循环while中的语句
}
6、模块
模块是Rust中代码组织形式,与C++的命名空间、Java的包类似,模块是Rust分割一个大型应用的方式,目的是让每个文件完成的功能单一,这样文件中的代码行数就比较少,便于开发人员在应用需求变更时能够更快速的修改相关文件。一般同一个模块的所有文件都包含在一个目录下,这个目录下必须有mod.rs文件,mod.rs中声明了所有本模块所使用的子模块及文件。猜数游戏中的history模块的mod.rs文件定义如下:
//声明本模块中包含的所有文件(与mod.rs文件同级目录下)
mod show_his_records;
mod write_record_to_file;
mod read_record_from_file;
//对其他模块公布本模块可调用的函数,这里不仅仅时函数,还可以是struct等其他元素
pub use show_his_records::show_his_records;
pub use write_record_to_file::write_record_to_file;
pub use read_record_from_file::read_record_from_file;
以上是模块的定义。在调用模块的文件中,还需要在文件头部用mod关键字和use关键字,引入需要的内容:
//main.rs文件内容
mod history;//声明使用history模块
use history::{show_his_records, write_record_to_file, read_record_from_file};//使用use关键字声明调用的history模块中的函数
fn main() {
//……
//调用history模块中的函数,完成对应功能
show_his_records(&mut his_records);
write_record_to_file(&mut his_records);
read_record_from_file();
}
模块切分,可以从开发人员的角度来看,是大型程序的一种组织源码组织形式,目的是便于程序的维护,和后续面向对象的程序设计类似。