内置类型
Rust 的内置类型(也叫原始类型 primitive type)是语言自带的基本类型, 不需要 use 导入, 直接就能用, 它们是所有其他类型的基础
标量类型
标量类型只代表一个值, 分为四类: 整数, 浮点数, 布尔, 字符
| 有符号整数 | 无符号整数 | 大小 |
|---|---|---|
| i8 | u8 | 1 字节 |
| i16 | u16 | 2 字节 |
| i32 | u32 | 4 字节 |
| i64 | u64 | 8 字节 |
| i128 | u128 | 16 字节 |
| isize | usize | 跟系统位数有关(64 位系统 8 字节) |
| 类型 | 说明 |
|---|---|
| f32 | 单精度浮点数, 4 字节 |
| f64 | 双精度浮点数, 8 字节(默认浮点类型) |
| bool | 布尔值, true / false, 1 字节 |
| char | 单个 Unicode 字符, 4 字节(注意不是 1 字节) |
为什么
char占 4 个字节?因为
char存储的是 Unicode 标量值(Unicode Scalar Value), 要能表示全世界的所有字符(包含中文/emoji), 4 字节才够用, 跟 C 语言的char(1 字节)完全不同
复合类型
复合类型由多个值组合而成
| 类型 | 说明 |
|---|---|
| tuple | 元组, 可以放不同类型的值: (i32, bool, &str), 大小 = 各字段之和 |
| array | 数组, 只能放相同类型的值, 长度固定: [i32; 3] |
| slice | 切片, 对连续内存的一块"视图", 没有所有权: &[i32] |
| str | 字符串切片, 一般以 &str 形式出现 |
其他内置类型
| 类型 | 说明 |
|---|---|
| fn | 函数指针, 可以像值一样传递函数 |
| unit 单元类型 () | 空元组, 表示"什么都没有", 占 0 字节, 函数无返回值时返回的就是它 |
| 裸指针 | *const T / *mut T, 不安全代码中使用 |
类型的大小
可以用 std::mem::size_of 查看每种类型占用的字节数
fn main() {
use std::mem::size_of;
// 标量类型 (64 位系统)
println!("i8: {} 字节", size_of::<i8>()); // 1
println!("i32: {} 字节", size_of::<i32>()); // 4
println!("i64: {} 字节", size_of::<i64>()); // 8
println!("f64: {} 字节", size_of::<f64>()); // 8
println!("bool: {} 字节", size_of::<bool>()); // 1
println!("char: {} 字节", size_of::<char>()); // 4
// 复合类型
println!("(i32, bool): {} 字节", size_of::<(i32, bool)>()); // 8
println!("[i32; 4]: {} 字节", size_of::<[i32; 4]>()); // 16
// 单元类型: 不占任何空间
println!("(): {} 字节", size_of::<()>()); // 0
}类型别名
type 关键字可以给已有类型起一个"外号", 方便书写和阅读
// 语法: type 别名 = 原类型;
type Age = u8; // 年龄用 u8 就够
type UserId = u64; // 用户 id
type FnType = fn(i32, i32) -> i32; // 函数指针类型
fn add(a: i32, b: i32) -> i32 {
a + b
}
fn main() {
// 别名可以直接当原类型使用
let age: Age = 18;
let id: UserId = 1001;
println!("age = {age}, id = {id}"); // age = 18, id = 1001
// 别名简化了函数指针类型的写法
let f: FnType = add;
println!("{}", f(1, 2)); // 3
}别名不是新类型
别名只是原类型的"代号", 它们完全等价, 可以互相赋值; 如果想要一个全新的类型, 需要用"新类型模式"(元组结构体)
type UserId = u64;
fn main() {
let a: UserId = 1;
let b: u64 = 2;
// 别名和原类型完全等价, 直接互相赋值没问题
let c: u64 = a;
let d: UserId = b;
println!("{c} {d}"); // 1 2
}// 元组结构体: 定义了一个全新的类型
struct UserId(u64);
fn main() {
let a = UserId(1);
let b: u64 = 2;
// 编译报错: 新类型和原类型不能直接互相赋值
// let c: u64 = a;
// let d = UserId(b);
// 必须手动取出内部的值
let c: u64 = a.0;
println!("{c}"); // 1
}使用场景
use std::collections::HashMap;
// 1. 简化复杂的泛型类型(重复写太长了)
type UserMap = HashMap<String, u64>;
// 2. 简化闭包/回调类型
type Callback = Box<dyn Fn(i32) -> i32>;
fn main() {
// 使用别名
let mut users: UserMap = HashMap::new();
users.insert("tom".to_string(), 18);
let cb: Callback = Box::new(|x| x * 2);
println!("{}", cb(10)); // 20
}注意点
- 别名是"零成本"的, 编译后跟原类型一模一样, 不会带来任何运行时开销
- 但别名没有类型安全:
Age和UserId如果都是u8, 互相传参编译器不会报错 - 需要类型安全(比如两个 id 不能混用), 请用新类型模式(元组结构体)
类型转换
Rust 的类型转换分为两大类: 隐式转换(编译器自动做) 和 显式转换(手动写)
| 转换方式 | 类型 | 特点 | 典型场景 |
|---|---|---|---|
| as 关键字 | 显式 | 只支持基本类型(数值/bool/char/指针) | 数值窄化/拓宽 |
| From/Into | 显式 | 所有权转换, 实现 From 自动获得 Into | 自定义类型转换 |
| TryFrom/TryInto | 显式 | 可能失败的转换, 返回 Result | 数值窄化(可能溢出) |
| AsRef/AsMut | 显式 | 引用之间的转换, 零拷贝, 只换"视角" | 函数参数接收多种类型 |
| FromStr/parse | 显式 | 字符串 -> 其他类型, 可能失败 | 字符串解析 |
| Display/ToString | 显式 | 其他类型 -> 字符串 | 类型转字符串 |
| Deref 强制转换 | 隐式 | 编译器自动解引用, 零成本 | &String -> &str 等 |
隐式转换
Rust 的隐式转换很少(这也是它安全的原因之一), 常见的只有这几种:
- 解引用强制转换:
&String->&str,&Vec<T>->&[T](详细见标准库 trait) - 可变性提升:
&mut T自动变成&T - 借用自动发生: 函数参数是
&T时, 传T会自动加& - 数字字面量默认类型: 整数默认
i32, 浮点数默认f64
fn main() {
// 1. 数字字面量的默认类型
let x = 10; // i32
let y = 3.14; // f64
// 2. 可变性提升: &mut T 自动变成 &T
let mut s = String::from("hello");
fn takes_str(s: &str) {
println!("{s}");
}
// 传 &mut String 也能编译(先降级为 &String, 再解引用为 &str)
takes_str(&mut s); // hello
// 3. 解引用强制转换: &String -> &str
takes_str(&s); // 传 &String 也能编译
// 4. 解引用强制转换: &Vec<i32> -> &[i32]
let v = vec![1, 2, 3];
fn takes_slice(s: &[i32]) {
println!("{s:?}");
}
takes_slice(&v); // 传 &Vec<i32> 也能编译
}注意点
Rust 不会做以下隐式转换(其他语言会, 容易踩坑):
- 整数不会自动变浮点数:
let f: f64 = 10;编译报错 - 小整数不会自动变大整数:
let b: u32 = 10u8;编译报错 &str不会自动变成String(需要显式to_string())
as 显式转换
as 关键字是最直接的类型转换, 只支持基本类型, 语法: 表达式 as 类型
| 转换 | 示例 | 说明 |
|---|---|---|
| 数值拓宽 | 10u8 as u32 | 小类型 -> 大类型, 值不变 |
| 数值窄化 | 300u32 as u8 | 大类型 -> 小类型, 溢出部分被截断 |
| 有符号 <-> 无符号 | -1i32 as u32 | 按二进制位重新解释, 值会变 |
| float -> 整数 | 3.99f64 as i32 | 直接截断小数部分 |
| 整数 -> float | 1i32 as f64 | 可能存在精度损失 |
| bool -> 整数 | true as u8 | true = 1, false = 0 |
| char -> 整数 | 'A' as u32 | 得到字符的 Unicode 码点 |
| 引用 -> 裸指针 | &x as *const i32 | 拿到内存地址(不安全代码用) |
fn main() {
// 拓宽: 小类型 -> 大类型, 值不变
let a: u8 = 255;
let b = a as u32;
println!("{b}"); // 255
// 窄化: 大类型 -> 小类型, 溢出部分被截断
let c: u32 = 300;
let d = c as u8;
println!("{d}"); // 44 (300 超过 u8 范围, 取余 256)
// bool -> 整数
println!("{}", true as u8); // 1
println!("{}", false as u8); // 0
// float -> 整数: 小数部分直接截断(不是四舍五入)
let pi = 3.99f64 as i32;
println!("{pi}"); // 3
// 有符号 -> 无符号: 按二进制位重新解释
let n: i32 = -1;
let m = n as u32;
println!("{m}"); // 4294967295
// char -> 整数: 得到 Unicode 码点
let code = 'A' as u32;
println!("{code}"); // 65
}as 转换的注意点
fn main() {
// 编译报错: non-primitive cast: `&str` as `i32`
// let n = "123" as i32;
// 字符串 -> 数字必须用 parse()
let n: i32 = "123".parse().unwrap();
println!("{n}"); // 123
}fn main() {
// 300 的二进制是 1 0010 1100 (9 位)
// 截断成 u8(8 位): 0010 1100 = 44
let big: u32 = 300;
let small = big as u8;
println!("{small}"); // 44
}fn main() {
// u8 的 200 二进制是 1100 1000
// 按 i8 解释(补码): 最高位是符号位, 结果是负数
let u: u8 = 200;
let i = u as i8;
println!("{i}"); // -56
}结论: 在数值之间转换用
as没问题, 但要注意窄化会丢数据; 如果担心溢出, 应该用下面的TryFrom/TryInto
标准库 trait
std::convert 模块提供了类型转换的核心 trait, 自定义类型的转换应该实现这些 trait, 而不是依赖 as
From 和 Into
From<T> for U: 定义了从T转换到U的规则, 调用方式是U::from(t)Into<U> for T: 是From的反向, 调用方式是t.into()
只要实现了
From, 就会自动获得Into, 所以一般只需要实现From一个就行
fn main() {
// 标准库已经实现好的转换
// &str -> String
let s1 = String::from("hello");
let s2: String = "hello".into();
println!("{s1} {s2}"); // hello hello
// u8 -> u32
let n1 = u32::from(10u8);
let n2: u32 = 10u8.into();
println!("{n1} {n2}"); // 10 10
}自定义类型实现 From:
struct Celsius(f64); // 摄氏度
struct Fahrenheit(f64); // 华氏度
// 实现 From: 华氏度 -> 摄氏度
// 公式: (F - 32) * 5 / 9
impl From<Fahrenheit> for Celsius {
fn from(f: Fahrenheit) -> Self {
Celsius((f.0 - 32.0) * 5.0 / 9.0)
}
}
fn main() {
// 方式一: 用 From 的关联函数
let c = Celsius::from(Fahrenheit(212.0));
println!("{}", c.0); // 100
// 方式二: 用 .into() (实现 From 后自动获得)
let c: Celsius = Fahrenheit(212.0).into();
println!("{}", c.0); // 100
// 常见用法: 函数参数自动转换
fn show_temp(t: Celsius) {
println!("{} 摄氏度", t.0);
}
show_temp(Fahrenheit(32.0).into()); // 0 摄氏度
}转换的方向
impl From<A> for B 定义的是 A -> B 的转换:
B::from(a)得到B(目标类型B在前面)a.into()得到B(需要标注目标类型, 否则编译器不知道转成什么)
TryFrom 和 TryInto
TryFrom 是"可能失败"的 From, 转换结果用 Result 返回: 成功是 Ok, 失败是 Err
fn main() {
// u32 -> u8 可能溢出, 标准库只提供了 TryFrom (没有 From)
let big = 300u32;
// 使用 try_into(), 返回 Result
let small: Result<u8, _> = big.try_into();
match small {
Ok(v) => println!("转换成功: {v}"),
Err(e) => println!("转换失败: {e}"),
// 转换失败: out of range integral type conversion attempted
}
// 范围内的值可以成功
let ok: Result<u8, _> = 100u32.try_into();
println!("{ok:?}"); // Ok(100)
}自定义类型实现 TryFrom:
use std::convert::TryFrom;
// 校验年龄: 只接受 0-150 岁
// derive Debug 只是为了后面能用 {:?} 打印
#[derive(Debug)]
struct Age(u8);
impl TryFrom<i32> for Age {
// 必须指定错误类型
type Error = String;
fn try_from(value: i32) -> Result<Self, Self::Error> {
if value < 0 || value > 150 {
Err(format!("年龄不合法: {value}"))
} else {
Ok(Age(value as u8))
}
}
}
fn main() {
// 合法范围
let age = Age::try_from(20);
println!("{:?}", age.is_ok()); // true
// 超出范围
let age: Result<Age, _> = 200i32.try_into();
println!("{age:?}"); // Err("年龄不合法: 200")
}AsRef 和 AsMut
AsRef<T> / AsMut<T> 用于引用之间的转换, 转换前后不产生新数据, 只是换一种"视角"看同一块内存
fn main() {
let s = String::from("hello");
// &String -> &str, 零成本
let s1: &str = s.as_ref();
println!("{s1}"); // hello
// String 也实现了 AsRef<[u8]>, 可以当字节数组看
let bytes: &[u8] = s.as_ref();
println!("{bytes:?}"); // [104, 101, 108, 108, 111]
}最常用的场景: 函数参数用泛型 T: AsRef<str>, 就能同时接收 &str, String, &String, Box<str> 等多种类型
fn print_len<T: AsRef<str>>(text: T) {
// as_ref 把各种类型统一转成 &str
let s: &str = text.as_ref();
println!("长度: {}", s.len());
}
fn main() {
print_len("hello"); // 传入 &str
print_len(String::from("hello")); // 传入 String
print_len(&String::from("hello")); // 传入 &String
}FromStr 和 ToString
fn main() {
// parse(): 字符串 -> 其他类型 (返回 Result)
let n: i32 = "42".parse().unwrap(); // 字符串 -> 数字
let b: bool = "true".parse().unwrap(); // 字符串 -> bool
let f: f64 = "3.14".parse().unwrap(); // 字符串 -> float
// 解析失败时返回 Err
let result = "abc".parse::<i32>();
println!("{result:?}"); // Err(ParseIntError { kind: InvalidDigit })
// to_string(): 其他类型 -> 字符串 (要求实现 Display)
let s = 42.to_string(); // 数字 -> 字符串
let s2 = true.to_string(); // bool -> 字符串
println!("{s} {s2}"); // 42 true
}自定义类型实现 FromStr, 就能直接用 parse() 解析:
use std::str::FromStr;
// derive Debug 只是为了后面能用 {:?} 打印
#[derive(Debug)]
struct Point {
x: i32,
y: i32,
}
// 约定字符串格式: "x,y", 比如 "1,2"
impl FromStr for Point {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let parts: Vec<&str> = s.split(',').collect();
if parts.len() != 2 {
return Err(format!("格式错误, 应该是 x,y: {s}"));
}
let x = parts[0].parse().map_err(|_| "x 不是数字".to_string())?;
let y = parts[1].parse().map_err(|_| "y 不是数字".to_string())?;
Ok(Point { x, y })
}
}
fn main() {
// 实现了 FromStr 后, 就可以用 parse() 了
let p: Point = "1,2".parse().unwrap();
println!("p = ({}, {})", p.x, p.y); // p = (1, 2)
// 格式错误时返回 Err
let bad: Result<Point, _> = "abc".parse();
println!("{bad:?}"); // Err("格式错误, 应该是 x,y: abc")
}Deref 强制转换
解引用强制转换(deref coercion)是前面提到的隐式转换, 当编译器需要 &str 却发现传过来的是 &String 时, 会自动调用 String 的 Deref 实现, 拿到内部的 &str
| 类型 | Deref 到 | 自动转换示例 |
|---|---|---|
| String | str | &String -> &str |
| Vec<T> | [T] | &Vec<T> -> &[T] |
| Box<T> | T | &Box<T> -> &T |
| &T | T | &&T -> &T |
fn main() {
let s = String::from("hello");
let v = vec![1, 2, 3];
fn takes_str(s: &str) -> usize {
s.len()
}
fn takes_slice(s: &[i32]) -> usize {
s.len()
}
// 不需要手动写 &s[..] 或 &v[..]
// 编译器自动调用 Deref 完成转换
println!("{}", takes_str(&s)); // 5
println!("{}", takes_slice(&v)); // 3
// 方法调用时也会自动解引用
// s.trim() 实际调用的是 str::trim()
println!("{}", s.trim().len()); // 5
}注意点
- 解引用强制转换只发生在函数参数和方法调用的地方, 不是所有场景都生效
- 它是零成本的, 不产生新数据, 只是换了个引用
- 它只转换引用, 不会把
String变成str的所有权