前言
本文介绍Rust中的基本类型(Primitive Type)。
不同于js或java的基本类型(primitive value)或引用类型的概念, Rust 里的“primitive types”意思是:
- 这些类型由 Rust 语言本身内建,不是用 trait 或用户自定义类型实现的。
- 某些“基本类型”是直接可以用的(比如
i32),但有些(比如str)本身不能被直接拿来用,只能通过引用或指针来用。 - Rust 的类型系统允许基本类型是dynamically sized type(DST),但不是所有 primitive type 都能在不带引用情况下独立出现。
rust的基本类型非常多,但好在大多数都容易掌握。
以上截图来自于rust标准库文档,这是rust最重要的一个文档。
标量类型和复合类型
Rust 的类型系统更倾向于使用标量类型(scalar types)和复合类型(compound types)来分类其内置的原始类型(primitive types):标量类型代表单一值,而复合类型允许将多个值组合成一个单元。
1. 标量类型(Scalar Types)
-
特点:
- 固定大小:在编译时已知大小,便于优化。
- 不可分解:不能进一步拆分成更小的子类型。
- 用于基本计算和逻辑操作。
-
Rust 中的标量类型示例:
- 整数类型(Integers) :有符号(i8, i16, i32, i64, i128, isize)和无符号(u8, u16, u32, u64, u128, usize)。例如,let x: i32 = 42; 表示一个单一的整数值。
- 浮点类型(Floating-point) :f32 和 f64。例如,let y: f64 = 3.14; 表示一个单一的浮点数值。
- 布尔类型(Boolean) :bool,只有 true 或 false。例如,let z: bool = true;。
- 字符类型(Character) :char,表示单个 Unicode 标量值(Scalar Value),占用 4 字节。例如,let c: char = 'A';。
标量类型常用于简单变量声明和表达式计算,是 Rust 性能优化的基础。
2. 复合类型(Compound Types)
-
特点:
- 可组合:可以嵌套其他类型,形成层次结构。
- 大小通常固定(对于数组和元组),但可以是动态的(如果涉及切片)。
- 用于表示关系或集合的数据,支持模式匹配和解构。
-
Rust 中的复合类型示例(聚焦于原始复合类型):
- 元组类型(Tuples) :一个固定长度的异构集合(元素类型可以不同)。例如,let tup: (i32, f64, char) = (42, 3.14, 'A');。你可以解构它:let (a, b, c) = tup;。
- 数组类型(Arrays) :一个固定长度的同构集合(所有元素类型相同)。例如,let arr: [i32; 3] = [1, 2, 3];。数组的大小在编译时必须已知。
- 注意:Rust 的原始复合类型主要限于元组和数组。更高级的复合类型如结构体(structs)和枚举(enums)是用户定义的类型(derived types),但它们基于这些基础构建。
数字
对标js中的number,rust中分为了8,16,32,64,128位整数/无符号整形(只能正数)/浮点数,分别以i,u,f开头,比如i32,f64,u128。
除了这些,还有两个特殊的类型,isize和usize。集合类型(数组,向量,字符串等)的下标,长度只能用unsize,isize可用于指针偏移量。
pub fn number_test() {
int_test();
float_test();
spec_num_test();
}
fn int_test() {
let decimal = 98_222; // 十进制:98,222
let hex = 0xff; // 十六进制:255
let octal = 0o77; // 八进制:63
let binary = 0b1111_0000; // 二进制:240
let byte = b'A'; // 字节:65(仅限 u8)
let with_type = 42u32; // 指定类型:42 as u32
println!(
"{},{},{},{},{},{}",
decimal, hex, octal, binary, byte, with_type
)
}
fn float_test() {
let x = 2.0; // f64(默认)
let y: f32 = 3.0; // f32
let z = 1.0e10; // 科学计数法:1.0 × 10^10
let inf = f32::INFINITY; // 正无穷大
let neg_inf = f32::NEG_INFINITY; // 负无穷大
let nan = f32::NAN; // 非数字
println!("{},{},{},{},{},{}", x, y, z, inf, neg_inf, nan);
}
fn spec_num_test() {
let max = f32::MAX; // 最大正值:3.4028235e38
let min = f32::MIN; // 最小负值:-3.4028235e38
let epsilon = f64::EPSILON; // 1.0 和下一个可表示数的差值
// 检查特殊值
let x: f64 = 0.0 / 0.0;
println!("{},{}", max, min);
println!("{}", (0.1 + 0.2) - 0.3 < epsilon); //true
println!("is NaN: {}", x.is_nan()); // true
println!("is infinite: {}", x.is_infinite()); // false
println!("is finite: {}", x.is_finite()); // false
}
数字的常用方法
以上代码中使用了is_nan,is_infinite等方法,js中数字和数学相关的方法在Number和Math对象上,而rust则可以直接从数字本身上调用相关方法。
所有的方法都可以从标准库文档中查阅。
和包装类的区别
这个设计有点类似js或java中的自动包装类,但它本质上完全不是包装类的实现。
在 Rust 中,所有基本类型(如 i32, f64)都是直接在栈上的值,没有“包装类”的概念。
你能直接在它们上面调用方法,主要归功于Rust编译器强大的 自动引用/解引用机制 和 特质(Trait)系统。
这是本系列文章第一次讲到引用机制,如果不能理解下面的内容,完全没有关系,只要知道这里并不是自动包装类的实现即可。
🧠 核心机制:编译器在幕后做了什么?
当你写下 5.to_string() 时,编译器会为你自动处理以下步骤:
-
自动借用:由于
to_string方法通常以&self作为第一个参数(需要一个引用),所以5会被自动借用,变为&5。 -
方法查找与分发:编译器知道
5是i32类型。然后它会去查找i32类型实现的所有特质。它发现i32实现了ToString特质,而ToString特质里定义了to_string(&self)方法。于是,编译器将你的调用静态地解析为:<i32 as ToString>::to_string(&5)这个转换在编译时就已完成,没有任何运行时开销。
一个更清晰的例子:
(-1.01_f64).floor() 的完整转换路径是:
.操作触发方法调用。- 编译器发现
-1.01_f64是f64类型。 - 查找发现,
f64实现了std::ops::Deref(使其可以获得更多方法),并且直接定义了floor方法。 - 最终调用被静态解析,等价于
f64::floor(-1.01_f64)。
布尔
rust中的bool的使用和js基本一致
let t = true;
let f: bool = false; // 显式类型注解
// 逻辑运算
let and = true && false; // false
let or = true || false; // true
let not = !true; // false
// 比较运算(返回布尔值)
let x = 5;
let y = 10;
let result = x < y; // true
字符
如果你还了解java,那完全可以对照java中的char来理解。
Rust中的字符类型char表示一个Unicode标量值,占用4个字节。这意味着它可以表示比ASCII更广泛的字符,包括中文、日文、韩文、emoji等。
let c = 'z'; // 单引号,字符字面量
let z = 'ℤ'; // Unicode 字符
let unicode = '\u{2764}'; // Unicode:'❤'
let hanzi = '布';
let heart_eyed_cat: char = '😻'; // Emoji
// 转义字符
let newline = '\n'; // 换行
let tab = '\t'; // 制表符
let single_quote = '\''; // 单引号
let backslash = '\\'; // 反斜杠
let hex_char = '\x41'; // 十六进制:'A'
println!(
"{},{},{},{},{},{},{},{},{},{}",
c, z, heart_eyed_cat, newline, tab, single_quote, backslash, hex_char, unicode, hanzi
);
// 字符方法
let c = 'A';
println!("is alphabetic: {}", c.is_alphabetic()); // 是字母,true
println!("is digit: {}", c.is_digit(10)); // true(十进制中是数字)
println!("to lowercase: {}", c.to_lowercase()); // 'a'
如果你更熟悉js,那么很自然地将char当成使用单引号的单个字符的字符串,虽然表现上相似,但不管怎样,不要将char看成字符串,这是两个类型。
str
这一节介绍str类型,细心的读者会发现,前面章节标题都是中文,但本节的标题是str而不是字符串。
从语言层面上来说,str就是rust的字符串类型,但在rust开发中说到字符串类型,一般是&str或String。&str和String最直接的区别是,&str不拥有数据,不可改变数据,而String是拥有数据且可改变数据。
The
strtype, also called a ‘string slice’, is the most primitive string type. It is usually seen in its borrowed form,&str. It is also the type of string literals,&'static str.
str 是 Rust 中的字符串切片类型(string slice),通常以引用的形式出现:&str。它是不可变的 UTF-8 编码字节序列的视图。——相比char,字符串中的字符所占的字节数是变化的(1 - 4) ,这样有助于大幅降低字符串所占用的内存空间。
这里出现了一个符号&,Rust中的&表示 引用(Reference),它是一个指向数据的指针,但不是拥有数据的所有权,而&'static str中的static则表示了它的生命周期。
rust中的str揭开了这门语言复杂性,相对于前面的内容,字符串的掌握难度将陡然上升,但它也是进入rust世界的一个非常好的切入点。
可以先不管其他的概念,先了解如何创建一个字符串。
字符串创建
1. 字面量创建
let s: str = "hello"; //❌ 错误
// str 通常以 &str 的形式使用(引用)
let s: &str = "Hello, Rust!"; // 字符串字面量就是 &str 类型
let multi_line = r#"这是一个
多行
原始字符串"#;
2. 从String创建(借用)
let owned = String::from("Hello");
// 整个字符串的切片
let slice1: &str = &owned; // 隐式转换
let slice2: &str = owned.as_str(); // 显式方法
// 部分字符串的切片
let slice3 = &owned[0..5]; // "Hello"
let slice4 = &owned[..]; // 整个字符串
3. 其他形式创建
// 从 数组创建
let vec_bytes = vec![72, 101, 108, 108, 111]; // "Hello" 的 ASCII
let str_from_vec = std::str::from_utf8(&vec_bytes).unwrap();
let words = vec!["Hello", "World", "Rust"];
let sentence = words.join(" "); // 用空格连接
let fruits = ["Apple", "Banana", "Orange"];
let result = fruits.join(", ");
// 从 char 创建
let ch = 'A';
let str_from_char = ch.to_string().as_str(); // 需要先转 String
字符串常用操作
1. 拼接
// 拼接字符串
let s1: &str = "Hello";
let s2: &str = " World";
let combined: String = s1.to_string() + s2; // 返回 String
let combined_str = combined.as_str(); //String 转&str
&str无法直接拼接,需要转为String,拼接后的字符串类型也是String。
2. 长度和容量相关
let s = "Hello 🦀";
// 字节数(UTF-8 编码)
println!("字节数: {}", s.len()); // 10(Hello=5, 🦀=4, 空格=1)
// 字符数(Unicode 标量值)
println!("字符数: {}", s.chars().count()); // 7
// 是否为空
println!("是否为空: {}", s.is_empty()); // false
// 判断是否以字节序列开始/结束
println!("是否以'H'开始: {}", s.starts_with('H')); // true
println!("是否以'🦀'结束: {}", s.ends_with('🦀')); // true
3. 搜索和查找
let s = "Hello, Rust Programming!";
// 查找字符
if let Some(pos) = s.find('R') {
println!("找到 'R' 在位置: {}", pos); // 7
}
// 查找字符串
if let Some(pos) = s.find("Prog") {
println!("找到 'Prog' 在位置: {}", pos); // 12
}
// 检查包含关系
println!("包含 'Rust'?: {}", s.contains("Rust")); // true
// 匹配模式
let matches: Vec<_> = s.match_indices("o").collect();
println!("所有 'o' 的位置: {:?}", matches); // [(4, "o"), (14, "o")]
4. 分割
let s = "apple,banana,orange,grape";
// 按字符分割
let fruits: Vec<&str> = s.split(',').collect();
println!("水果: {:?}", fruits); // ["apple", "banana", "orange", "grape"]
// 按字符串分割
let s2 = "apple::banana::orange";
let fruits2: Vec<&str> = s2.split("::").collect();
for f in fruits2 {
println!("{}", f);
}
// 按闭包条件分割
let s3 = "apple1banana2orange3";
let fruits3: Vec<&str> = s3.split(char::is_numeric).collect();
for f in fruits3 {
println!("{}", f);
}
// 保留分割符
let s4 = "apple,banana,orange";
let parts: Vec<&str> = s4.split_inclusive(',').collect();
println!("包含分隔符: {:?}", parts); // ["apple,", "banana,", "orange"]
// 按行分割
let text = "Line 1\nLine 2\r\nLine 3";
for line in text.lines() {
println!("行: {}", line);
}
4. 大小写转换
// 转为大写
let upper = s.to_uppercase(); // 返回 String
println!("大写: {}", upper); // "HELLO RUST!"
// 转为小写
let lower = s.to_lowercase(); // 返回 String
println!("小写: {}", lower); // "hello rust!"
5. trim
let s = " Hello, Rust! ";
println!("原始: '{}'", s);
println!("修整两边空白: '{}'", s.trim()); // "Hello, Rust!"
println!("修整左边空白: '{}'", s.trim_start()); // "Hello, Rust! "
println!("修整右边空白: '{}'", s.trim_end()); // " Hello, Rust!"
6. 替换和修改
替换后返回的都是String类型
let s = "I like cats and cats are cute";
// 替换所有匹配
let replaced = s.replace("cats", "dogs"); // 返回 String
println!("替换后: {}", replaced); // "I like dogs and dogs are cute"
// 只替换第一个
let replaced_n = s.replacen("cats", "dogs", 1);
println!("替换第一个: {}", replaced_n); // "I like dogs and cats are cute"
// 替换范围
let mut s_string = String::from(s);
s_string.replace_range(7..11, "dogs"); // 修改 String,不是 &str
println!("替换范围: {}", s_string);
关于字符串还有很多内容需要进一步学习,但入门的话只要大致掌握以上内容即可。等学完Rust的一些特殊语法再回头来看字符串,就会觉得它非常简单了。
切片
切片(Slice) 是 Rust 中对集合中一段连续元素序列的引用。切片没有所有权,只是数据的视图。
切片是一个胖指针(fat pointer),包含两个部分:指向数据的指针和切片的长度(元素个数)。在内存中,切片引用(如&[T])占用两个机器字(在64位系统上是16字节)。
&str是就是一种特殊的切片,它保证是有效的UTF-8。字符串切片与普通切片类似,但针对字符串进行了优化。
切片的类型表示为[T],其中T是元素类型。由于切片是动态大小类型(DST),我们通常使用其引用:&[T](不可变切片)或&mut [T](可变切片)。
切片的常用操作
1. 切片创建和修改
let arr = [1, 2, 3, 4, 5];
// 完整切片
let full_slice: &[i32] = &arr[..]; // [1, 2, 3, 4, 5]
// 部分切片
let middle = &arr[1..4]; // [2, 3, 4](包含1,不包含4)
println!("完整: {:?}", full_slice);
println!("中间: {:?}", middle);
let vec = vec![10, 20, 30, 40, 50];
// 从 Vec 借用切片
let slice1: &[i32] = &vec; // 自动解引用转换
let slice2: &[i32] = &vec[..]; // 显式切片,结果和自动解引用一致,但过程不一样
println!("向量切片: {:?}", slice1);
println!("显式切片: {:?}", slice2);
let s = String::from("Hello, Rust!");
// 字符串切片是 &str,它是 &[u8] 的特殊形式
let str_slice: &str = &s[..]; // 整个字符串
println!("{}", str_slice);
println!("可变切片");
let mut data = vec![1, 2, 3, 4, 5];
let slice: &mut [i32] = &mut data[1..4]; // 可变切片[2,3,4]
// 可以修改元素
slice[0] = 20; // 修改第一个元素 ,[20,3,4]
slice.swap(0, 2); // 交换元素 [4,3,20]
slice.reverse(); // 反转切片[20,3,4]
println!("修改后的数据: {:?}", data); // [1, 20, 3, 4, 5]
2. 切片搜索
let slice = &[10, 20, 30, 20, 40, 50];
// 线性搜索
if let Some(pos) = slice.iter().position(|&x| x == 20) {
println!("第一个20在位置: {}", pos); // 1
}
// 从后向前搜索
if let Some(pos) = slice.iter().rposition(|&x| x == 20) {
println!("最后一个20在位置: {}", pos); // 3
}
// 二分搜索(需要排序)
let sorted = &[10, 20, 30, 40, 50];
match sorted.binary_search(&30) {
Ok(pos) => println!("30在位置: {}", pos), // 2
Err(pos) => println!("30应该在位置: {}", pos),
}
// 查找满足条件的元素
if let Some(&value) = slice.iter().find(|&&x| x > 25) {
println!("第一个大于25的数: {}", value); // 30
}
数组
数组是Rust中固定大小的、相同类型的元素的集合,存储在栈上。
数组的类型表示为[T; N],其中T是元素类型,N是数组长度,且N在编译时必须已知。
特点:
- 固定长度:一旦声明,长度不能改变。
- 元素类型相同。
- 存储在连续的内存区域(栈上)。
- 通过索引访问,索引从0开始。
数组和切片的区别: 数组长度固定,切片长度动态。数组存储在栈上(除非被移动),切片是引用,指向一段连续内存。
数组的常用操作
1. 数组创建和元素访问
// 完整初始化
let arr1 = [1, 2, 3, 4, 5];
// 重复值初始化(最常用)
let zeros = [0; 10]; // [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
println!("完整: {:?}", arr1);
println!("零数组: {:?}", zeros);
let arr = [10, 20, 30, 40, 50];
// 读取元素
let first = arr[0]; // 10
// 安全访问
if let Some(value) = arr.get(2) {
println!("安全访问第三个元素: {}", value); // 30
}
let point = [10.0, 20.0, 30.0];
// 解构数组
let [x, y, z] = point;
println!("坐标: x={}, y={}, z={}", x, y, z);
// 部分解构
let [first, .., last] = [1, 2, 3, 4, 5];
println!("第一个: {}, 最后一个: {}", first, last);
2. 数组遍历
let arr = [1, 2, 3, 4, 5];
// 1. 使用 for 循环(借用)
println!("for 循环(借用):");
for &item in &arr {
print!("{} ", item); // 1 2 3 4 5
}
println!();
// 2. 使用 for 循环(直接迭代)
println!("for 循环(直接):");
for item in arr {
print!("{} ", item); // 1 2 3 4 5
}
println!();
// 3. 使用 iter() 方法
println!("iter() 方法:");
for item in arr.iter() {
print!("{} ", item); // 1 2 3 4 5
}
println!();
// 4. 使用迭代器方法
let sum: i32 = arr.iter().sum();
let doubled: Vec<i32> = arr.iter().map(|&x| x * 2).collect();//后文将更详细介绍迭代器
println!("总和: {}", sum); // 15
println!("加倍后: {:?}", doubled); // [2, 4, 6, 8, 10]
元组
元组(Tuple) 是一种可以存放多个不同类型的值的数据类型,它将这些值组合成一个整体。元组的值是有顺序的,可以通过位置索引访问。
- 语法:在小括号内用逗号分隔多个元素。例如:
(i32, f64, char) - 元组长度是固定的,创建时就确定不可变。
- 元组可以包含任意数量、任意类型的元素。
let tup = (500, 6.4, 1);
let (x, y, z) = tup; // 解构,分别绑定到 x, y, z
单元
Rust中的()居然也是一个类型,这个类型就是单元(),这个类型只有一个值,也是()。
// 没有返回值的函数实际上返回 ()
fn say_hello(name: &str) { // 等价于 -> ()
println!("Hello, {}!", name);
// 隐式返回 ()
}
// 表达式语句的值是 ()
let x = {
let y = 5;
y + 1 // 表达式,返回 6
}; // x = 6
let z = {
let y = 5;
y + 1; // 语句,返回 ()
}; // z = ()
never
Never 类型 ! 是 Rust 中表示永不返回的计算的类型。它是零大小类型,表示一个永远不会产生值的计算。
fn infinite_loop() -> ! {
loop {
println!("永远循环...");
}
}
never类型的关键特性是可以强制转换为任何类型。
fn not_implemented_yet() -> String {
todo!("还没实现这个功能"); // 返回 !,但函数签名要求 String
}
reference和pointer
Rust 中的 Pointer(指针)和 Reference(引用)是内存操作与所有权模型的核心概念,本节简单介绍这两者含义,后续将详细解析Rust的所有权模型。
1. Reference(引用)
- 引用是对已有值的“借用”,不会获得所有权,因此不会销毁值。
- 语法:
&T(不可变引用),&mut T(可变引用)
let x = 42;
let r: &i32 = &x; // 不可变引用
println!("r: {}", r);
let mut y = 5;
let m: &mut i32 = &mut y; // 可变引用
*m += 1;
println!("y: {}", y); //6
2. Pointer(指针)
指针是更底层的内存地址。Rust 有多种指针类型,各自有不同的安全级别和用途。
裸指针(raw pointer)
*const T 和 *mut T
*const T:不可变原生指针(裸指针)*mut T:可变原生指针(裸指针)- 没有自动生命周期检查、不保证安全,需要
unsafe代码使用。
智能指针(smart pointer)
智能指针是指针的升级版,也实际拥有或管理内存,常见有:
- Box :堆分配,所有权明确,单一所有者
- Rc (Reference Counted):引用计数,适合单线程多所有者
- Arc (Atomic Reference Counted):多线程安全的引用计数
- RefCell 、Mutex 等:运行时可变性的智能指针