Rust的变量必须先声明后使用。对于局部变量,最常见的声明语法为:
let variable : i32 = 100;
与传统的C/C++语言相比,Rust的变量声明语法不同。这样的设计主要有以下几个方面的考虑。
let
开头,类型一定是跟在冒号:
后面。语法歧义更少,语法分析器更容易编写。let
语句不光是局部变量声明语句,而且具有parttern destructure(模式结构)的功能。fn main() {
let x = 5;
println!("{}", x);
x = 10;
println!("{}", x);
}
会得到“cannot assign twice to immutable variable x”这样的编译错误。
如果我们需要让变量是可写的,那么需要使用mut
关键字:
fn main() {
let mut x = 5;
println!("{}", x);
x = 10;
println!("{}", x);
}
此时,变量x才是可读写的。
实际上,let
语句在此处引入了一个模式解构
,我们不能把let mut
视为一个组合,而应该将mut x
视为一个组合。
let mut
是一个“模式”,我们还可以用这种方式同时声明多个变量:
let (mut a, mut b) = (1, 2);
let Point {x: ref a, y: ref b} = p;
Rust中,每个变量必须被合理初始化之后才能被使用。使用未初始化变量这样的错误,在Rust中是不可能出现的。也不可能编译通过:
fn main() {
let x : i32;
println!("{}", x)
}
错误信息为: used binding x
isn’t initialized
编译器会帮我们做一个执行路径的静态分析,确保变量在使用前一定被初始化:
fn test(condition: bool) {
let x : i32; // 声明变量x,不必使用mut修饰
if condition {
x = 1; // 初始化x,不需要x是mut的,因为这是初始化,不是修改
println!("{}", x);
}
// 如果条件不满足,x没有被初始化
// 不使用x就没关系
}
Rust允许在同一个代码块中声明同样名字的变量。如果这样做,后面声明的变量会将前面声明的变量"遮蔽"起来
fn main() {
let x = "hello";
println!("x is {}", x);
let x = 5;
println!("x is {}", x);
}
上面这个程序是可以编译通过的。注意第5行代码,它不是x=5;它前面有一个let关键字。如果没有这个let关键字,这条语句就是对x的重新赋值。而有了这个let关键字,就是又声明了一个新的变量,只是它的名字恰巧与前面一个变量相同而已。
但是这两个x代表的内存空间完全不同,类型也完全不同,他们实际上是两个不同的变量。从第5行开始,一直到这个代码快结束,我们没有办法再去访问前一个x变量,因为它的名字已经被遮蔽了。
变量遮蔽在某些情况下非常有用,比如,我们需要在同一个函数内部把一个变量转换为另一个类型的变量,但又不想给他们起不同的名字。再比如,在同一个函数内部,需要修改一个变量绑定的可变性。例如,我们对一个可变数组执行初始化,希望此时它是可读写的,但是初始化完成后,我们希望它是只读的。可以这样做:
fn main() {
let mut v = Vec::new();
v.push(1);
v.push(2);
v.push(3);
let v = v;
for i in &v {
println!("{}", i);
}
}
反过来,如果一个变量是不可变的,我们也可以通过变量遮蔽创建一个新的、可变的同名变量。
fn main() {
let v = Vec::new();
let mut v = v;
v.push(1);
println!("{:?}", v);
}
Rust的类型推导功能是比较强大的。它不仅可以从变量声明的当前语句中获取信息进行推导,而且还能通过上下文信息进行推导。
fn main() {
// 没有明确标出变量的类型,但是通过字面量的后缀,编译器知道elem的类型为u8
let elem = 5u8;
// 创建一个动态数组,数组内包含的是什么元素类型可以不写
let mut vec = Vec::new();
vec.push(elem);
// 到后面调用了push函数,通过elem变量的类型,编译器可以推导出vec的实际类型是Vec
println!("{:?}", vec);
}
我们可以用type关键字给同一个类型起个别名(type alias)。示例如下:
type Age = u32;
fn grow(age: Age, year: u32) -> Age {
age + year
}
fn main() {
let x : Age = 20;
println!("20 years later: {}", grow(x, 20))
}
类型别名还可以用在泛型场景,比如:
type Double
那么以后使用Double的时候,就等同于(i32, Vec),可以简化代码
Rust中可以用static
关键字声明静态静态变量。如下所示:
static GLOBAL: i32 = 0;
与let语句一样,static语句同样也是一个模式匹配。与let语句不同的是,用static声明的变量的生命周期是整个程序,从启动到退出。static变量的声明周期永远是'static
,他占用的内存空间也不会再执行过程中回收。这也是Rust中唯一的声明全局变量的方法。
由于Rust非常注重内存安全,因此全局变量的使用有许多限制。这些限制都是为了防止程序员写出不安全的代码:
fn main() {
// 局部变量声明,可以留待后面初始化,只要保证使用前已经初始化即可
let x;
let y = 1_i32;
x = 2_i32;
println!("{} {}", x, y);
// 全局变量必须声明的时候初始化,因为全局变量可以写到函数外面,被任意一个函数使用
static G1 : i32 = 3;
println!("{}", G1);
// 可变全局变量无论读写都必须用unsafe修饰
static mut G2 : i32 = 4;
unsafe {
G2 = 5;
println!("{}", G2)
}
// 全局变量的内存不是分配到当前函数栈上,函数推出的时候,并不会销毁全局变量占用的内存空间,程序退出才会回收
}
Rust禁止在声明static变量的时候调用普通函数,或者利用语句块调用其他非const代码:
fn main() {
// 这样是允许的
static array : [i32; 3] = [1, 2, 3];
// 这样是不允许的
static vec : Vec<i32> = { let mut v = Vec::new(); v.push(1); v};
}
在Rust中还可以用const关键字做声明。如下所示:
const GLOBAL: i32 = 0;
使用const声明的是常量,而不是变量。因此一定不允许使用mut关键字修饰这个变量绑定,这是语法错误。常量的初始化表达式也一定要是一个编译期常量,不能是运行期的值。它与static变量的最大区别在于:编译器并不一定会给const常量分配内存空间,在编译过程中,它很可能会被内联优化。因此,千万不要用hack的方式,通过unsafe代码去修改常量的值,这么做是没有意义的。以const声明一个常量,也不具备类似let语句的模式匹配功能。
布尔类型(bool)代表的是"是"和"否"的二值逻辑。它有两个值:true和false。一般用在逻辑表达式中,可以执行“与”“或”“非”等运算。
fn main() {
let x = true;
let y: bool = !x; // 取反运算
let z = x && y; // 逻辑与,带短路功能
println!("{}", z);
let z = x || y; // 逻辑或,带短路功能
println!("{}", z);
let z = x & y; // 按位与,不带短路功能
println!("{}", z);
let z = x | y; // 按位或,不带短路功能
println!("{}", z);
let z = x ^ y; // 按位异或,不带短路功能
println!("{}", z);
}
一些比较运算表达式的类型就是bool类型:
fn logical_op(x: i32, y: i32) {
let z: bool = x < y;
println!("{}", z);
}
bool类型表达式可以用在if/while等表达式中,作为条件表达式 。比如:
if a >= b{
...
} else {
...
}
字符类型由char表示。它可以描述任何一个符合unicode标准的字符值。在代码中,单个的字符字面量用单引号包围。
let love = 'a'; // 可以嵌入任何unicode字符
字符类型字面量也可以使用转义符:
let c1 = '\n'; // 换行符
let c2 = '\x7f'; // 8 bit 字符变量
let c3 = '\u{7FFF}'; // unicode字符
Rust有很多的数字类型,主要分为整数类型和浮点数类型。各种整数类型之间的主要区分特征是:有符号/无符号,占据空间大小。
整数类型 | 有符号 | 无符号 |
---|---|---|
8 bits | i8 | u8 |
16 bits | i16 | u16 |
32 bits | i32 | u32 |
64 bits | i64 | u64 |
128 bits | i128 | u128 |
Pointer size | isize | usize |
所谓有符号/无符号,指的是如何理解内存空间中bit表达的含义。如果一个变量是有符号类型,那么它的最高为的哪一个bit就是"符号位",表示该数为正值还是负值。如果一个变量是无符号类型,那么它的最高为和其他位一样,表示该数的大小。比如对于一个byte(8 bits)的数据来说,如果存的是无符号数,那么它的表达范围是0~255,如果存的是有符号数,那么它的表达范围是-128~127.
关于各个整数类型所占据的空间大小,在名字中就已经表现的很明确了。Rust原生支持了从8位到128位的整数。需要特别关注的是isize和usize类型。它们占据的空间是不定的,与指针占据的空间一致,与所在平台有关。
数字类型的字面量表示可以有许多方式:
let var1: i32 = 32; // 十进制表示
let var2: i32 = 0xFF; // 以0x开头代表十六进制表示
let var3: i32 = 0o55; // 以0o开头代表八进制表示
let var4: i32 = 0b1001; // 以0b开头代表二进制表示
在所有的数字字面量中,可以在任意地方添加任意的下划线,以方便阅读:
let var5 = 0x_1234_ABCD;
字面量后面可以跟后缀,可代表该数字的具体类型,从而省略掉显示类型标记:
let var6 = 123usize; // i6变量是usize类型
let var7 = 0x_ff_u8; // i7变量是u8类型
let var8 = 32; // 不写类型,默认为i32类型
在Rust中,我们可以为任何一个类型添加方法,整形也不例外。比如在标准库中,整数类型有一个方法是pow(),它可以计算n次幂,于是我们可以这么用:
let x: i32 = 9;
println!("9 power 3 = {}", x.pow(3));
同理,我们甚至可以不使用变量,直接对整形字面量调用函数:
println!("9 power 3 = {}", 9_i32.pow(3));
我们可以看到这是非常方便的设计。
对于整数类型,如果Rust编译器通过上下文无法分析出该变量的具体类型,则自动默认为i32类型。比如:
fn main() {
let y = 10;
let z = y * y;
println!("{}", z);
}
在此例中,编译器只知道x是一个整数,但是具体是i8 i16 i32或者u8 u16 u32等,并没有足够的信息判断,这些都是有可能的。在这种情况下,编译器就默认把x当作i32类型处理。这么做的好处是,很多时候,我们不想在每个地方都明确地指定数字类型,这么做很麻烦。
在整数的算数运算中,有一个比较头疼的事情是"溢出"。在c语言中,对于无符号类型,算数运算永远不会overflow,如果超过表示范围,则自动舍弃高位数据,对于有符号类型,如果发生了overflow,标准规定这是undefined behavior,也就是说随便怎么处理都可以。
Rust的设计思路更倾向于预防bug,而不是无条件的压榨效率,Rust设计者希望能尽量减少"未定义行为"。比如彻底杜绝"Segment Fault"这种内存错误是Rust的一个重要设计目标。当然还有许多其他种类的bug,即便是无法完全解决,我们也希望能尽量避免。整数溢出就是这样的一种bug.
Rust在这个问题上的处理方式为:默认情况下,在debug模式下编译器会自动插入整数溢出检查,一旦发生溢出,则会引发pacin;在release模式下,不检查整数溢出,而采用自动舍弃高位的方式。实力如下:
fn arithmetic(m: i8, n: i8) {
// 加法运算,有溢出风险
println!("{}", m + n)
}
fn main() {
let m:i8 = 120;
let n:i8 = 120;
arithmetic(m, n)
}
执行这个程序,结果为:
thread 'main' pacicked at 'attempt to add with overflow', test.rs:3:20
Rust提供了基于IEEE 754-2008标准的浮点类型。按占据空间大小区分,分别为f32和f63,其使用方法与整形区别不大。浮点数字面量表示方式有如下几种。
fn main() {
let f1 = 123.0f64; // type f64
let f2 = 0.1f64; // type f64
let f3 = 0.1f32; // type f64
let f4: f64 = 12E+99_f64; // type f64 科学计数法
let f5: f64 = 2.; // type f64
}
与整数类型相比,Rust的浮点数类型相对复杂的多。浮点数的麻烦之处在于:它不仅可以表达正常的数值,还可以表达不正常的数值。
无GC的变成语言,如C、C++以及Rust,对数据的组织操作有更多的自由度,具体表现为:
类型名 | 简介 |
---|---|
Box | 指向类型T的、具有所有权的指针,有权释放内存 |
&T | 指向类型T的借用指针,也称为引用,无权释放内存,无权写数据 |
&mut T | 指向类型T的mut型指针,无权释放内存,有权写数据 |
*const T | 指向类型T的只读裸指针,没有声明周期信息,无权写数据 |
*mut T | 指向类型T的可读写裸指针,没有生命周期信息,有权写数据 |
除此之外,在标准库中还有一种封装起来的可以当作指针使用的类型,叫"智能指针"。常见的智能指针如下:
类型名 | 简介 |
---|---|
Rc | 指向类型T的引用计数指针,共享所有权,线程不安全 |
Arc | 指向类型T的原子性引用计数指针,共享所有权,线程安全 |
Cow<'a, T> | Clone-on-write,写时复制指针。可能是借用指针,也可能是具有所有权的指针 |
Rust对不同类型之间的转换控制的非常严格。即便是下面这样的程序,也会出现编译错误:
fn main() {
let var1: i8 = 41;
let var2: i16 = var1;
}
编译结果为mismatched types!
i8类型的变量无法向i16类型的变量赋值!这可能对于很多用户来会所都是一个意外。
Rust提供了一个关键字as,专门用于这样的类型转换:
fn main() {
let var1: i8 = 41;
let var2: i16 = var1 as i16;
println!("{}", var2)
}
也就是说,Rust设计者希望在发生类型转换的时候不是偷偷摸摸进行的,而是显式的标记出来,防止隐藏bug.虽然在很多时候会让代码显得不那么精简,但这也算是一种合理的折中。
as关键字也不是随便可以用的,它只允许编译器认为合理的类型转换。任意类型转换是不允许的。
fn main() {
let a = "some string";
let b = a as u32; // 编译错误
}
有些时候,甚至需要连续写多个as才能转换成功,比如&i32类型就不能直接转换为*mut i类型,必须像下面这样写才可以:
fn main() {
let a = "some string";
let p = &i as *const i32 as *mut i32;
println!("{:p}", p);
}
as表达式允许的转换类型如表所示。对于表达式e as u,e是表达式,u是要转换的目标类型,表中所示的类型转换是允许的。如果需要更复杂的类型转换,一般是使用标准库的From Into等trait.
Type of e | U |
---|---|
Integer or Float type | Integer or Float type |
C-like enum | Integer type |
bool or char | Interger type |
i8 | char |
*T | *V whereV: Sized * |
*T whereT: Sized | Numeric type |
Integer type | *V whereV: Sized |
&[T; n] | *const T |
Function pointer | &V whereV: Sized |
Function pointer | Integer |
复合数据类型可以在其他类型的基础上组成更复杂的组合关系。本章介绍tuple、struct等集中复合数据类型。
tuple指的是元组类型,它通过元括号包含一组表达式构成。tuple内的元素没有名字。tuple是把集合类型组合到一起的最简单的方式。比如:
初始化
fn main() {
let a = (li32, false); // 元组中包含两个元素,第一个是i32类型,第二个是bool类型
let b = ("a", (li32, 2i32)); // 元组中包含两个元素,第二个元素本身也是元组,它又包含两个元素
}
如果元组中之包含一个元素,应该在后面添加一个逗号,以区分括号表达式和元组:
let a = (0, )
let b = (0)
访问元组元素
访问元组内部元素有两种方法,一种是模式匹配
,另一种是数字索引
fn main() {
let p = (1i32, 2i32);
// 索引访问
let x = p.0;
let y = p.1;
// 解构元组
let (a, b) = p;
println!("{} {} {} {}", a, b, x, y);
// 模式匹配
match p {
(age, hei) => {
println!("age: {} hei: {}", age, hei);
}
}
// 元组输出
println!("{:?}", p)
}
结构体与元组类似,也可以把多个类型组合到一起,作为新的类型。区别在于,它的每个元素都有自己的名字。举个例子:
struct Point {
x: i32,
y: i32,
}
结构体实例化
每个元素之间采用逗号分开,最后一个逗号可以忽略不写。类型依旧跟在冒号后面,但是不能使用自动类型推导功能,必须显式指定。struct类型的初始化语法类似于json的语法,是哟将那个"成员-冒号-值"的格式。
fn main() {
// 声明式实例化
let p = Point{x: 0, y: 1};
println!("Point is at {} {}", p.x, p.y);
// 可变式实例化
let mut p = Point{x:0, y: 1};
p.x = 10;
p.y = 11;
println!("Point is at {} {}", p.x, p.y);
}
有些时候,Rust允许struct类型的初始化使用一种简化写法。如果有局部变量名字和成员变量名字恰好一致,那么可以忽略掉重复的冒号初始化:
fn main() {
// 刚好局部变量名字和结构体成员名字一致
let x = 10;
let y = 20;
// 下面是简略写法,等同于Point {x: x, y: y}
let p = Point{x, y};
println!("Point is at {} {}", p.x, p.y);
}
访问结构体元素
访问结构体内部的元素,也是使用"点"加变量名的方式。当然,我们也可以使用模式匹配功能。
fn main() {
let p = Point{x: 0, y: 1};
// 声明了px和py,分别绑定到成员x和成员y
let Point {x: px, y: py} = p;
println!("point is at {} {}", px, py);
// 同理,在模式匹配的时候,如果新的变量名刚好和成员名字相同,可以使用简写方式
let Point { x, y } = p;
println!("point is at {} {}", x, y);
}
结构体的方法
在Rust中,结构体可以拥有自己的方法。方法是与结构体关联的函数,可以通过结构体实例调用。
struct Point {
x: i32,
y: i32,
}
impl Point {
fn hello(&self) -> &str{
return "hello";
}
}
let p = Point{x: 0, y: 1};
println!("{}", p.hello());
在上述示例中,定义了一个名为Point的结构体,并为其实现了一个hello方法。