第 3 章:数据类型——不只是 `int` 和 `String`

在上一章中,我们通过 letmut 揭示了 Rust 在变量设计上的核心哲学——默认安全。现在,让我们将目光投向这些变量所承载的“内容”:数据类型

对于 Java 开发者来说,我们对 intdoublebooleanString 等类型了如指掌。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 代表无符号(只能为非负数)。
  • isizeusize 的位数取决于你的计算机架构(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 类型只有两个值:truefalse

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 的情况下,通过类型系统来管理资源,实现内存安全。

我们已经了解了数据是如何存储和分类的。接下来,我们自然会问:如何根据这些数据来控制程序的执行路径呢?

在下一章 《控制流——ifformatch 的新花样》 中,我们将看到 Rust 如何将 iffor 等我们熟悉的结构变成更强大、更安全的“表达式”,并认识一下 switch 语句的“超进化”形态——match