在上一章中,我们通过 let
和 mut
揭示了 Rust 在变量设计上的核心哲学——默认安全。现在,让我们将目光投向这些变量所承载的“内容”:数据类型。
对于 Java 开发者来说,我们对 int
、double
、boolean
和 String
等类型了如指掌。Rust 也有类似的类型,但其设计中蕴含着对内存和性能更精细的控制。更重要的是,通过探讨 Rust 的字符串,我们将首次与“所有权”系统进行正面交锋。
标量类型(Scalar Types)- 精确的基石
标量类型代表一个单一的值。Rust 提供了四种主要的标量类型。
1. 整型(Integers)
在 Java 中,我们有 byte
(8 位), short
(16 位), int
(32 位), long
(64 位),它们都是有符号的。
Rust 则提供了更丰富的选择,并且严格区分有符号(signed)和无符号(unsigned):
位数 | 有符号 | 无符号 | Java 对应 |
---|---|---|---|
8-bit | i8 |
u8 |
byte |
16-bit | i16 |
u16 |
short |
32-bit | i32 |
u32 |
int |
64-bit | i64 |
u64 |
long |
128-bit | i128 |
u128 |
BigInteger (概念上) |
arch | isize |
usize |
无 |
i
代表有符号(可以为负),u
代表无符号(只能为非负数)。isize
和usize
的位数取决于你的计算机架构(32 位系统上是 32 位,64 位系统上是 64 位)。usize
特别重要,因为它被用作集合的索引和长度,能够保证表示内存中任何对象的大小。
思维转变: Rust 强迫你在声明时就思考数据的取值范围,选择最合适的类型。这不仅仅是为了节省内存,更是为了代码的准确性。想用一个变量表示网页的 HTTP 状态码?u16
(0-65535) 远比 i32
更贴切。
2. 浮点型(Floating-Point Numbers)
这部分和 Java 很像:
f32
:单精度浮点,相当于 Java 的float
。f64
:双精度浮点,相当于 Java 的double
。
默认类型是 f64
,因为在现代 CPU 上,它的速度与 f32
相差无几,但精度更高。
3. 布尔型(Booleans)
与 Java 完全一样,bool
类型只有两个值:true
和 false
。
4. 字符型(Characters)
这里有一个重要的区别!
- Java 的
char
是一个 16 位(2 字节)的 UTF-16 编码单元。 - Rust 的
char
是一个 32 位(4 字节)的 Unicode 标量值(Unicode Scalar Value)。
这意味着 Rust 的 char
可以表示任何一个 Unicode 字符,包括各种语言的文字、符号,甚至是 Emoji(例如 '😂'
是一个合法的 char
)。
let c = 'Z';
let z = 'ℤ';
let heart_eyed_cat = '😻'; // 这都是合法的 char
思维转变: Rust 在语言底层就对 Unicode 提供了更完善的支持,避免了 Java 中处理复杂字符(如需要两个 char
才能表示的 Emoji)时可能遇到的麻烦。
复合类型(Compound Types)- 数据的结构
复合类型可以将多个值组合成一个类型。
1. 元组(Tuple)
元组是一个你可能在 Java 中不熟悉的概念。它是一种将多个不同类型的值组合进一个复合类型的通用方式。元组的长度是固定的,一旦声明,就不能改变大小。
fn main() {
let tup: (i32, f64, u8) = (500, 6.4, 1);
// 解构元组
let (x, y, z) = tup;
println!("The value of y is: {}", y); // 输出 6.4
// 通过索引访问
let five_hundred = tup.0;
println!("The value of tup.0 is: {}", five_hundred); // 输出 500
}
元组非常适合用于函数需要返回多个值的场景,避免了在 Java 中需要为此专门创建一个新类的繁琐。
2. 数组(Array)
Rust 的数组与 Java 的数组有一个根本性的区别:
- Java 的数组是存储在**堆(Heap)**上的对象,长度可变(虽然引用本身指向的对象大小固定)。
- Rust 的数组长度是固定的,且数据是存储在**栈(Stack)**上的。
// 数组的类型签名是 [type; length]
let a: [i32; 5] = [1, 2, 3, 4, 5];
let first = a[0]; // 访问方式相同
因为数组长度固定且存储在栈上,所以它非常快。当你确定元素的数量不会改变时(比如一周有七天),数组是绝佳的选择。对于像 Java 的 ArrayList
那样可动态增长的集合,Rust 提供了 Vector
(我们将在后续章节介绍)。
核心对决:String
vs &str
- 所有权的初体验
现在,我们来到了本章最关键的部分,也是你作为 Java 开发者最需要进行思维转变的地方。
在 Java 中,String
是一个我们每天都在使用的类。它是一个不可变的对象,由 GC 负责管理内存,我们用起来非常省心。
Rust 则将字符串分为了两种主要类型,理解它们的区别是通往地道 Rustacean(Rust 程序员)之路的第一个重要关口。
&str
:字符串切片(String Slice)
&str
(读作 “string slice”) 是一种引用类型。它本身并不拥有数据,而是“借用”了别处的数据。你可以把它想象成一个带有长度的指针,指向一块 UTF-8 编码的字节序列。
- 它很轻量:因为它只包含一个地址和长度信息。
- 它不可变:你不能通过一个
&str
来修改它所指向的字符串。 - 它无处不在:你代码中所有的字符串字面量(
"Hello, world!"
)都是&str
类型。更准确地说,它们的类型是&'static str
,意味着它们的数据被直接硬编码在程序的可执行文件里,生命周期和整个程序一样长。
String
:可增长的字符串
String
是一个拥有其数据的类型。
- 它的数据存储在**堆(Heap)**上,因此它可以动态地增长和修改。
- 它是可变的(如果用
mut
声明)。 - 它拥有自己的数据。这意味着当一个
String
变量离开作用域时,Rust 会自动调用代码释放它在堆上占用的内存。这就是所有权规则在起作用!
核心对比与思维转变
特性 | String |
&str (String Slice) |
Java String 类比 |
---|---|---|---|
所有权 | 拥有数据 | 借用数据 | 由 GC 拥有和管理 |
存储位置 | 堆 (Heap) | 任意位置(栈、堆、静态数据区) | 堆 (Heap) |
可变性 | 可变 (需mut ) |
不可变 | 不可变 |
用途 | 需要构建、修改、或拥有字符串时 | 仅需读取或传递字符串视图时 | 通用 |
一个绝佳的类比:
String
就像是你电脑硬盘里的一份 Word 文档原件。你可以编辑它、加长它、删除内容。你拥有这份文件。&str
就像是你把这份文档用微信的“只读模式”分享给朋友的链接。朋友可以看,但不能改。这个链接本身很小,不包含文档的实际内容。
fn main() {
// 从 &str 创建 String (类似 new String("..."))
// 这会在堆上分配内存并复制数据
let s1: String = String::from("hello");
let s2: String = "world".to_string();
// 从 String 获取 &str (借用)
// 这是一个非常廉价的操作,没有内存分配
let s1_slice: &str = &s1;
println!("s1: {}, s1_slice: {}", s1, s1_slice);
// 函数应该优先接收 &str,因为它更通用
// 这个函数既可以接收 String (通过&s1),也可以接收 &str (通过s1_slice)
print_string(&s1);
print_string(s1_slice);
}
fn print_string(s: &str) {
println!("{}", s);
}
关键思想: Rust 强迫你思考:你的函数是真的需要“拥有”一份可变的字符串数据,还是仅仅需要“读取”一下字符串的内容?在 Java 中,我们总是传递 String
对象的引用,GC 帮我们处理了一切。在 Rust 中,通过区分 String
和 &str
,我们获得了在编译期就确定的、无需 GC 的、对内存使用的精确控制。
本章小结
我们今天探索了 Rust 的数据世界。从基础的标量类型到复合的元组和数组,我们看到 Rust 在类型设计上对精确性和内存布局的重视。
最重要的是,通过 String
和 &str
的对决,我们第一次具体地触碰到了所有权和借用这两个核心概念。这个区别不仅仅是语法,它是一种全新的编程范式:在没有 GC 的情况下,通过类型系统来管理资源,实现内存安全。
我们已经了解了数据是如何存储和分类的。接下来,我们自然会问:如何根据这些数据来控制程序的执行路径呢?
在下一章 《控制流——if
、for
和 match
的新花样》 中,我们将看到 Rust 如何将 if
和 for
等我们熟悉的结构变成更强大、更安全的“表达式”,并认识一下 switch
语句的“超进化”形态——match
。