前言
本文主要对Rust基础概念中关于变量、常量、数据类型的知识点进行梳理。
变量
- 变量默认不可变,不能对不可变变量进行二次赋值。要使变量可变,可以在变量名前mut;
- 变量在使用前,必须初始化。编译器会做一个执行路径的静态分析,确保变量在使用前一定被初始化。
- 变量可以在同一代码块被遮蔽(Shadowing,有些语言也允许遮蔽,但只能在嵌套的子块内)。
- 变量的标识符必须由数 字、字母、下划线组成,且不能以数字开头。不能使用关键字作为标识符,如果一定要,可加上前缀:r#。单个下划线(_)作为标识符,表示忽略此绑定。
- 变量申明时,可以省略类型,由编译器根据上下文进行类型推导;Rust允许对局部变量和全局变量进行推导,函数签名的类型不允许推导;类型推导的示例:字面量后带上类型后缀(例如:let elem = 5u8;)、Vec声明时不指定类型,当push item后编译器自动推导;
- 可以用type关键字给某个类型取个别名;类型别名也可用在泛型场景。
- 静态变量使用static关键字进行声明;其生命周期为'static,在整个程序运行期间都存在;static是Rust中声明全局变量的唯一方法;
- 全局变量的规则:1,声明时必须马上初始化,且必须是编译期可确定的常量,2,带mut的全局变量,使用时必须使用unsafe关键字;3,禁止声明时调用普通函数(或者其他的非const代码);
- 如果用户需要使用比较复杂的全局变量初始化,推荐使用lazy_static库。
常量
- 常量使用const关键字进行声明;不允许用mut修饰;初始化表达式必须是编译期常量;
- 和static声明的变量相比:编译器并不一定会给const常量分配内存空间,在编译过程中,它很可能会被内联优化。
注:
let语句不光是局部变量声明语句,而且具有pattern destructure(模式解构)的功能;与let语句一样,static语句同样也是一个模式匹配;但const声明一个常量,不具备类似let语句的模式匹配功能。
基本数据类型
分为标量类型和复合类型。标量类型有:整型、浮点型、布尔型、字符型。复合类型有:tuple(元组)、数组、struct、enum。
标量类型
- 整型:i8/i16/i32/i64/i128/isize(有符号)和 u8/u16/u32/u64/u128/usize(有符号)
isize
和usize
类型依赖运行程序的计算机架构:64 位架构上它们是 64 位的, 32 位架构上它们是 32 位的。isize
或usize
主要作为某些集合的索引。- 16进制:前缀0x;8进制:前缀0o;二进制:0b;单字节(仅限u8):前缀b。除 byte 以外的所有数字字面值允许使用类型后缀,例如
57u8
,同时也允许使用_
做为分隔符以方便读数,例如1_000。
一些示例如下: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; // var6变量是usize类型 let var7 = 0x_ff_u8; // var7变量是u8类型 let var8 = 32; // 不写类型,默认为 i32 类型 fn main() { println!("9 power 3 = {}", 9_i32.pow(3)); } //可以不使用变量,直接对整型字面量调用函数。
- 整数溢出:Rust在这个问题上选择的处理方式为:默认情况下,在debug模式 下编译器会自动插入整数溢出检查,一旦发生溢出,则会引发panic;在 release模式下,不检查整数溢出,而是采用自动舍弃高位的方式。Rust编译器还提供了一个独立的编译开关供我们使用,通过这个开关(overflow-checks为yes或者no),可以设置溢出时的处理策略。
$ rustc -C overflow-checks=no test.rs
- 如果需要更精细地自主控制整数溢出的行 为,可以调用标准库中的checked_*、saturating_*和wrapping_*系列函 数。
fn main() { let i = 100_i8; println!("checked {:?}", i.checked_add(i)); //输出:checked None,checked_*系列函数返回的类型是Option<_>,当出现溢 出的时候,返回值是None; println!("saturating {:?}", i.saturating_add(i)); //输出:saturating 127,saturating_*系列函数返回类型是整数,如果 溢出,则给出该类型可表示范围的“最大/最小”值; println!("wrapping {:?}", i.wrapping_add(i)); //输出:wrapping -56,wrapping_*系列函数 则是直接抛弃已经溢出的最高位,将剩下的部分返回。 }
- 在很多情况下,整数溢出应该被处理为截断,即丢弃最高位。为了 方便用户,标准库还提供了一个叫作std::num::Wrapping<T>的类 型。凡是被它包裹 起来的整数,任何时候出现溢出都是截断行为。
use std::num::Wrapping; fn main() { let big = Wrapping(std::u32::MAX); let sum = big + Wrapping(2_u32); println!("{}", sum.0); }
- Rust 的浮点数类型是
f32
和f64
,分别占 32 位和 64 位。默认类型是f64。
一些示例如下:let f1 = 123.0f64; //type f64 let f2 = 0.1f64; //type f64 let f3 = 0.1f32; //type f32 let f4 = 12E+99_f64; //type f64 科学计数法 let f5 : f64 = 2.; //type f64
- 在标准库中,有一个std::num::FpCategory枚举,表示了浮点数可能的状态:
enum FpCategory { Nan, //Nan代表的是“不是数字”(not a number)。 Infinite, //Infinite代表的是“无穷大”. Zero, //Zero表示0值 Subnormal, Normal, //Normal表示正常状态的浮点数 }
注:subnormal 请参考非规格化浮点数
- NaN这个特殊值有个特殊的麻烦,主要问题还在于它不具备“全序”的特点(一个数字可以不等于自己)。因为NaN的存在,浮点 数是不具备“全序关系”(total order)。
fn main() { let nan = std::f32::NAN; println!("{} {} {}", nan < nan, nan > nan, nan == nan); } //输出结果:false false false
- Rust 中的布尔类型有两个可能的值:
true
和false
。Rust 中的布尔类型使用bool
表示。 - Rust 的
char
类型是语言中最原生的字母类型。Rust 的char
类型的大小为四个字节(four bytes),并代表了一个 Unicode 标量值。以下为一些示例:let c1 = '\n'; //换行符 let c2 = '\x7f'; //8 bit 字符变量 let c3 = '\u{7FFF}'; //unicode字符 let x :u8 = 1; let y :u8 = b'A'; let s :&[u8;5] = b"hello"; let r :&[u8;14] = br#"hello \n world"#; //第2个#不能删除。否则编译器会认为后面的字符全部都是。
tuple
- 元组(tuple)通过圆括号包含一组表达式来表示;元组长度固定:一旦声明,其长度不会增大或缩小;为了从元组中获取单个值,可以使用模式匹配(pattern matching)来解构(destructure)元组值;除了使用模式匹配解构外,也可以使用点号(
.
)后跟值的索引来直接访问它们。let a = (1i32, false); // 元组中包含两个元素,第一个是i32类型,第二个是bool类型 let b = ("a", (1i32, 2i32)); // 元组中包含两个元素,第二个元素本身也是元组,它又包含了一个元组 let a = (0,); // a是一个元组,它有一个元素 let b = (0); // b是一个括号表达式,它是i32类型。注意它没有逗号 let p = (1i32, 2i32); let (a, b) = p; //模式解构方式访问 let x = p.0; let y = p.1; println!("{} {} {} {}", a, b, x, y);//索引方式访问
- 元组内部也可以一个元素都没有,称之为:unit。unit类型是Rust中最简单的类型之一(它占用0内存空间),也是占用空间最小的类型之一(空struct也是如此)。示例如下:
let empty : () = ();
struct
- struct 定义、初始化方式、简化的初始化方式,如下示例。相比元组,它的成员有名字。
//定义示例 struct Point { x: i32, y: i32, } //初始化示例 fn main() { let p = Point { x: 0, y: 0}; // key: value 键-值对的形式,提供各个成员的值 println!("Point is at {} {}", p.x, p.y); } //如果有局部变量名字和成员变量名字相同时,那么可以简化如下: 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); }
- struct有两种访问方式:“点”号加变量名;模式解构。如下示例。
//两种访问方式:“点”号加变量名;模式解构。 fn main() { let p = Point { x: 0, y: 0}; // 声明了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); }
- 简化struct赋值的语法糖:default函数和..expr语法,也叫结构体更新语法(struct update syntax)
struct Point3d { x: i32, y: i32, z: i32, } //default()函数示例 fn default() -> Point3d { Point3d { x: 0, y: 0, z: 0 } } // 可以使用default()函数初始化其他的元素 let origin = Point3d { x: 5, ..default()}; // ..expr 这样的语法,只能放在初始化表达式中所有成员的最后,最多只能有一个 let point = Point3d { z: 1, x: 2, ..origin };
- 定义空struct的方式。没有成员的struct也叫类单元结构体(unit-like structs)。其常见的应用场景:当你想要在某个类型上实现 trait 但不需要在类型中存储数据的时候发挥作用。
//以下三种都可以,内部可以没有成员 struct Foo1; struct Foo2(); struct Foo3{}
- 定义tuple struct的方式如下。tuple struct有名字,而它们的成员没有名字。就像是tuple和struct的混合体。tuple struct有一个特别有用的场景:那就是当它只包含一个元素的 时候,就是所谓的newtype idiom。
struct Color(i32, i32, i32); struct Point(i32, i32, i32);
- 定义struct时,要考虑成员的所有权。可以使struct存储被其他对象拥有的数据的引用,但此时需要用上 生命周期(lifetimes)注解。如下例,username 不是String而是&str,表示该成员的所有权不归User。此时编译器会报错。解决办法:要么将&str改成String;要么加上生命周期注解。所有权和生命周期后续会详细解释。
struct User { username: &str, email: &str, sign_in_count: u64, active: bool, } fn main() { let user1 = User { email: "someone@example.com", username: "someusername123", active: true, sign_in_count: 1, }; } //加上生命周期注解: struct User<'a> { username: &'a str, email: &'a str, ...//此处省略 }
enum
- enum中的每个元素的定义语法与struct的定义语法类似。可以像空结构体一样,不指定它的类型;也可以像tuple struct一样,用圆括号加无名成员;还可以像正常结构体一样,用大括号加带名字的成员。以下是示例:
//定义方式一: enum IpAddrKind { V4, V6,} struct IpAddr { kind: IpAddrKind, address: String, } let home = IpAddr { kind: IpAddrKind::V4, address: String::from("127.0.0.1"), }; //定义方式二:更简洁。使用枚举并将数据直接放进每一个枚举成员,而不是将枚举作为结构体的一部分。 //无需一个额外的struct enum IpAddr { V4(String), V6(String), } let home = IpAddr::V4(String::from("127.0.0.1"));
-
定义enum时可以为每个成员指定附属的类型信息。用enum替代struct还有另一个优势:每个成员可以处理不同类型和数量的数据。如下示例。
enum IpAddr { V4(u8, u8, u8, u8), V6(String), } let home = IpAddr::V4(127, 0, 0, 1); let loopback = IpAddr::V6(String::from("::1"));
-
enum和struct还有另一个相似点:就像可以使用
impl
来为struct定义方法那样,也可以在enum上定义方法。如下示例:impl Message { fn call(&self) { // 在这里定义方法体 } } let m = Message::Write(String::from("hello")); m.call();
-
Rust中常见的一个enum:Option<T>。定义如下。因为太常用,所以被包含进prelude,使用时你无需显式地引入作用域;其设计目的是为了限制空值的泛滥以增加 Rust 代码的安全性;将T从Some(T)取值出来,可以参考它的文档(看了下,是unwrap*系列方法)。
enum Option<T> { Some(T), None, }
-
Rust的enum类型的变量需要区分它里面的数据究竟是哪种变体,所 以它包含了一个内部的“tag标记”来描述当前变量属于哪种类型。这个标记对用户是不可见的(但会占用存储空间)。如果是在FFI场景下,要保证Rust里面的enum的内存布局和C语言兼 容的话,可以给这个enum添加一个#[repr(C,Int)]属性标签。
注 :
1,《Rust深入浅出》中提到这个属性标签#[repr(C,Int)]当时有通过设计还未实现。动手试了下,发现编译器会报错。使用#[repr(C)] 虽然不报错,但不确定是否同样的意义。列成一个TODO项吧。
2,《Rust深入浅出》中举了一个例子(如下代码)。文中有句话:“它总共占用的内存是8 byte,多出来的4 byte就是用于保存类型标记的”。那么对于成员为不同类型的enum,其总共占用的空间该如何计算呢?是各个成员所占用空间的最大值,再加上一个类型标记(4字节)吗?动手试了一下。如下的Message占32byte,成员中的类型String占24byte(成员中它最多),多出来的8个byte怎么来的?还不太理解。列成一个TODO项吧。
//此例来自《深入浅出》2.3.4
fn main() {
// 使用了泛型函数的调用语法,请参考第21章泛型
println!("Size of Number: {}", std::mem::size_of::<Number>());
println!("Size of i32: {}", std::mem::size_of::<i32>());
println!("Size of f32: {}", std::mem::size_of::<f32>());
}
//输出是:8,4,4
//自动动手试的代码
#![allow(unused)]
fn main() {
enum Message {
Quit,
Move { x: i32, y: i32 },
Write(String),
ChangeColor(i32, i32, i32),
}
println!("Size of Message: {}", std::mem::size_of::<Message>()); //为啥是32?目前还未理解。
println!("Size of String: {}", std::mem::size_of::<String>());
println!("Size of String: {}", std::mem::size_of_val(&Message::Move{x:10,y:11}));//看任意一个成员占多少。答案是和Message一样多。这个倒还好理解。
}
//输出是32,24,32
-
Rust里面也支持union类型,这个类型与C语言中的union完全一致。 但在Rust里面,读取它内部的值被认为是unsafe行为,一般情况下我们 不使用这种类型。它存在的主要目的是为了方便与C语言进行交互。
-
在Rust中,enum和struct为内部成员创建了新的名字空间。如果要 访问内部成员,可以使用::符号。因此,不同的enum中重名的元素 也不会互相冲突。
-
也可以手动指定每个变体自己的标记值。如下示例:
fn main() { enum Animal { dog = 1, cat = 200, tiger, } let x = Animal::tiger as isize; println!("{}", x); }
- Rust的enum实际上是一种代数类型系统(Algebraic Data Type, ADT)。enum内部的variant只是一个名字而已,恰好我们还可以将这个名字作为类型构造器使用。意思是说,我们可以把enum内部的variant当成一个函数使用。如下例子:
//这是rust标准库中定义的:enum Option<T> { None, Some(T), } fn main() { let arr = [1,2,3,4,5]; // 请注意这里的map函数 let v: Vec<Option<&i32>> = arr.iter().map(Some).collect(); println!("{:?}", v); }
- enum和struct还有另一个相似点:就像可以使用
impl
来为struct定义方法那样,也可以在enum上定义方法。如下示例:
数组
- 数组中的值位于中括号内的逗号分隔的列表中;Rust 中的数组与一些其他语言中的数组不同,因为 Rust 中的数组是固定长度的:一旦声明,它们的长度不能增长或缩小。
- 声明数组:在方括号中包含每个元素的类型,后跟分号,再后跟数组元素的数量。例如:
let a: [i32; 5] = [1, 2, 3, 4, 5];
-
如果希望创建一个每个元素都相同的数组,可以在中括号内指定其初始值,后跟分号,再后跟数组的长度。例如:
let a = [3; 5];
-
数组并不如 vector 类型灵活。当不确定是应该使用数组还是 vector 的时候,你可能应该使用 vector。
-
数组是一整块分配在栈上的内存。可以使用索引来访问数组的元素。数组访问越界时,编译虽然通过,但程序运行时会panic。
-
类型转换可以使用as关键字;类型转换须是显式,编译器不会做自动地隐式转换;as关键字也不是随便可以用的,它只允许编译器认为合理的类型转换;有些时候,甚至需要连续写多个as才能转成功;如果需要更复杂的类型转换,一般是使用标准库的From Into等 trait,
注:
1,这些知识点主要参考了官方的《Rust程序设计语言》和《Rust深入浅出》。《Rust编程之道》还没看。后续若看了再更新此文。
2,针对复合数据类型,例如struct、enum、数组等,本章主要说明了其概念和定义方式等。还有些其他知识点(使用方式、高级主题相关)等,不适合放在本篇。后续笔记要再补上这一块。列成TODO项吧。