本文主要对Rust基础概念中关于变量、常量、数据类型的知识点进行梳理。
注:
let语句不光是局部变量声明语句,而且具有pattern destructure(模式解构)的功能;与let语句一样,static语句同样也是一个模式匹配;但const声明一个常量,不具备类似let语句的模式匹配功能。
分为标量类型和复合类型。标量类型有:整型、浮点型、布尔型、字符型。复合类型有:tuple(元组)、数组、struct、enum。
isize
和 usize
类型依赖运行程序的计算机架构:64 位架构上它们是 64 位的, 32 位架构上它们是 32 位的。isize
或 usize
主要作为某些集合的索引。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)); } //可以不使用变量,直接对整型字面量调用函数。
$ rustc -C overflow-checks=no test.rs
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_*系列函数 则是直接抛弃已经溢出的最高位,将剩下的部分返回。
}
use std::num::Wrapping;
fn main() {
let big = Wrapping(std::u32::MAX);
let sum = big + Wrapping(2_u32);
println!("{}", sum.0);
}
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
enum FpCategory {
Nan, //Nan代表的是“不是数字”(not a number)。
Infinite, //Infinite代表的是“无穷大”.
Zero, //Zero表示0值
Subnormal,
Normal, //Normal表示正常状态的浮点数
}
注:subnormal 请参考非规格化浮点数
fn main() {
let nan = std::f32::NAN;
println!("{} {} {}", nan < nan, nan > nan, nan == nan);
}
//输出结果:false false false
true
和 false
。Rust 中的布尔类型使用 bool
表示。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个#不能删除。否则编译器会认为后面的字符全部都是。
.
)后跟值的索引来直接访问它们。 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);//索引方式访问
let empty : () = ();
//定义示例
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);
}
//两种访问方式:“点”号加变量名;模式解构。
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 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 Foo1;
struct Foo2();
struct Foo3{}
struct Color(i32, i32, i32);
struct Point(i32, i32, i32);
struct User {
username: &str,
email: &str,
sign_in_count: u64,
active: bool,
}
fn main() {
let user1 = User {
email: "[email protected]",
username: "someusername123",
active: true,
sign_in_count: 1,
};
}
//加上生命周期注解:
struct User<'a> {
username: &'a str,
email: &'a str,
...//此处省略
}
//定义方式一:
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
enum Option {
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::());
println!("Size of i32: {}", std::mem::size_of::());
println!("Size of f32: {}", std::mem::size_of::());
}
//输出是: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::()); //为啥是32?目前还未理解。
println!("Size of String: {}", std::mem::size_of::());
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 Option { None, Some(T), }
fn main() {
let arr = [1,2,3,4,5];
// 请注意这里的map函数
let v: Vec
impl
来为struct定义方法那样,也可以在enum上定义方法。如下示例: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项吧。