基本类型
当我们了解完变量,那么我们肯定要知道这些变量可以是哪些类型。上节课的课后练习都是let x = "hello"
这样的赋值语句,可能会导致一个错误的印象,会认为 Rust 是动态语言。但很可惜,这是错的,Rust区别于Python,JavaScript,lua等动态语言,Rust是一门实实在在的 静态类型语言,也就是说编译器必须在编译期就该知道我们所有变量的类型。
之所以上节课我们定义变量的时候,又可以let x = 5
又可以let x ="hello"
,是因为 Rust编译器很聪明,它可以根据变量的值和上下文中的使用方法来自动推导出变量的类型 ,但是这也不是万能的,就比如说:
let guess = "42".parse().expect("Not a number!");
先不用管”42“后面,如果有点编程经验的话,不难看出这句话的意思是将字符串”42“转化为整数42。这时,用方法的话,编译器就猜不准我们到底想要什么类型了,那么就会给你一个报错:
$ cargo build
Compiling no_type_annotations v0.1.0 (file:///projects/no_type_annotations)
error[E0282]: type annotations needed
--> src/main.rs:2:9
|
2 | let guess = "42".parse().expect("Not a number!");
| ^^^^^ consider giving `guess` a type
错误写着很明白type annotations needed
(需要类型注释) 。那我们可以给变量guess加一个显示的类型标注:let guess: i32
或者 在强转的时候加一个强转的类型:"42".parse::<i32>()
。
好了,解释完昨天学的内容带来的疑惑之后。我们来讲一下Rust中的数据类型,总的来说包含两类:基本类型 和 由基本类型组成的 复合类型 。
先来简单的看看到底有哪些数据类型吧!
- 数值类型: 有符号整数 (
i8
,i16
,i32
,i64
,isize
)、 无符号整数 (u8
,u16
,u32
,u64
,usize
) 、浮点数 (f32
,f64
)、以及有理数、复数 - 字符串:字符串字面量和字符串切片
&str
- 布尔类型:
true
和false
- 字符类型: 表示单个 Unicode 字符,存储为 4 个字节
- 单元类型: 即
()
,其唯一的值也是()
数值类型
数值类型又可以简单的分解成:整数类型 和 浮点类型。
整数类型
整数,相信能看到这篇文章的人类应该都知道。这部分每个语言都大差不差,相信有编程经验的同学看两眼就懂了,所以我就直接借鉴Rust语言圣经了。
一般在使用过程中默认的是i32
类型,表示有符号的32位整数(i 表示英文单词 integer)还有一种无符号的整数一般使用u32
代表着应该是unsigned integer
。下表显示了Rust中的内置的整数类型:
长度 | 有符号类型 | 无符号类型 |
---|---|---|
8 位 | i8 | u8 |
16 位 | i16 | u16 |
32 位 | i32 | u32 |
64 位 | i64 | u64 |
128 位 | i128 | u128 |
视架构而定 | isize | usize |
类型定义的形式统一为:有无符号 + 类型大小(位数)
。无符号数表示数字只能取正数和0,而有符号则表示数字可以取正数、负数还有0。就像在纸上写数字一样:当要强调符号时,数字前面可以带上正号或负号;然而,当很明显确定数字为正数时,就不需要加上正号了。有符号数字以补码形式存储。
每个有符号类型规定的数字范围是 -(2n - 1) ~ 2n - 1 - 1,其中 n
是该定义形式的位长度。因此 i8
可存储数字范围是 -(27) ~ 27 - 1,即 -128 ~ 127。无符号类型可以存储的数字范围是 0 ~ 2n - 1,所以 u8
能够存储的数字为 0 ~ 28 - 1,即 0 ~ 255。
此外,isize
和 usize
类型取决于程序运行的计算机 CPU 类型: 若 CPU 是 32 位的,则这两个类型是 32 位的,同理,若 CPU 是 64 位,那么它们则是 64 位。
整形字面量可以用下表的形式书写:
数字字面量 | 示例 |
---|---|
十进制 | 98_222 |
十六进制 | 0xff |
八进制 | 0o77 |
二进制 | 0b1111_0000 |
字节 (仅限于 u8 ) | b'A' |
这么多类型,有没有一个简单的使用准则?答案是肯定的, Rust 整型默认使用 i32
,例如 let i = 1
,那 i
就是 i32
类型,因此你可以首选它,同时该类型也往往是性能最好的。isize
和 usize
的主要应用场景是用作集合的索引。
字面量
这里简单讲一下字面量的概念:字面量是指在编程中直接表示自己的值,而不是变量或表达式的符号。它们是程序中的固定值,例如整数、浮点数、字符串、布尔值等。字面量通常用于初始化变量或作为常量值直接在代码中使用。例如,整数字面量"5"表示值为5的整数,字符串字面量"Hello, World!"表示一个具体的字符串值。字面量在编程中起到直接指定数值或文本的作用,而不需要进行计算或执行任何操作。
整型溢出
这是我觉的Rust做的很有趣的点。
整型溢出就是给一个声明类型的变量将其修改为他范围之外的值,比如说:let x: i8 = 128
。
当你选择在debug模式编译的时候,Rust则会检测出整型溢出,并会给你一个panic ( Runtime Error,Rust 使用这个术语来表明程序因错误而退出)。
而当你在release模式构建时,不是说不检测,然是不会 panic ,取而代之的是,会按照补码循环溢出(two’s complement wrapping)的规则处理。简而言之,大于该类型最大值的数值会被补码转换成该类型能够支持的对应数字的最小值。比如在 u8
的情况下,256 变成 0,257 变成 1,依此类推。程序不会 panic,但是该变量的值可能不是你期望的值。依赖这种默认行为的代码都应该被认为是错误的代码。
要显式处理可能的溢出,可以使用标准库针对原始数字类型提供的这些方法:
- 使用
wrapping_*
方法在所有模式下都按照补码循环溢出规则处理,例如wrapping_add
- 如果使用
checked_*
方法时发生溢出,则返回None
值 - 使用
overflowing_*
方法返回该值和一个指示是否存在溢出的布尔值 - 使用
saturating_*
方法使值达到最小值或最大值
下面是一个演示wrapping_*
方法的示例:
fn main() {
let a : u8 = 255;
let b = a.wrapping_add(20);
println!("{}", b); // 19
}
浮点类型
浮点类型数字 是带有小数点的数字,在 Rust 中浮点类型数字也有两种基本类型: f32
和 f64
,分别为 32 位和 64 位大小。默认浮点类型是 f64
,在现代的 CPU 中它的速度与 f32
几乎相同,但精度更高。
下面是一个演示浮点数的示例:
fn main() {
let x = 2.0; // f64
let y: f32 = 3.0; // f32
}
浮点数根据 IEEE-754
标准实现。f32
类型是单精度浮点型,f64
为双精度。
浮点数陷阱
由于计算机内的数字都是由二进制数,但我们熟悉的十进制浮点数例如0.1并不存在精确的表达形式在二进制上。所以只能用近似值存储。由于这不是Rust独有的,而是计算机中任何语言都有的通病,我就不细描述了。
NaN
相信如果你做过一些关于浮点数的题,并不会对NaN感到陌生。
NaN(not a number)来表示一些数学上没有定义的结果,例如对负数取平方根 -42.1.sqrt()
。
所有跟 NaN
交互的操作,都会返回一个 NaN
,而且 NaN
不能用来比较,下面的代码会崩溃:
fn main() {
let x = (-42.0_f32).sqrt();
assert_eq!(x, x);
}
出于防御性编程的考虑,可以使用 is_nan()
等方法,可以用来判断一个数值是否是 NaN
:
fn main() {
let x = (-42.0_f32).sqrt();
if x.is_nan() {
println!("未定义的数学行为")
}
}
数字运算
也没什么特殊的点,相信来两断示例就明白了:
fn main() {
// 加法
let sum = 5 + 10;
// 减法
let difference = 95.5 - 4.3;
// 乘法
let product = 4 * 30;
// 除法
let quotient = 56.7 / 32.2;
// 求余
let remainder = 43 % 5;
}
fn main() {
// 编译器会进行自动推导,给予twenty i32的类型
let twenty = 20;
// 类型标注
let twenty_one: i32 = 21;
// 通过类型后缀的方式进行类型标注:22是i32类型
let twenty_two = 22i32;
// 只有同样类型,才能运算
let addition = twenty + twenty_one + twenty_two;
println!("{} + {} + {} = {}", twenty, twenty_one, twenty_two, addition);
// 对于较长的数字,可以用_进行分割,提升可读性
let one_million: i64 = 1_000_000;
println!("{}", one_million.pow(2));
// 定义一个f32数组,其中42.0会自动被推导为f32类型
let forty_twos = [
42.0,
42f32,
42.0_f32,
];
// 打印数组中第一个值,并控制小数位为2位
println!("{:.2}", forty_twos[0]);
}
位运算
Rust的运算基本上和其他语言一样
运算符 | 说明 |
---|---|
& 位与 | 相同位置均为1时则为1,否则为0 |
| 位或 | 相同位置只要有1时则为1,否则为0 |
^ 异或 | 相同位置不相同则为1,相同则为0 |
! 位非 | 把位中的0和1相互取反,即0置为1,1置为0 |
<< 左移 | 所有位向左移动指定位数,右位补0 |
>> 右移 | 所有位向右移动指定位数,带符号移动(正数补0,负数补1) |
序列
这是Rust提供的一个独有的,非常简洁的方式,用来生成连续的数值,例如1..5
,规则:经典左闭右开 [1 , 5 )。如果你想都是用闭区间,则可以加一个等号:1…=5。
这个最常见的应用在于循环里。
for i in 1..=5{
print!("{}",i);
}
序列只允许用于数字或字符类型,原因是:它们可以连续,同时编译器在编译期可以检查该序列是否为空,字符和数字值是 Rust 中仅有的可以用于判断是否为空的类型。如下是一个使用字符类型序列的例子:
for i in 'a'..='z' {
println!("{}",i);
}
有理数和复数
很可惜,Rust的标准库中并未包含这两部内容,同时也不包过高精度的整数和任意精度的浮点数。需要引用别人的库了,至于具体怎么引用,过几天会谈到的吧。
总结
Rust中的数值类型基本上跟其他语言一样,特殊的提几点不一样的点:
- Rust 是 静态类型语言,需要显示声明类型,但是Rust的编译器大部分时候能知道你的类型,但是在今天提到的类型转换的时候就需要声明。
- Rust的数值后面可以使用方法。例如:
13.14.round()
- 序列 是一个Rust中方便且常用的方式。
练习
1、
// 移除某个部分让代码工作
fn main() {
let x: i32 = 5;
let mut y: u32 = 5;
y = x;
let z = 10; // 这里 z 的类型是?
}
Tips: 如果我们没有显式的给予变量一个类型,那编译器会自动帮我们推导一个类型.
fn main() {
let x = 5;
let mut y: u32 = 5;
y = x;
let z = 10; // 这里 z 的类型是i32
}
//移除x声明的类型,编译器会根据上下文定义类型为u32
//或者
fn main() {
let x: i32 = 5;
let mut y = 5;
y = x;
let z = 10;
}
//让x和y都为默认的i32类型
2、
// 填空
fn main() {
let v: u16 = 38_u8 as __;
}
Tips:类型转换。
fn main() {
let v: u16 = 38_u8 as u16;
}
//在这个转换过程中需要使用”as“关键字来指示类型转换。由于 u16 比 u8 更宽,因此在转换时不会发生数据丢失。
3、
// 修改 `assert_eq!` 让代码工作
fn main() {
let x = 5;
assert_eq!("u32".to_string(), type_of(&x));
}
// 以下函数可以获取传入参数的类型,并返回类型的字符串形式,例如 "i8", "u8", "i32", "u32"
fn type_of<T>(_: &T) -> String {
format!("{}", std::any::type_name::<T>())
}
Tips: 如果我们没有显式的给予变量一个类型,那编译器会自动帮我们推导一个类型
fn main() {
let x = 5;
assert_eq!("i32".to_string(), type_of(&x));
}
fn type_of<T>(_: &T) -> String {
format!("{}", std::any::type_name::<T>())
}
//默认类型为i32
4、
// 填空,让代码工作
fn main() {
assert_eq!(i8::MAX, __);
assert_eq!(u8::MAX, __);
}
fn main() {
assert_eq!(i8::MAX, 127);
assert_eq!(u8::MAX, 255);
}
5、
// 解决代码中的错误和 `panic`
fn main() {
let v1 = 251_u8 + 8;
let v2 = i8::checked_add(251, 8).unwrap();
println!("{},{}",v1,v2);
}
fn main() {
let v1 = 247_u8 + 8;
let v2 = i8::checked_add(119, 8).unwrap();
println!("{},{}",v1,v2);
}
//不能越界
6、
// 修改 `assert!` 让代码工作
fn main() {
let v = 1_024 + 0xff + 0o77 + 0b1111_1111;
assert!(v == 1597);
}
fn main() {
let v = 1_024 + 0xff + 0o77 + 0b1111_1111;
assert!(v == 1597);
}
//emmm,这个建议摁计算器,只会100以内加减法。
7、
// 将 ? 替换成你的答案
fn main() {
let x = 1_000.000_1; // ?
let y: f32 = 0.12; // f32
let z = 0.01_f64; // f64
}
fn main() {
let x = 1_000.000_1; // f64
let y: f32 = 0.12; // f32
let z = 0.01_f64; // f64
}
//默认f64
8、
//使用两种方法来让下面代码工作
fn main() {
assert!(0.1+0.2==0.3);
}
fn main() {
assert!(0.1_f32+0.2_f32==0.3_f32);
}
//降低精度
fn main() {
let eps=0.001;
assert!((0.1_f64+ 0.2 - 0.3).abs() < eps);
}
//设置允许误差
9、
//两个目标: 1. 修改 assert! 让它工作 2. 让 println! 输出: 97 - 122
fn main() {
let mut sum = 0;
for i in -3..2 {
sum += i
}
assert!(sum == -3);
for c in 'a'..='z' {
println!("{}",c);
}
}
fn main() {
let mut sum = 0;
for i in -3..2 {
sum += i
}
assert!(sum == -5);
for c in 'a'..='z' {
println!("{}",c as u8);
}
}
//序列和强制类型转换
10、
// 填空
use std::ops::{Range, RangeInclusive};
fn main() {
assert_eq!((1..__), Range{ start: 1, end: 5 });
assert_eq!((1..__), RangeInclusive::new(1, 5));
}
use std::ops::{Range, RangeInclusive};
fn main() {
assert_eq!((1..6), Range{ start: 1, end: 5 });
assert_eq!((1..=5), RangeInclusive::new(1, 5));
}
//看不懂可以先运行一下,编译器直接告诉你什么意思了。好用的很
11、
// 填空,并解决错误
fn main() {
// 整数加法
assert!(1u32 + 2 == __);
// 整数减法
assert!(1i32 - 2 == __);
assert!(1u8 - 2 == -1);
assert!(3 * 50 == __);
assert!(9.6 / 3.2 == 3.0); // error ! 修改它让代码工作
assert!(24 % 5 == __);
// 逻辑与或非操作
assert!(true && false == __);
assert!(true || false == __);
assert!(!true == __);
// 位操作
println!("0011 AND 0101 is {:04b}", 0b0011u32 & 0b0101);
println!("0011 OR 0101 is {:04b}", 0b0011u32 | 0b0101);
println!("0011 XOR 0101 is {:04b}", 0b0011u32 ^ 0b0101);
println!("1 << 5 is {}", 1u32 << 5);
println!("0x80 >> 2 is 0x{:x}", 0x80u32 >> 2);
}
fn main() {
assert!(1u32 + 2 == 3);
assert!(1i32 - 2 == -1);
assert!(1i8 - 2 == -1);
assert!(3 * 50 == 150);
assert!(9 / 3 == 3);
assert!(24 % 5 == 4);
assert!(true && false == false);
assert!(true || false == true);
assert!(!true == false);
println!("0011 AND 0101 is {:04b}", 0b0011u32 & 0b0101);
println!("0011 OR 0101 is {:04b}", 0b0011u32 | 0b0101);
println!("0011 XOR 0101 is {:04b}", 0b0011u32 ^ 0b0101);
println!("1 << 5 is {}", 1u32 << 5);
println!("0x80 >> 2 is 0x{:x}", 0x80u32 >> 2);
}
本文是作者为了学习Rust有动力,才打算发表的。全文都是参考Rust语言圣经 。有不懂的地方,建议看原文,毕竟可能我也是瞎写的,哈哈。
== 3);
assert!(24 % 5 == 4);
assert!(true && false == false);
assert!(true || false == true);
assert!(!true == false);
println!("0011 AND 0101 is {:04b}", 0b0011u32 & 0b0101);
println!("0011 OR 0101 is {:04b}", 0b0011u32 | 0b0101);
println!("0011 XOR 0101 is {:04b}", 0b0011u32 ^ 0b0101);
println!("1 << 5 is {}", 1u32 << 5);
println!("0x80 >> 2 is 0x{:x}", 0x80u32 >> 2);
}
本文是作者为了学习Rust有动力,才打算发表的。全文都是参考[Rust语言圣经](https://link.zhihu.com/?target=https%3A//course.rs/) 。有不懂的地方,建议看原文,毕竟可能我也是瞎写的,哈哈。