前言:
这是一个系列的学习笔记,会将笔者学习Rust语言的心得记录。
当然,这并非是流水账似的记录,而是结合实际程序项目的记录,如果你也对Rust感兴趣,那么我们可以一起交流探讨,使用Rust来构建程序。
注:本文中使用Rust都是在windows环境下,如果是macOS或者linux,其指令或有不同,请注意。
系列第一篇:Rust学习日记(一)Cargo的使用
概述:
这是Rust学习笔记的第二篇,主要说一下Rust中的变量,本文将结合两个实例来说明,这两个实例分别是:
1、在华氏度和摄氏度之间转换温度
2、生成指定个数(n)的斐波那契数列
如果你看过rust的官方手册,那你会熟悉,这两个例子事实上就是官方留的“课后习题”
在进入实例之前,还是要先来简单看看变量的使用。
变量的申明
1、关键词let
在rust中,申明变量的关键词就是let。如果你使用过其他语言,那么你可能对rust的变量申明有疑问,为什么用let?
我觉得不用纠结,这就是一个定义,使用什么单词来作为关键词并不重要,重要的是,这个关键词的作用。
fn main() {let x=5;println!("x is {x}");
}
上面的示例程序段,是在主函数中创建了变量x,并打印,在终端输入cargo run,输出如下:
PS D:\008 rustpro\var> cargo runFinished dev [unoptimized + debuginfo] target(s) in 0.02sRunning `target\debug\var.exe`
x is 5
2、变量的可变与不可变
变量,顾名思义,就是可变化的量,不可变的是变量吗?不可变的应该是常量吧?
在rust中,默认情况下,变量是不可变的。
这是rust的设计,它的目的,当然是为了安全。“安全”是贯穿rust整个体系的,后面会有更多更复杂更高级的功能,都会和它有关。
fn main() {let x=5;println!("x is {x}");x=x+1;println!("x is {x}");
}
对前面的函数稍作修改,给变量x再次赋值,再看输出:
PS D:\008 rustpro\var> cargo runCompiling var v0.1.0 (D:\008 rustpro\var)
error[E0384]: cannot assign twice to immutable variable `x`--> src\main.rs:4:5|
2 | let x=5;| -| || first assignment to `x`| help: consider making this binding mutable: `mut x`
3 | println!("x is {x}");
4 | x=x+1;| ^^^^^ cannot assign twice to immutable variableFor more information about this error, try `rustc --explain E0384`.
error: could not compile `var` (bin "var") due to previous error
错误提示,cannot assign twice to immutable variable,不能给“immutable variable”分配两次,什么叫immutable variable?就是不可变变量。
所以,现在你就知道,rust是比较奇葩的了。
但是,这就是rust的设计,如果你定义变量的时候,只是简单的let x=5;那么这个x是不可变的。那如果要可变呢:
let mut x=5;
再加一个关键词mut,mut其实就是mutable的简写,可变的。
fn main() {let mut x=5;println!("x is {x}");x=x+1;println!("x is {x}");
}
cargo run:
PS D:\008 rustpro\var> cargo runCompiling var v0.1.0 (D:\008 rustpro\var)Finished dev [unoptimized + debuginfo] target(s) in 0.43sRunning `target\debug\var.exe`
x is 5
x is 6
可以发现,“变量x”的值可以多次分配,也就是真正的可变了。
看到这里你发现什么?
你会发现,在rust里,你对一个对象,比如变量的定义,必须要十分清楚,可变就可变,不可变就不可变,
这样一来,rust编译器就能非常清楚地知道,你定义这个变量的目的,它会编译过程对你的意图全程跟踪,
不可变变量如果不小心让它变化,编译器就会报错,提示你,对你来说,这就是安全。
你不用等程序都运行了,然后偶尔崩溃了一次,然后去找bug,找来找去无论怎么编译就是没问题,但一运行就报错,这是令人崩溃的。
你已经了解到rust中变量的奇葩性了,我们接着再看几个,然后再进入实例。
3、常量
rust中常量用const来申明,这个const应该是比较熟悉的单词了,很多编程语言申明常量都是用它吧。
const也就是Constants的简写。
看一下申明的格式吧:
const C_1=5;
如果你真的这么写,cargo check一下,会报错:
PS D:\008 rustpro\var> cargo checkChecking var v0.1.0 (D:\008 rustpro\var)
error: missing type for `const` item--> src\main.rs:6:14|
6 | const C_1=5;| ^ help: provide a type for the constant: `: i32`error: could not compile `var` (bin "var") due to previous error
rust编译器提示你,需要给常量提供一个数据类型,并且给了你一个选项:i32。
rust中的数据类型----整数型:
关于数据类型,其实我认为不用多说,照着手册看看了解一下即可,因为一般来说,对编程有所了解的,对数据类型一般都不陌生,可以说,几乎所有语言中的数据类型,都大同小异。
常见的比如说整数、浮点数、布尔量、字符串等,还有一些复杂的,比如结构体(复合体)、数组、列表、元组等。
我们接着说常量,上面说这样写const C_1=5;会报错,原因是未给常量提供数据类型,所以我们应该这么来申明常量
const C_1:i32=5;
rust就是这样,它要求你明确,哪怕是常量,它不会为你隐式转换,它要求你定义常量时,明确常量的数据类型。
到此,你会发现,rust似乎很“麻烦”,这也要明确,那也要明确,为什么不能像其他语言一样,直接a=5;或者const a=5;
我只能说,rust它就是这样一门语言,如果你对此实在不能接受,那么放弃是好的,如果无所谓,那么就按照rust的规则,暂时先走下去。
4、变量的覆盖
我们在本文前面说过,rust中的变量有可变和不可变的区别,直接申明变量,默认不可变,如果要可变,需加关键词mut。
但现在我们来看另一种使用:
fn main() {let x=5;println!("x is {x}");let x:char='a';println!("x is {x}");
}
我们在第一行定义x变量,且赋值为5,但没有用mut使其可变,所以x是不可变的,但是我们在第三行再次使用x变量,而且给它赋值为字符‘a’。
我们先cargo run一下:
PS D:\008 rustpro\var> cargo runCompiling var v0.1.0 (D:\008 rustpro\var)Finished dev [unoptimized + debuginfo] target(s) in 0.63sRunning `target\debug\var.exe`
x is 5
x is a
可以看到,rust正常编译,且结果x的值被改变了。你应该看到区别,就是第三行再次使用x时,重新定义了一次。
rust中,用let来定义变量,是可以重名的,并且后一个可以覆盖前面的,所谓“shadow”,即旧的变量活在新的变量的阴影里。
你可能会觉得,这样是不是会造成困扰,如果不小心使用了一样的变量名,覆盖之前的变量数据,岂不是会影响程序。但这个问题容易解决,只需要你在命名变量时有自己的规律即可,但是rust中这样的机制会带来好处。
我们看到,同样的变量x,在第一次和第二次的定义中,不仅值可以不同,连数据类型也可以不同。
什么场景会这样使用,比如你要输入一个值,是字符类型,但程序接收字符后,要以整数型来运算,但是这两个其实一个东西,如果是其他语言,你可能会这样定义变量,x_str和x_int,但是rust中你可以只定义一个x,
let x:char='a';
let x:i32=5;
好了,关于变量就先说到这,基本上差不多了,当然这并非是说把变量的所有方面都涉及到了,只是说基本的概念都已经了解了。
下面将开始实例:
一、温度转换
1、新建一个temp项目
PS D:\008 rustpro> cargo new tempCreated binary (application) `temp` package
PS D:\008 rustpro> cd temp
PS D:\008 rustpro\temp> cargo buildCompiling temp v0.1.0 (D:\008 rustpro\temp)Finished dev [unoptimized + debuginfo] target(s) in 0.49s
PS D:\008 rustpro\temp>
新建项目后,rust会自动生成一个main.rs主函数。
这个主函数里默认的是打印“hello world”,我们要修改它,以达到温度转换的功能。
华氏度和摄氏度的转换关系如下:
1 摄氏度=33.8 华氏度
所以,如果我们输入一个摄氏度值为x,那么转换为华氏度应该是:f(x)=33.8*x
数学关系很清楚,是很简单的一元一次函数。
先看一下main.rs的结构:
fn main() {println!("Hello, world!");
}
fn和let一样,是关键词,定义函数,所以fn后面的main就是函数,只不过它是主函数,所以地位特殊,但是,main这个名字,想必大家也不陌生,无论是C还是python,都有主函数,地位也差不多,谁让它是main呢!
但我们先不修改main,我们先新建一个函数,就叫temp_convert:
fn temp_convert(){}
添加逻辑代码:
fn temp_convert(t:f32)->f32{let t2=t*33.8;return t2;
}
可以看到,我们在新建的函数基础上,添加了一个形式参数t,把它的数据类型定义为f32,即浮点数。
定义了一个函数返回值类型:->f32,即函数返回值也是f32类型。
然后在函数体内,定义了变量t2,其赋值为t*33.8,
最后将计算后的t2的值返回。
这个函数中形式参数t即输入的摄氏度,返回的t2即转换的华氏度。
接下来,就是在主程序里调用了:
修改一下main函数:
fn main() {let t1=temp_convert(3.0);println!("t_f is {t1}");
}
因为函数temp_convert有返回值,所以我们定义一个变量t1来储存它。
到此,简单的温度转换就实现了,只要给函数的参数赋予不同的值(摄氏度),函数就可以返回华氏度:
PS D:\008 rustpro\temp> cargo runCompiling temp v0.1.0 (D:\008 rustpro\temp)Finished dev [unoptimized + debuginfo] target(s) in 0.42sRunning `target\debug\temp.exe`
t_f is 101.399994
但这样肯定不算是一个完整的程序,我们设想应该是这样,程序启动,会输出一个文字提示,让我们输入一个摄氏度,我们输入后,程序输出一个华氏度。然后能循环,这样才算是完整的转换程序。
我们分两部分来说,一是如何接受用户输入,二是如何循环。
1 用户输入
用户输入,需要用到IO接口,在rust中即std::io。
std::io,这是一个程序包crate,类似于其他程序的模块。在使用它之前,需要先导入它。
use std::io;
然后使用它:
use std::io;
fn main() {let mut t_in=String::new();println!("请输入摄氏度值:");io::stdin().read_line(&mut t_in).expect("error");let t_in:f32=t_in.trim().parse().expect("error");let t1=temp_convert(t_in);println!("华氏度是 {t1}");
}
上面的主函数main变化较大,我们一一来说一下。我们使用use关键词导入了std::io,这是用来接受用户输入的模块。
然后在主函数体内添加程序:
let mut t_in=String::new();
这行是定义了一个可变变量t_in且类型为string。
io::stdin().read_line(&mut t_in).expect("error");
这行就是io接口的调用,用于读取用户的输入,read_line即是std::io模块的函数。&mut t_in是将我们定义的变量t_in来接受read_line的输入值。
其实上面这行直接写如下:
io::stdin().read_line(&mut t_in).expect("error");
之所以写成多行样式,是为了便于阅读。
这里的expect是对潜在错误的处理,关于错误的处理不在本文的范围,后续会有专门叙述。此处只要知道这样用就可以了。
我们现在已经从用户处接受到输入值,即t_in,接下来要对输入值进行处理:
let t_in:f32=t_in.trim().parse().expect("error");
此处我们使用前面介绍的变量覆盖,先定义了t_in为用户输入,且数据类型为string类型,接着此行用同名变量t_in定义为f32类型,覆盖之前的字符串类型。
其中的trim是消除一个字符串的开头和结尾的多余空格,parse是将字符串转为另一种数据类型,在此处就是转为f32类型。expect我们已经说过,是对返回值的判断时可能发生的错误处理。
`let t1=temp_convert(t_in);
在顺利获得用户输入的值且转为浮点类型后,我们调用写好的temp_convert转换函数,其形式参数t在调用时传入实际参数t_in。将temp_convert函数的返回值,传给变量t1。
完整程序:
use std::io;
fn main() {let mut t_in=String::new();println!("请输入摄氏度值:");io::stdin().read_line(&mut t_in).expect("error");let t_in:f32=t_in.trim().parse().expect("error");let t1=temp_convert(t_in);println!("华氏度是 {t1}");
}
fn temp_convert(t:f32)->f32{let t2=t*33.8;return t2;
}
在终端运行cargo run:
PS D:\008 rustpro\temp> cargo runCompiling temp v0.1.0 (D:\008 rustpro\temp)Finished dev [unoptimized + debuginfo] target(s) in 0.74sRunning `target\debug\temp.exe`
请输入摄氏度值:
2
华氏度是 67.6
好了,第一部分完成了,现在我们能成功根据输入的摄氏度转为华氏度且输出,但缺点是每次运行只能进行一次,所以需要循环。
循环
想让程序循环起来,也很简单,使用loop指令,它会循环执行主程序的逻辑,直到触发停止。本文中未添加明确的停止程序,因为这不在我们的学习范围,后续会有说明,此文不赘述。
fn main() {loop{let mut t_in=String::new();println!("请输入摄氏度值:");io::stdin().read_line(&mut t_in).expect("error");let t_in:f32=t_in.trim().parse().expect("error");let t1=temp_convert(t_in);println!("华氏度是 {t1}");}}
我们可以看到,只需要将main中的原程序添加进loop的{}中即可。
让我们运行一下:
PS D:\008 rustpro\temp> cargo runCompiling temp v0.1.0 (D:\008 rustpro\temp)Finished dev [unoptimized + debuginfo] target(s) in 0.46sRunning `target\debug\temp.exe`
请输入摄氏度值:
2
华氏度是 67.6
请输入摄氏度值:
3
华氏度是 101.399994
请输入摄氏度值:
4
华氏度是 135.2
请输入摄氏度值:
1
华氏度是 33.8
请输入摄氏度值:
2
华氏度是 67.6
请输入摄氏度值:
r
thread 'main' panicked at src\main.rs:10:42:
error: ParseFloatError { kind: Invalid }
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
error: process didn't exit successfully: `target\debug\temp.exe` (exit code: 101)
可以看到,程序在不停地执行,如果用户一直输入正确的值得话,但你会发现在终端的记录中,最后输入r时,rust报错且退出了,这是因为程序中没有对非法输入的应对措施,当输入非法值时,只能报错退出。
但我不准备在本文中完善它,我相信随着我们学习rust的深入,这个问题会迎刃而解。
如果你看到这里,我相信你对rust中变量、函数的使用,有了初步的印象了。
下面我们说一下第二个实例,即实现斐波那契数列。
斐波那契数列
斐波那契数列应该是比较经典的练手题目了,你在学习其他语言如C、python、java中都可以通过编写斐波那契数列来加深对语言的理解。
rust也不例外,我在文章一开头就说过,此文所列举的两个实例,都是rust官方手册中给读者留的课后习题。
其实官方在手册的第二章给了一个很好的实例即猜谜游戏,说的非常详细,但我们作为学习者,在学习了官方手册后,自己要学会应用,所以我选择用课后习题来作为示例。
斐波那契数列是这样的数列:
1,1,2,3,5,8,13,21,34,55,89…
这个数列的规律是,从第三项开始,每一项的值都是它前面两项的和。用公式表示为:
f(n)=f(n-1)+f(n-2);n>2
所以,如果要用函数来实现斐波那契数列,函数肯定是递归的。
rust实现,首先是创建新项目,这个不用多说了。
接着创建新的函数,命名为fibonacci:
fn fibonacci(num:i32)->i32{if num<3{return 1;}else{return fibonacci(num-1)+fibonacci(num-2);}
}
这个fibonacci函数有个形式参数num,数据类型为i32,函数的返回值类型由’->'符号指出,也是i32。
函数体内的逻辑,是对num进行判断,如果num小于3,也就是fibonacci数列的前2项,那么函数返回值固定为1,如果num大于等于3,那么返回值就等于前2项之和。
此处的判断,使用了if。。else语句。if else应该不用多说了吧,这应该是学习编程的人,都会了解的。
几乎你学习任何编程语言,都会遇到if语句。
所以,本文也不会赘述if。。else语句。
接下来看main函数:
use std::io;
fn main() {loop{println!("please enter fibonacci number:");let mut number=String::new();io::stdin().read_line(&mut number).expect("err");let number:i32=number.trim().parse().expect("err");println!("number is {number}");for y in 1..number{let x=fibonacci(y);println!("fibo({y}) is {x}");}} }
可以看到,几乎和前面的实例一样,一点区别在于子函数fibonacci的调用处,这里用了for语句:
之所以使用for语句,因为斐波那契数列是一个数列,当我们给定一个斐波那契数列的项索引(n)时,我们实际上希望程序能够列出至n项的斐波那契数列所有的项。
for y in 1..number{let x=fibonacci(y);println!("fibo({y}) is {x}");}
同理,for循环也是常见的,和if语句一样,我们会在后续的文章里统一说明,此文不赘述。
我们还是看一下实际运行效果吧:
PS D:\008 rustpro\fibonacci> cargo runCompiling fibonacci v0.1.0 (D:\008 rustpro\fibonacci)Finished dev [unoptimized + debuginfo] target(s) in 0.71sRunning `target\debug\fibonacci.exe`
please enter fibonacci number:
2
number is 2
fibo(1) is 1
please enter fibonacci number:
5
number is 5
fibo(1) is 1
fibo(2) is 1
fibo(3) is 2
fibo(4) is 3
please enter fibonacci number:
10
number is 10
fibo(1) is 1
fibo(2) is 1
fibo(3) is 2
fibo(4) is 3
fibo(5) is 5
fibo(6) is 8
fibo(7) is 13
fibo(8) is 21
fibo(9) is 34
please enter fibonacci number:
r
thread 'main' panicked at src\main.rs:10:46:
err: ParseIntError { kind: InvalidDigit }
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
error: process didn't exit successfully: `target\debug\fibonacci.exe` (exit code: 101)
总结一下,rust变量:
let x=5;//x不可变
let mut x=5;//x可变
let x=5;
let x='a';//x依旧不可变,但可以被重名者覆盖,且可不同数据类型
const x:i32=5;//常量需指定数据类型