《Rust编程与项目实战》(朱文伟,李建英)【摘要 书评 试读】- 京东图书 (jd.com)
3.5 数据类型的定义和分类
在Rust编程中,所谓数据类型,就是对数据存储的安排,包括存储单元的长度(占多少字节)以及数据的存储形式。不同的数据类型分配不同的长度和存储形式。
在编程时,我们将变量存储在计算机的内存中,但是计算机要知道我们要用这些变量存储什么样的值,因为一个简单的数值,一个字符或一个巨大的数值在内存中所占用的空间是不一样的。
在Rust中,每个值都属于某一个数据类型,用来告诉Rust它被指定为何种数据,以便明确数据处理方式。Rust基本数据类型主要有两类子集:标量(Scalar)类型和复合(Compound)类型。标量类型是单个值类型的统称。Rust中内建了4种标量类型:整数、浮点数、布尔值及字符类型。复合类型包括数组、元组、结构体和枚举等。
这里所讲的基本数据类型都是Rust原生的数据类型,它们都是创建在栈上的数据结构。Rust 标准库还提供了一些更复杂的数据类型,它们有些是创建在堆上的数据结构,这里先不讲。
Rust是静态类型语言,因此在编译时就必须知道所有变量的类型。通常,根据值及其使用方式,Rust 编译器可以推断出我们想要用的类型,当多种类型均有可能时,必须增加类型注解,否则编译会报错。
3.6 标量数据类型
3.6.1 整型
整型也叫整数类型,是专门用来定义整数变量的数据类型。按照整型变量占用的内存空间大小来讲,整型可以划分为 1字节整型、2字节整型、4字节整型、8字节整型、16字节整型。
按最高位是否当作符号位来讲,Rust 中的整型又分为有符号整型(Unsigned)和无符号整型(Signed)。有符号整型的左边最高位为0表示正数,为1则表示负数。有符号整型可以用来定义存储负数的变量,当然也可以定义存储非负数的变量。而无符号整型定义的变量只存储非负整数。
整型数据在内存中的存储方式:用整数的补码形式存放,原因在于,使用补码可以将符号位和数值域统一处理;同时,加法和减法也可以统一处理。一个整数X的补码计算方式可以用以下公式得到:
- 当X≥0时,X的补码=X的二进制形式。
- 当X < 0时,X的补码=(X + 2n)的二进制形式,n是补码的位数。
可以看出,一个非负整数的补码就是该数本身的二进制形式,负整数稍微复杂一些。
比如0,如果用1字节的存储单元存储一个整数,则其补码就是00000000,在内存中存储的形式就是00000000;又比如8,其二进制形式是1000,如果用1字节的存储单元存储一个整数,则在内存中的数据就是00001000。对于−8,假设用1字节的存储单元存储一个整数,根据公式,−8的补码就是(−8+28=−8+256=248)的二进制形式,即11111000,也就是它在内存中存放的形式。
- 有符号的8位整型i8
有符号的8位整型的类型名是i8,为了方便,经常会省略有符号3个字,直接称8位整型,或超短整型。编译器分配1字节长度的存储单元给i8定义的变量,最高位是符号位,0表示正数,1表示负数。比如定义一个i8类型的变量n:
let n: i8 = -6;
这个变量n在内存中占据1字节的存储单元,其在内存中的补码位(−6+256=250)的二进制形式为11111010。如果不信,当场验证一下,代码如下:
fn main() {
let n:i8=-6; //定义一个i8类型的变量n
println!("{:b}",n); //:b的意思是以二进制形式输出变量n的值
}
结果输出:11111010。
下面我们来看i8类型所能定义的数据的最小值和最大值,这个问题很多书都没讲清楚。i8类型整数的补码有8位,表示的范围为0000 0000~1111 1111,我们通过补码计算公式可以得到表3-2。
从表3-2中不难看出,最大值只能表示到127,因为128的补码最高位(符号位)是1,不能用来表示一个正数。而最小值是−128,因为−129的补码最高位(符号位)是0,不能用来表示一个负数。所以,我们得出i8类型所表示的最小值是−128,最大值是127。这里使用列表法得到最小值和最大值,似乎有点笨笨的。笔者再介绍一个更简便的方法得到最小值和最大值。
根据补码计算公式,非负数的二进制形式和补码相同,我们可以这样考虑来得到最大值,i8类型定义的变量所占用的存储空间长度是1字节,且最高位是符号位,则能存储的最大数据是01111111,左边第一位是0表示正数,后面7位表示数值,7位全为1时最大,因此最大数据就是27−1=127。对于最小值,则根据负数的补码计算公式,我们知道,正整数的补码是其本身的二进制数,负整数的补码就是(X + 2n)的二进制数,现在,0~127的二进制数已经用作正整数的补码了,那么一个负整数X的补码只能从128的二进制数开始找,也就是有这样的关系:X+256≥128,即X≥−128,从而得出最小值Xmin=−128。这个方法方便多了。如果不信,当场验证一下,代码 如下:
fn main() {
assert_eq!(i8::MIN, -128);
assert_eq!(i8::MAX, 127);
println!("{},{}",i8::MIN,i8::MAX);
}
结果输出就是−128,127。其中,Rust提供了i8::MIN来表示i8变量的最小值(−128),用i8::MAX表示i8变量的最大值(127)。而assert_eq!表示传入的两个参数如果不相等,则抛出异常,也就是会在输出窗口打印一行语句:thread 'main' panicked at 'assertion failed: `(left == right)`。
- 无符号8位整型u8
无符号8位整型的类型名是u8,又称无符号超短整型,它占据1字节长度的存储单元,最高位不是符号位。这里的u表示unsigned,u8定义的变量取值是非负整数,存储形式依旧是补码,根据补码计算公式,非负整数的补码和变量本身的二进制形式相同,比如变量值是255,那么其在内存中的存储形式就是11111111,它就是补码,也是255的二进制形式。
定义一个u8变量示例如下:
let n: u8 = 100;
u8的最高位不是符号位,是有效的数值位,因此u8定义的变量,其最小值是0,最大值是11111111,即28−1=255,我们可以用下列代码来验证:
fn main() {
println!("{},{}",u8::MIN,u8::MAX);
}
输出结果:0,255。
- 有符号16位整型i16
有符号16位整型的类型名是i16,占据2字节长度的存储单元,最高位是符号位。i16有时又称为短整型。定义一个i16变量示例如下:
let n: i16 = -100;
i16的最高位是符号位,若为0则表示正数,若为1则表示负数。i16定义的变量,其最大值是0111111111111111,即215−1=32767,最小值这样计算:X+65536≥32768,即X≥−32768,从而得出最小值Xmin=−32768,对应补码为(−32768+65536)的二进制形式,即32768的二进制数 1 000 000 000 000 000。得到最小值和最大值的原理这里不再赘述,因为已经在讲i8的时候详述过了。我们可以用下列代码来验证:
fn main() {
println!("{},{}",i16::MIN,i16::MAX);
println!("{:b},{:b}",i16::MIN,i16::MAX);
}
运行结果如下:
-32768,32767
1000000000000000,111111111111111
- 无符号16位整型u16
无符号16位整型的类型名是u16,占据2字节长度的存储单元,最高位不是符号位。u16有时又称无符号短整型。定义一个u16变量示例如下:
let n: u16 = 100;
u16定义的变量取值范围是[0, 65535]。
- 32位、64位、128位整型
一理通百理融。了解了8位、16位整型后,32位、64位、128位整型与之类似。i32是默认的整型,如果直接说出一个数字而不说它的数据类型,那么它默认就是i32。i64通常称为长整型,i128称为超长整型。
我们可以用一张表来归纳这些整型,Rust内建的整数类型如表3-3所示。
整型的长度还可以是arch。arch是由CPU构架决定大小的整型类型。大小为arch的整数在x86机器上为32位,在x64机器上为64位。arch整型通常用于表示容器的大小或者数组的大小,或者数据在内存上存储的位置。
有符号整型所表示的范围如表3-4所示。
无符号整型所表示的范围如表3-5所示。
3.6.2 布尔型
Rust使用关键字bool表示布尔数据类型,布尔型变量共有两个值:true和false。比如定义一个bool变量:
let checked:bool = true;
布尔变量占用1字节,使用bool类型的场景主要是条件判断。下列代码将输出bool变量的值:
fn main() {
let checked:bool = true;
println!("{}", checked);//输出true
}
输出结果:true。
3.6.3 字符类型
字符类型是Rust的一种基本数据类型,使用关键字char来表示字符类型。字符类型变量用于存放单个Unicode字符,这意味着ASCII字母、重音字母、中文、日文、韩文、表情符号和零宽度空格都是Rust中的有效字符值。Unicode 标量值的范围为U+0000~U+D7FF和U+E000~U+10FFFF(含)。然而,"字符"在Unicode中并不是一个真正的概念。在存储char类型数据时,会将其转换为UTF-8编码的数据(即Unicode代码点)进行存储。
char定义变量占用4字节空间(32bit),且不依赖于机器架构。我们可以用代码验证一下:
fn main() {
println!("{}", std::mem::size_of::<char>());
}
结果输出:4。看来的确占用了4字节。std是Rust的标准库,mem是std中的一个模块,size_of是模块mem中的函数,它返回某种数据类型占用的字节数。关于库、模块和函数的概念后面详述,现在只要知道这样调用是可以得到某个类型所占用的字节数的。
char类型变量的值是单引号包围的任意单个字符,例如'a'、'我'。注意:char和单字符的字符串String是不同的类型。比如下列代码定义字符类型变量并输出:
fn main() {
let a = 'z';
let b = '\n'; //赋值转义字符'\n'
let c = '我';
print!("{},{},{}",a,b,c); //输出
}
输出结果:
z,
,我
b的值是'\n',因此会出现换行。
另外,可使用关键字as将char转为各种整数类型,目标类型小于4字节时,将从高位截断,这种转换叫作显式转换。注意:Rust不会自动将char类型转换为其他类型,必须使用as进行显式转换。比如:
fn main() {
// char -> Integer
println!("{}", '我' as i32); // 25105=0x6211
println!("{}", '是' as u16); // 26159=0x662f
println!("{}", '是' as u8); // 47=0x2f,被截断了,因此66就没输出
}
结果输出:
25105
26159
47
我们以十进制形式输出了3个字符的Unicode值,第三行中的'是'转为u8类型,只能把0x2f存于u8中,66就被截断了。如果想在线查询某个字符的Unicode编码值,可以到网站https://www.unicodery.com上查询。
关于整型转为char类型,将用到标注库的char模块,我们到讲标准库的时候再讲。
3.6.4 浮点型
浮点型变量用来表示具有小数点的实数。为何在Rust中把实数称为浮点数呢?这是因为在Rust中,实数是以指数形式存放在存储单元中的。一个实数表示为指数可以有多种形式,比如5.1234可以表示为5.1234×100、51.234×10-1、0.51234×101、0.051234×102、51234×10-5、512340×10-6等。可以看到,小数点的位置可以在5、1、2、3、4这几个数字之间、之前或之后(需添加0)浮动,只要在小数点位置浮动的同时改变指数的值,就可以保证表示的是同一个实数。因为小数点位置可以浮动,因此以指数形式表示的实数称为浮点数。Rust 编程语言按照 IEEE 754 二进制浮点数表示与算术标准存储浮点数。IEEE 754这里就不展开了,如果以后大家从事这方面的底层开发,可以深入研读这个标准。这里只是让大家心里有个数。
与大多数编程语言一样,Rust也拥有两种不同精度的浮点类型,分为单精度浮点类型f32和双精度浮点类型f64。f32的数据使用32位来表示,f64的数据使用64位来表示。Rust中的默认浮点类型是f64,因为现在的CPU几乎为64位的,因此在处理f64和f32类型的数据时所耗的时间基本相同,但f64可表示的精度更高。值得注意的是,所有的浮点类型都是有符号的。下列代码输出3个浮点类型变量:
fn main() {
let x = 2.01; // 默认f64
let y: f32 = 3.14; // f32
let z:f64=6.28; //f64
println!("{},{},{}",x,y,z);
}
结果输出:2.01,3.14,6.28。
值得注意的是,Rust中不能将0.0赋值给任意一个整型,也不能将0赋值给任意一个浮点型,但可以将0.0赋值给浮点类型变量。
当数字很大的时候,Rust可以用下画线(_)来分段数字,这样可以使数字的可读性变得更好。比如:
fn main() {
let a=1_000_000;
let b:i64 =1_000_00088000;
let x:f64=1_000_000.666_123;
println!("{},{},{}",a,b,x);
}
结果输出:1000000,100000088000,1000000.666123。
3.6.5 得到变量的字节数
我们可以用std::mem::size_of::<类型>得到类型所占的字节数,比如:
println!("{}", std::mem::size_of::<char>());
输出结果是4。
除此之外,还可以通过std::mem::size_of_val获取变量所占用的字节数。比如:
fn main() {
//明确指定类型
let a:i64=100;
// 通过变量类型后缀指定变量的类型
let x = 1u8;
let y = 2u32;
let z = 3f32;
// 没有变量类型后缀,通过怎么使用变量来进行推断
let i = 1;
let f = 1.0;
println!("size of `a` in bytes: {}", std::mem::size_of_val(&a));
println!("size of `x` in bytes: {}", std::mem::size_of_val(&x));
println!("size of `y` in bytes: {}", std::mem::size_of_val(&y));
println!("size of `z` in bytes: {}", std::mem::size_of_val(&z));
println!("size of `i` in bytes: {}", std::mem::size_of_val(&i));
println!("size of `f` in bytes: {}", std::mem::size_of_val(&f));
}