之前已经配置好了Rust的环境,那学习一门语言最开始就是去掌握了解它的基本语法.其实Rust的语法和大多编程语言没什么差别,熟悉C++的应该很容易上手,所以今天就快速过一遍基础.
1. 变量与常量
变量应该是编程中最常用到的,但是Rust与其他语言不同的是在声明变量的时候必须说明这个变量是否可变,变量默认是不可变的.
变量声明语句:let a;
,如果想声明可变变量: let mut a;
和不可变变量类似的—常量,这才是真正意义上的不可变,无法对常量使用mut,而且得使用const
关键字声明,并且常量往往是全大写字母命名.
还有一种特殊的用法,那就是利用let声明同名变量实现对之前变量的“隐藏”,而这种实际上是每次创建了一个新的变量.
fn main(){
let a=10;
let a=20;
let a="aaa";
println!("{}",a)
}
忽视掉未使用变量的警告,这段代码最终能通过编译,也实现了上面说的“隐藏”功能.
2. 数据类型
上面讲到了变量声明,有了变量那就涉及到变量所属的数据类型,所以紧接着来聊聊Rust里面的数据类型.
Rust是静态类型语言,不过借助编译器往往可以帮助我们推断出变量所属类型,如上面的代码中我并没有声明a所属的数据类型,但是依旧可以顺利编译.不过在一些特殊场景,如将命令行输入进行运算时,必须将字符串转为数值类型才能计算,而这个时候必须添加类型注解否则编译器并不能顺利推断应该转换的具体类型.Rust数据类型主要分为两类:标量类型与复合类型,其中标量类型和大多语言中基本数据类型相似,而Rust原生的复合类型只有元组和数组.为了方便日常编程,在标准库中包含了一系列封装的数据结构:vector,string,map
2.1 标量类型
具体包括整型,浮点型,布尔类型和字符型.这部分与其他语言类似,整型包括有符号整型和无符号整型,前缀用i和u表示.浮点型包括32位和64位浮点类型,默认是64位.
2.2 复合类型
原生复合类型包括元组
和数组
元组可以将不同类型的数据进行组合,但元组长度固定访问时通过.+索引
fn main(){
let tuple:(i32,f64,u32)=(-100,3.14,100);
let (x,_,_)=tuple;
println!("tuple={},{},{}",tuple.0,tuple.1,tuple.2);
println!("x={}",x);
}
数组中的元素必须是相同类型的,并且数组的长度也是固定的.但是对于习惯了使用更具灵活性的动态数组的开发者来说,Rust在标准库中也提供了vector,所以也许vector更加常用,而数组仅仅用于明确数据量的情况(如月份,星期等等)
题外话,数组分配空间在栈上而不是在堆上,这部分内容对于C++开发者应该比较熟悉,而栈中的数据访问效率是高于堆上的,减少了指针跳转访问过程.往往在某些实时性以及安全性要求高的场景,代码执行效率和开发难度上需要做平衡.
fn main(){
let months = ["January", "February", "March", "April", "May", "June", "July",
"August", "September", "October", "November", "December"];
for (index,month) in months.iter().enumerate(){
let index=index+1;
match index{
1=>println!("The {}st month is {}",index,month),
2=>println!("The {}nd month is {}",index,month),
3=>println!("The {}rd month is {}",index,month),
_=>println!("The {}th month is {}",index,month),
};
}
}
这个demo展示了数组的访问以及后续会讲到的for循环和match,这里就当个开胃小菜.
2.3 封装类型
2.3.1 vector
上面提到数组是不可变的,但是大多时候我们在初始化时并不知道数据量大小,而且后续可能还需要增加数据,所以这个时候就需要更加灵活的vector,当然vector同样只能存储相同的数据类型!!!
fn main(){
let mut v:Vec<i32>=Vec::new();
//let v=vec![1,2,3];
v.push(1);
v.push(2);
v.push(3);
v.push(4);
println!("{:?}",v);
for i in v.iter_mut(){
*i*=2;
}
println!("{:?}",v);
println!("{:?},{:?}",v[1],v.get(1));
println!("{:?}",v.pop());
}
如果想在vector中存储不同的类型,就需要对数据类型进行某些封装,例如用结构体,枚举或者trait对象.
2.3.2 string
string是由字节组成的vector,且是UTF-8编码.因此,我们可以看看下面这个demo
fn main(){
let mut s1 = String::from("this is a ");
let s2 = "star";
s1.push_str(s2);
println!("s1 is {},s2 is {}",s1,s2);
let star=String::from_utf8(vec![240,159,146,150]).unwrap();
s1+= ☆
println!("s1 is {}",s1);
}
我们不仅可以拼接字符串slice,还可以通过vector构建字符串进行拼接.
2.3.3 map
map也是一种常用的数据结构,通过键值对的方式实现关系映射.既然这么常用,基本概念就不过多介绍直接看demo
use std::collections::HashMap;
fn main(){
let mut demo_map=HashMap::new();
demo_map.insert(String::from("1"),"10");
demo_map.insert(String::from("2"),"20");
demo_map.insert(String::from("3"),"30");
println!("{:?}",demo_map);
let keys=vec![String::from("a"),String::from("b")];
let values=vec![111,222];
let map:HashMap<_,_>=keys.iter().zip(values.iter()).collect();
println!("{:?}",map);
//get
let query=String::from("a");
let result=map.get(&query);
println!("query's result: {:?}",result);
for (k,v) in &map{
println!("{}:{}",k,v);
}
//update
demo_map.insert(String::from("3"),"300");
println!("{:?}",demo_map);
//entry_insert
demo_map.entry(String::from("5")).or_insert("50");
let tmp=demo_map.entry(String::from("1")).or_insert("100");
let mut tmp_string=tmp.to_string();
tmp_string.push_str("01");
*tmp=&tmp_string[..];
println!("{:?}",demo_map);
demo_map.remove("2");
println!("{:?}",demo_map);
}
这个demo基本囊括了常见的map操作,如一开始的创建,插入,访问以及最后的remove删除键值对.其中具有特色的就是entry
,它可以获得map中的对应项,并进行原地操作,当访问的键不存在时可以插入新的键值对,如果存在将不会进行修改;同时or_insert
会返回这个键对应值的可变引用,因此我们也可以直接对这个引用进行操作从而修改原始map中的值.
3. 函数
了解C/C++的都知道,main函数是程序的入口,第一天的内容也提到了fn
作为关键字来进行函数声明,其他关于函数的内容与其他语言也没有太多差异还是比较容易接受的,这里主要来看看容易弄混的函数与表达式,语句之间的差别.
- 语句:执行一些操作但是没有返回值
- 表达式:计算并且产生返回值,一个数值本身就是表达式!!!
有的时候函数定义了返回值类型后,可以直接将最后的表达式作为返回值,不用return
关键字也能返回.不过一定要注意最后的是表达式而不是语句,否则老老实实用return也能减少很多错误.
4. 控制流
编程中最基本的三种结构:顺序,选择,循环,在上面的那些demo中从main函数开始一行行语句执行直到结束,这就是顺序结构;但是其中有match
关键字的选择结构,或者Rust中称呼的模式匹配;还有包含for语句的循环结构.上面的demo其实已经预先透露了这些控制流,这一小节就来详细介绍Rust某些特性.
4.1 if
if也是选择结构的一种,根据不同条件对应不同的操作.不过值得注意的是,Rust在if语句中的条件一定是一个布尔类型,类似于C++常用的语句if(1){...}
这在Rust中是错误的,必须改为if 1>0{...}
和大多数语言一样,Rust同样支持if-else if-else这样的多重条件分支结构,并且类似于C++的三目运算符,Rust可以写成下面这样
let result=if 10>5{true} else{false};
值得注意的是,这里两个条件分支中的表达式返回的值必须是同一类型.由于Rust是强类型语言,所以定义的时候必须确切知道变量数据类型.
4.2 match
match作为选择结构,可以用来匹配不同的模式(情况),最简单的demo如下
use rand::Rng;
fn main(){
let mut rng = rand::thread_rng();
let flag:i32=rng.gen_range(0, 10);
println!("flag={}",flag);
match flag {
1=>println!("flag=1"),
3=>println!("flag=3"),
5=>println!("flag=5"),
7=>println!("flag=7"),
_=>println!("flag is not in [1,3,5,7]"),
}
}
当我们不需要使用匹配到的值时,可以使用_
来表示默认缺省匹配,如果需要使用通配值则可以用一个变量来代替.
use rand::Rng;
fn main(){
let mut rng = rand::thread_rng();
let flag:i32=rng.gen_range(0, 10);
println!("flag={}",flag);
match flag {
1=>println!("flag=1"),
3=>println!("flag=3"),
5=>println!("flag=5"),
7=>println!("flag=7"),
tmp=>println!("flag={}",tmp),
}
}
对于一些简单的选择分支,也可以尝试使用if let
来代替match
4.3 loop
loop
算是一个其他语言没怎么见过的关键字,从实际使用来看loop{}
其实和while true{}
的差别不大,也就是说loop
更适合在无限循环的场景下使用,比如不断请求重试或者保持心跳.当然,也可以在代码中加入break
让循环退出.
4.4 while
上面说了loop
适合无限循环,在Rust中while
则适合处理不确定循环次数的情况,即保证能退出但是无法保证何时.最容易想到的场景应该就是条件变量wait的情况,当收到notify之后while也就不满足条件不会再wait.
4.5 for
for循环应该是最熟悉的循环结构了,在事先知道循环次数或者知道循环对象大小的前提下,对数据对象进行操作.
下面写个综合的demo来对比一下三种循环语句的差异
fn loop_sum(v:Vec<i32>)->i32{
let mut vector_len=v.len();
let mut sum=0;
loop{
vector_len-=1;
sum+=v[vector_len];
if vector_len==0{
break sum;
}
}
}
fn while_sum(v:Vec<i32>)->i32{
let mut sum=0;
let mut vector_len=v.len();
while vector_len>0{
vector_len-=1;
sum+=v[vector_len];
}
return sum;
}
fn for_sum(v:Vec<i32>)->i32{
let mut sum=0;
for value in v.iter(){
sum+=value;
}
return sum;
}
fn main(){
let test_vec=vec![1,2,3,4,5,6];
println!("{}",loop_sum(test_vec.clone()));
println!("{}",while_sum(test_vec.clone()));
println!("{}",for_sum(test_vec.clone()));
}
由于数据是有限个,因此这里for循环显得最优.所以应对不同业务场景要求,可以选择使用不同的循环语句来达到最高的效率.