今天在Rust学习群中看到一个提问:
这是一个入门的猜数程序,通过输入数字来猜测一个随机数,如果猜对了就结束程序,如果猜错了就继续猜,直到猜对为止。
问题分析
从代码来看,程序发生了崩溃,很多rust
初学者在学习的过程中,会遇到第一个经典错误:
io::stdin().read_line(buf:&mut num).expect(msg:"read Err");
num.parse().expect("u32 Err");
因为控制台输入回车的时候,意味这提交了一个换行符,这个换行符会被read_line
读取到num
中,比如你输入1
,实际上num
中的值是1\n
,这个时候parse
就会报错,因为parse
无法解析这个换行符。
一开始我以为是这个错误,刚想说,这个错误太简单了,改成:
- num.parse().expect("u32 Err");
+ num.trim().parse().expect("u32 Err");
把\n
去掉就行了,刚想发出去,群里有人说:“是不是变量遮蔽” ?仔细一看,好像不是那么一回事,因为提问的代码中已经处理了.trim()
为了方便理解,我把代码贴出来:
use std::io;
use rand::Rng;
use std::cmp::Ordering;
fn main(){
println!("Welcome guessing number!"))
println!("请输入一个数")
let mut num:String = String::new();
let rand_num: i32 = rand::thread_rng().gen_range(low:1 ,high:11);
println!("这个随机数字是:{}",rand_num);
loop {
io::stdin().read_line(buf:&mut num).expect(msg:"read Err");
println!("你输入的数字是{}",num);
let num:i32=num.trim().parse().expect(msg:"u32 Err");
match num.cmp(&rand num){
Ordering::Equal => print!("nihao")
Ordering::Less=> print!("little")
Ordering::Greater=>print!("great")
}
}
}
在代码的第8行,定义了一个String
类型的num
, 然后在15行重新定义了i32
的num, 提问的人也说是第二次输入才会出现crash。
那么是不是因为变量遮蔽导致的问题呢?
变量遮蔽
在rust
中,可以通过let
关键字重新定义一个变量,这个时候,新定义的变量会遮蔽之前的变量,这个时候,之前的变量就无法访问了,这个时候,如果之前的变量还在使用,就会出现问题。
那么在loop循环中,这种遮蔽规则是怎么样的呢? 其实并不会,因为rust
是静态类型语言,同样的代码在python, js中确实是第二次循环的时候,num就变成了i32
了:
num = "1"
for i in range(2):
assert type(num) == str
num = int(num)
很不幸,第二次运行的时候, num的类型就不是str了
Traceback (most recent call last):
File "<stdin>", line 2, in <module>
AssertionError
回来看rust的代码,第二次循环的时候,在第15行的时候,num仍然还是String类型, 并不会因为17行的重新定义而改变类型,所以这个时候,代码并不会出现问题。
因为在rust
中,变量的作用域是严格被约束的,i32
的生命周期在17-22行之间,在8-16之间的生命周期还是属于String
类型的num
。
loop的每次循环,其实和第一遍的代码是完全一样的行为,因为静态语言和动态语言并不一样,并不会在运行时改变变量的类型。
可以理解为,rust
是一种静态类型语言,变量的类型是在编译时确定的,而不是在运行时确定的。
真正的问题是什么?
其实群里就有人第一时间指出了问题,是因为read_line
的append行为导致的,通过查看read_line
的文档,我们可以看到read_line
的定义是这样的:
/// Locks this handle and reads a line of input, appending it to the specified buffer.
pub fn read_line(&self, buf: &mut String) -> io::Result<usize> {
...
之所以在输入第二次的时候出现crash,是因为在代码的第15行,read_line
会把输入的内容追加到num
中,这个时候,num
的内容就变成了1\n2\n
,虽然调用了trim.()
,但是trim只会去掉首尾的空格,而不会去掉中间的换行符,所以parse
的时候就会出现问题。
解决这个问题的方法很简单,改变一下num
的声明顺序保证每次 num 都是空的。
将第8行的代码改到第14行就可以了。
use std::io;
use rand::Rng;
use std::cmp::Ordering;
fn main(){
println!("Welcome guessing number!"))
println!("请输入一个数")
let rand_num: i32 = rand::thread_rng().gen_range(low:1 ,high:11);
println!("这个随机数字是:{}",rand_num);
loop {
let mut num:String = String::new();
io::stdin().read_line(buf:&mut num).expect(msg:"read Err");
println!("你输入的数字是{}",num);
let num:i32=num.trim().parse().expect(msg:"u32 Err");
match num.cmp(&rand num){
Ordering::Equal => print!("nihao")
Ordering::Less=> print!("little")
Ordering::Greater=>print!("great")
}
}
}
欢迎大家关注添加我的微信交流学习编程: jinti2000, 也可以关注公众号 入职啦(ruzhila)
访问官网 https://www.ruzhila.cn 获取更多学习资源
Ext Link: https://ruzhila.cn/?from=rustcc
评论区
写评论和生命周期没关系,你可以说是学习Stdin::read_line或String的用法。。。
当然这个跟“Rust变量生命周期”也没啥关系,因为其他语言也会出现,你懂的,只要你这样去写代码,都会遇到这个问题
一般这个问题只会出现在新手时候,老手一般不会出现这样的代码!肯定会直接就写对了!根本不会给bug代码留下机会