5、初识Rust – 数据类型
一、整型
Rust中,每一个值都有一个数据类型,以便明确数据的处理方式。Rust中存在两类数据类型子集,一种是标量(scalar),一种是复合(compound)。
Rust是一门静态类型语言,在编译时就必须明确所有变量的类型,根据值和其使用方式,编译器推断出想要使用的类型,例如
fn main() { let x: i8 = 2; println!("{x}"); }
变量x中的值就是一个整型类型,整数是一个没有小数部分的数字。除了i8整数类型,你也许还能看见下面这重类型
fn main() { let x: u8 = 2; println!("{x}"); }
这其实也是一个整数类型,那u8和i8有什么区别呢?
它们其实代表了2种整数,一种是有符号整数,一种是无符号整数。
有符号整数,通俗点就是负整数,无符号整数,则是正整数。有符号整数是包括正整数的。
i8可以存储 -(27) 到 27 – 1 在内的数字(-128-127);
u8可以存储0 到 28 – 1在内的数字(0 到 255);
以下为整型表
长度 | 有符号 | 无符号 |
---|---|---|
8-bit | i8 | u8 |
16-bit | i16 | u16 |
32-bit | i32 | u32 |
64-bit | i64 | u64 |
128-bit | i128 | u128 |
arch | isize | usize |
其中有2个特殊的isize与usize,这2个是依赖与运行程序的计算机架构的,64位架构运行时就是64位,32位架构时运行就是32位,一般这2个类型主要作为某些集合的索引。
可以按下面表内的任何一种形式编写数字字面值。请注意可以是多种数字类型的数字字面值允许使用类型后缀,例如 57u8
来指定类型,同时也允许使用 _
做为分隔符以方便读数,例如1_000
,它的值与你指定的 1000
相同。
数字字面值 | 例子 |
---|---|
Decimal (十进制) | 98_222 |
Hex (十六进制) | 0xff |
Octal (八进制) | 0o77 |
Binary (二进制) | 0b1111_0000 |
Byte (单字节字符)(仅限于u8 ) |
b’A’ |
还有下面这种类型的代码。你也许会很疑惑,不是要指定类型么,为什么下面这段代码可以正常运行,而不会抛出未定义类型的错误。
fn main() { let x = 2; println!("{x}"); }
这是因为,Rust存在默认类型这个概念,而数字默认类型就是i32。
补充一个整型溢出的概念
整型溢出
比方说有一个
u8
,它可以存放从零到255
的值。那么当你将其修改为256
时会发生什么呢?这被称为 “整型溢出”(“integer overflow” ),这会导致以下两种行为之一的发生。当在 debug 模式编译时,Rust 检查这类问题并使程序 panic,这个术语被 Rust 用来表明程序因错误而退出。第九章 “panic!
与不可恢复的错误” 部分会详细介绍 panic。在 release 构建中,Rust 不检测溢出,相反会进行一种被称为二进制补码包装(two’s complement wrapping)的操作。简而言之,值
256
变成0
,值257
变成1
,依此类推。依赖整型溢出被认为是一种错误,即便可能出现这种行为。如果你确实需要这种行为,标准库中有一个类型显式提供此功能,Wrapping
。 为了显式地处理溢出的可能性,你可以使用标准库在原生数值类型上提供的以下方法:
- 所有模式下都可以使用
wrapping_*
方法进行包装,如wrapping_add
- 如果
checked_*
方法出现溢出,则返回None
值- 用
overflowing_*
方法返回值和一个布尔值,表示是否出现溢出- 用
saturating_*
方法在值的最小值或最大值处进行饱和处理
二、浮点型
除了不带小数点的整型类型外,还有带小数点的浮点类型。相对于整型,浮点的类型就少的多了,分别是f32与f64,分别占32位与64位。默认类型是f64也就是未声明类型时的默认类型。
fn main() { let x = 2.0; // f64 let t: f32 = 3.0; // f32 }
浮点数采用 IEEE-754 标准表示。f32
是单精度浮点数,f64
是双精度浮点数。
三、数值运算
Rust中所有的数字类型都支持基本数学运算:加减乘除以及取余。整数除法会向下舍入到最接近的整数。
fn main() { // 加法 let sum = 5 + 10; // 减法 let difference = 95.5 - 4.3; // 乘法 let product = 4 * 30; // 除法 let quotient = 56.7 / 32.2; // 因为整数除法会向下取整数,所以结果为0 let floored = 2 / 3; // 取余 let remainder = 43 % 5; }
四、布尔型
布而型是一个非常重要的类型,Rust也和其它的编程语言一样使用2个可能的布尔类型值: true 和 false。在Rust中,声明布尔类型使用bool表示。
fn main() { let x = true; let f: bool = true; }
使用布尔类型的基本场景都是在表达式,比如if这些表达式。
另外博主还有一个想法,像整数类型因为类型非常多,所以依靠Rust默认附值可能达不到需求最后产生报错,但bool类型值都比较简单,是否可以在实际生产中也省略: bool?
五、字符类型
Rust的 char类型是语言中最原生的字母类型。
fn main() { let c = 'z'; let z: char = 'ℤ'; // with explicit type annotation let heart_eyed_cat = '😻'; }
特别需要注意的是,使用单引号来声明 char 变量,用双引号声明字符串变量。
Rust 的char 类型大小为四个字节,并代表了一个Unicode 标量值(Unicode Scalar Value),这表示它可以比ASCII表示更多的内容。在Rust中,拼音字母,中文,日文,韩文, emoji和零长度的空白字符,都是有效的 char值 ,Unicode 标量值包含从 U+0000
到 U+D7FF
和 U+E000
到 U+10FFFF
在内的值。不过,“字符” 并不是一个 Unicode 中的概念,所以人直觉上的 “字符” 可能与 Rust 中的 char
并不符合。
六、复合类型
Rust中的复合类型可以将多个值组合成一个类型,有两个原生的复合类型,分别是:元祖和数组。
1、元祖类型
元祖类型是将多个其它类型的数值组合进一个复合类型的主要方式。元祖类型的长度声明后就无法变更。
fn main() { let tup: (i32, f64, u8) = (500, 6.4, 1); }
上面就是一个元祖类型,变量声明后使用括号并用逗号分隔指定类型来创建一个元组。元祖中每个位置都有一个类型。
如何使用元祖?
fn main() { let tup: (i32, f64, u8) = (500, 6.4, 1); let (x,y,z) = tup; let a = tup.0; let b = tup.1; let c = tup.2; }
上面这段代码,就演示了如何在使用元祖 tup ,使用let 和一个模式将tup分成了3个不同的变量x,y,z。这个叫做解构,因为它将一个元祖拆分成了3个部分。除了结构,我们也可以使用(.)后面跟值的索引来获取数值,索引从0开始。
不带任何值的元祖有一个特别的名称,叫做 单元(unit) 元组。这种值和对应的类型都写作()表示空值或者空的返回类型。如果表达式不返回任何其他的值,则会隐式返回单元值。
2、数组类型
另一个复合类型就是数组了。和元祖不一样,数组中每个类型都是相同的。Rust中的数组和其它语言的数组不太一样。Rust中的数组长度是固定的。
fn main() { let a = [1,2,3,4,5]; }
当你想要在栈(stack)而不是在堆(heap)上为数据分配空间,或者是想要确保总是有固定数量的元素时,数组非常有用。但是数组并不如 vector 类型灵活。vector 类型是标准库提供的一个 允许 增长和缩小长度的类似数组的集合类型。当不确定是应该使用数组还是 vector 的时候,那么很可能应该使用 vector。
fn main() { let b = [1,1,1,1,1]; let a: [u8; 5] = [1,1,1,1,1]; let c = [1; 5]; let x = a[0]; let y = a[1]; }
上面的代码展示了数组的3种创建方式
第一种是使用Rust默认的也是最简单的创建方式。
第二种是在声明变量后写一个方括号,内部第一个值是数据类型,第二个值是数据个数,然后写入内容。
第三种是在声明变量后,在值处写一个方括号,内部第一个值是每个数的值,第二个值是该值创建多少个。
数组访问方式和其它编程语言差不多,都是在数组变量后写一个方括号,使用索引访问。因为数组是在堆栈上分配已知的固定大小的单个内存块,所以它可以使用索引访问。
当获取的数组索引大于数组长度时,就会报错,所以在用有变动性的值访问数组时,先做判断,因为在编译时Rust不会检测,也无法检测这个错误。