Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

第十九 类型转换

概述

Rust 的类型系统以严格著称。与 C/C++ 和 JavaScript 等语言不同,Rust 不支持隐式类型转换(implicit type conversion),几乎所有类型转换都必须显式进行。这一设计哲学体现了 Rust 的核心理念:显式优于隐式,让每一处类型变化都清晰可见,从而减少因隐式转换引发的 bug。

Rust 中的类型转换主要分为以下几类:

方式关键字/trait特点典型场景
隐式强制转换coercion编译器自动处理,范围有限&T&dyn Trait&mut T&T
显式转换(trait)From / Into编译期检查,推荐方式自定义类型间转换
显式转换(trait)TryFrom / TryInto可能失败,返回 Result数值范围检查、字符串解析
廉价借用转换AsRef / AsMut仅借用,零拷贝函数参数泛型
强制类型转换as简单直接,可能丢失数据数值截断、指针转换
字符串解析FromStr / parse可能失败,返回 Result字符串转数值

隐式类型转换 vs 显式类型转换

隐式强制转换(Coercion)

Rust 虽然不支持一般的隐式类型转换,但在少数特定场景下,编译器会自动进行强制转换(coercion)

fn main() {
    // 1. 自动解引用强制转换(Deref coercion)
    let s: String = String::from("hello");
    let slice: &str = &s; // String 自动解引用为 &str

    let v: Vec<i32> = vec![1, 2, 3];
    let slice: &[i32] = &v; // Vec<T> 自动解引用为 &[T]

    // 2. &mut T 自动转换为 &T
    let mut x = 42;
    let r: &i32 = &x; // 可变引用自动转为不可变引用

    // 3. 数值字面量的类型推断
    let x: i32 = 42;       // 42 被推断为 i32
    let y: f64 = 3.14;     // 3.14 被推断为 f64
    let z: u8 = 255;       // 255 被推断为 u8

    // 4. 自动提升:整数可以自动转换为更大的同符号类型(在特定上下文中)
    // 注意:Rust 的自动提升非常有限,不像 C 语言那样自由
}

显式类型转换

Rust 要求绝大多数类型转换必须显式进行,这是 Rust 类型安全的重要保障:

fn main() {
    // 错误!不提供隐式转换
    // let integer: i32 = 3.14_f64; // 编译错误 E0308

    // 正确:使用 as 关键字显式转换
    let integer: i32 = 3.14_f64 as i32;
    println!("{}", integer); // 3
}

From/Into trait

FromInto 是 Rust 中最推荐的类型转换方式,它们在编译期就能确保转换的安全性。

原理:From 自动实现 Into

Rust 标准库中有一个 blanket implementation:为所有实现了 From<T> 的类型自动实现 Into<T>

#![allow(unused)]
fn main() {
// 标准库中的 blanket implementation(简化)
impl<T, U> Into<U> for T
where
    U: From<T>,
{
    fn into(self) -> U {
        U::from(self)
    }
}
}

这意味着:你只需要实现 From,就自动获得了 Into。反之,如果需要将类型 A 转为类型 B,只需为 B 实现 From<A> 即可。

标准库中的实现

Rust 标准库已经为许多基本类型实现了 From/Into

fn main() {
    // 数值类型之间的转换
    let x: i32 = i64::from(42);     // i64 -> i32
    let y: f64 = f32::from(3.14);   // f32 -> f64
    let z: u32 = u16::from(100);    // u16 -> u32

    // 使用 Into(更常见,因为自动实现)
    let a: String = "hello".into();  // &str -> String
    let b: Vec<u8> = "hello".into(); // &str -> Vec<u8>
    let c: i64 = 42i32.into();       // i32 -> i64

    // bool 转整数
    let d: i32 = true.into();       // bool -> i32 (值为 1)
    let e: i32 = false.into();      // bool -> i32 (值为 0)

    println!("a = {}, b = {:?}, c = {}, d = {}, e = {}", a, b, c, d, e);
}

自定义类型的 From 实现

#[derive(Debug)]
struct Celsius(f64);

#[derive(Debug)]
struct Fahrenheit(f64);

// Celsius -> Fahrenheit
impl From<Celsius> for Fahrenheit {
    fn from(c: Celsius) -> Self {
        Fahrenheit(c.0 * 9.0 / 5.0 + 32.0)
    }
}

// Fahrenheit -> Celsius
impl From<Fahrenheit> for Celsius {
    fn from(f: Fahrenheit) -> Self {
        Celsius((f.0 - 32.0) * 5.0 / 9.0)
    }
}

// 从 f64 直接构造
impl From<f64> for Celsius {
    fn from(value: f64) -> Self {
        Celsius(value)
    }
}

fn main() {
    let boiling = Celsius(100.0);
    let f: Fahrenheit = boiling.into(); // 自动调用 From<Celsius>
    println!("100°C = {:.1}°F", f.0); // 212.0°F

    // 使用 Into trait 约束作为函数参数
    fn print_temperature<T: Into<Celsius>>(temp: T) {
        let c: Celsius = temp.into();
        println!("温度: {:.1}°C", c.0);
    }

    print_temperature(36.5_f64);      // f64 -> Celsius
    print_temperature(Celsius(0.0));   // Celsius -> Celsius
}

TryFrom/TryInto:可能失败的转换

当转换可能失败时(例如数值溢出、字符串解析错误),应使用 TryFrom/TryInto,它们返回 Result 类型:

use std::convert::TryFrom;
use std::convert::TryInto;

fn main() {
    // TryFrom:检查数值范围
    let small: u8 = u8::try_from(300_i32);
    match small {
        Ok(v) => println!("转换成功: {}", v),
        Err(e) => println!("转换失败: {}", e), // 数值超出范围
    }

    let ok: u8 = u8::try_from(42_i32).unwrap();
    println!("42 -> u8: {}", ok); // 42

    // TryInto:等价的反向操作
    let result: Result<i8, _> = 300_i32.try_into();
    assert!(result.is_err());

    let result: Result<i8, _> = 100_i32.try_into();
    assert_eq!(result.unwrap(), 100);
}

建议:在 Rust 1.34+ 中,TryFrom/TryInto 已在 std::convert 中稳定。对于可能失败的转换,优先使用 TryFrom/TryInto 而非 as,因为后者会静默截断。


AsRef/AsMut trait

AsRef/AsMut 提供了一种廉价的借用转换机制,将一种类型转换为另一种类型的引用,不涉及数据拷贝。

原理

#![allow(unused)]
fn main() {
pub trait AsRef<T: ?Sized> {
    fn as_ref(&self) -> &T;
}

pub trait AsMut<T: ?Sized> {
    fn as_mut(&mut self) -> &mut T;
}
}

字符串泛型函数

AsRef 最常见的应用场景是编写能够接受多种字符串类型的泛型函数:

use std::fs;
use std::io;
use std::path::Path;

/// 读取文件内容,接受任何可以转换为 &str 的类型
fn print_length<S: AsRef<str>>(text: S) {
    let s = text.as_ref();
    println!("字符串长度: {}", s.len());
}

/// 打开文件,接受任何可以转换为 AsRef<Path> 的类型
fn read_file<P: AsRef<Path>>(path: P) -> io::Result<String> {
    fs::read_to_string(path)
}

fn main() {
    // AsRef<str>:接受 String、&str、&String 等
    print_length("hello");                    // &str
    print_length(String::from("hello"));      // String
    print_length(&String::from("hello"));     // &String

    // AsRef<Path>:接受 &str、String、&Path、PathBuf 等
    let content = read_file("Cargo.toml").unwrap();
    println!("Cargo.toml 前100字符: {}", &content[..100.min(content.len())]);
}

标准库中的 AsRef 实现

fn main() {
    // String 实现了 AsRef<str>
    let s = String::from("hello");
    let r: &str = s.as_ref();

    // Vec<T> 实现了 AsRef<[T]>
    let v = vec![1, 2, 3];
    let slice: &[i32] = v.as_ref();

    // String 实现了 AsRef<[u8]>
    let bytes: &[u8] = s.as_ref();

    println!("r = {}, slice = {:?}", r, slice);
}

as 关键字

as 关键字是 Rust 中最直接的显式类型转换方式,但也是最容易出问题的方式。

数值类型之间的转换

fn main() {
    // 整数之间的转换
    let a: i32 = 42;
    let b: i64 = a as i64;       // 小类型 -> 大类型:安全
    let c: i8 = a as i8;         // 大类型 -> 小类型:可能截断
    let d: u32 = a as u32;       // 有符号 -> 无符号:重新解释位模式

    println!("i32({}) -> i64({}), i8({}), u32({})", a, b, c, d);

    // 浮点数之间的转换
    let e: f32 = 3.14159265_f64;  // f64 -> f32:丢失精度
    let f: f64 = e as f64;       // f32 -> f64:安全(但已丢失的精度无法恢复)

    // 整数 <-> 浮点数
    let g: f64 = 42 as f64;       // i32 -> f64:安全
    let h: i32 = 3.7_f64 as i32; // f64 -> i32:截断小数部分(不是四舍五入)
    let i: i32 = 3.9_f64 as i32; // 同样截断为 3

    println!("f64(3.7) -> i32: {}", h); // 3
    println!("f64(3.9) -> i32: {}", i); // 3
}

精度丢失和截断

使用 as 进行数值转换时,需要特别注意以下情况:

fn main() {
    // 1. 溢出截断:大值放入小类型
    let x: i32 = 300;
    let y: i8 = x as i8; // 300 超出 i8 范围(-128~127),结果为 44(截断高位)
    println!("i32(300) -> i8: {}", y); // 44

    // 2. 负数转无符号数:重新解释位模式
    let neg: i32 = -1;
    let pos: u32 = neg as u32; // -1 的补码全为1,解释为 u32 即为最大值
    println!("i32(-1) -> u32: {}", pos); // 4294967295

    // 3. 浮点数 NaN 和无穷大转整数
    let nan: f64 = f64::NAN;
    let inf: f64 = f64::INFINITY;
    let nan_int = nan as i32;
    let inf_int = inf as i32;
    println!("NaN -> i32: {}", nan_int); // 0(未定义行为,当前实现返回0)
    println!("Inf -> i32: {}", inf_int); // 未定义行为

    // 4. 安全的替代方案
    let safe: Option<i8> = i8::try_from(300_i32).ok(); // None
    println!("i8::try_from(300) = {:?}", safe); // None
}

警告as 转换在溢出、NaN、无穷大等情况下的行为是未定义的(implementation-defined)。在需要安全转换的场景中,应使用 TryFrom/TryInto

指针转换

as 还可以用于指针类型之间的转换,但这属于 unsafe 操作:

fn main() {
    let x: i32 = 42;

    // 将 &i32 转为裸指针
    let raw: *const i32 = &x as *const i32;

    // 将裸指针转为整数地址
    let addr = raw as usize;
    println!("变量 x 的地址: {:p} (数值: {})", raw, addr);

    // 将 usize 转回裸指针(unsafe)
    unsafe {
        let reconstructed: *const i32 = addr as *const i32;
        println!("通过地址访问的值: {}", *reconstructed); // 42
    }
}

注意:指针转换涉及 unsafe Rust,仅在确实需要时使用,如 FFI(外部函数接口)调用。


String 与数值转换

字符串与数值之间的转换是日常开发中最常见的类型转换场景。

parse 方法与 FromStr trait

FromStr trait 定义了从字符串解析为特定类型的行为:

#![allow(unused)]
fn main() {
pub trait FromStr: Sized {
    type Err;

    fn from_str(s: &str) -> Result<Self, Self::Err>;
}

pub fn parse<F: FromStr>(&self) -> Result<F, F::Err> {
    FromStr::from_str(self)
}
}
use std::str::FromStr;

fn main() {
    // 使用 parse 方法
    let four: u32 = "4".parse().unwrap();
    println!("parse u32: {}", four); // 4

    // turbofish 语法:当类型无法推断时使用
    let four = "4".parse::<u32>();
    println!("parse::<u32>: {:?}", four); // Ok(4)

    // 使用 FromStr trait 直接调用
    let s = "5";
    let x = i32::from_str(s).unwrap();
    println!("from_str: {}", x); // 5

    // 解析失败的情况
    let result = "abc".parse::<i32>();
    println!("parse 'abc': {:?}", result); // Err(ParseIntError { ... })

    // 处理解析错误
    match "42abc".parse::<i32>() {
        Ok(n) => println!("解析成功: {}", n),
        Err(e) => println!("解析失败: {}", e),
    }
}

turbofish 语法

当编译器无法从上下文推断 parse 的目标类型时,需要使用 turbofish 语法 ::<T> 显式指定:

fn main() {
    // 类型可推断:不需要 turbofish
    let x: i32 = "42".parse().unwrap();

    // 类型不可推断:需要 turbofish
    let y = "42".parse::<i32>().unwrap();

    // 函数参数中
    fn double(s: &str) -> Result<i32, std::num::ParseIntError> {
        let n: i32 = s.parse()?;
        Ok(n * 2)
    }

    println!("double('21') = {:?}", double("21")); // Ok(42)
}

ToString trait

ToString trait 是 FromStr 的反向操作,将类型转换为字符串:

use std::fmt::Display;

fn main() {
    // 数值转字符串
    let s = 42.to_string();
    println!("42.to_string() = '{}'", s); // "42"

    let f = 3.14.to_string();
    println!("3.14.to_string() = '{}'", f); // "3.14"

    let b = true.to_string();
    println!("true.to_string() = '{}'", b); // "true"

    // 使用 format! 宏(更灵活)
    let formatted = format!("{}", 42);
    println!("format!('{}') = '{}'", 42, formatted); // "42"

    let hex = format!("{:x}", 255);
    println!("255 的十六进制: '{}'", hex); // "ff"

    let binary = format!("{:b}", 10);
    println!("10 的二进制: '{}'", binary); // "1010"
}

注意ToString 是自动为实现了 Display trait 的类型实现的 blanket implementation。因此,只要类型实现了 Display,就自动拥有 to_string() 方法。


String/&str/Vec<u8> 转换大全

以下是 Rust 中字符串相关类型之间的转换速查表:

完整转换表

源类型目标类型方法说明
&strStringString::from(s)s.to_string()s.to_owned()创建新的拥有所有权的 String
&str&[u8]s.as_bytes()零拷贝借用
&strVec<u8>s.as_bytes().to_vec()拷贝字节
&strVec<char>s.chars().collect()按字符拆分
String&strs.as_str()&s零拷贝借用
String&[u8]s.as_bytes()零拷贝借用
StringVec<u8>s.into_bytes()消费 String,获取底层 Vec
StringVec<char>s.chars().collect()按字符拆分
&[u8]&strstd::str::from_utf8(u)可能失败(需检查 UTF-8 合法性)
&[u8]StringString::from_utf8(v.to_vec())拷贝并验证 UTF-8
Vec<u8>StringString::from_utf8(v)消费 Vec,验证 UTF-8
Vec<u8>&strstd::str::from_utf8(&v).unwrap()借用并验证 UTF-8
&[char]StringString::from_iter(chars)chars.iter().collect()收集字符为字符串

代码示例

use std::str;

fn main() {
    // &str -> String
    let s: &str = "hello";
    let owned: String = String::from(s);
    let owned2: String = s.to_string();
    let owned3: String = s.to_owned();
    assert_eq!(owned, owned2);
    assert_eq!(owned2, owned3);

    // &str -> &[u8]
    let bytes: &[u8] = s.as_bytes();
    println!("bytes: {:?}", bytes); // [104, 101, 108, 108, 111]

    // String -> &str
    let hello = String::from("hello");
    let slice: &str = &hello;      // Deref coercion
    let slice2: &str = hello.as_str();
    assert_eq!(slice, slice2);

    // String -> Vec<u8>
    let hello = String::from("hello");
    let bytes_vec: Vec<u8> = hello.into_bytes();
    println!("into_bytes: {:?}", bytes_vec);

    // &[u8] -> &str(可能失败)
    let valid: &[u8] = b"hello";
    let s: &str = str::from_utf8(valid).unwrap();
    println!("from_utf8: {}", s);

    let invalid: &[u8] = &[0xff, 0xfe]; // 非 UTF-8
    let result = str::from_utf8(invalid);
    println!("invalid utf8: {:?}", result); // Err(Utf8Error)

    // Vec<u8> -> String(可能失败)
    let bytes = vec![104, 101, 108, 108, 111]; // "hello" 的 ASCII
    let s = String::from_utf8(bytes).unwrap();
    println!("from_utf8(Vec): {}", s);

    // &str -> Vec<char>
    let chars: Vec<char> = "hello".chars().collect();
    println!("chars: {:?}", chars); // ['h', 'e', 'l', 'l', 'o']

    // Vec<char> -> String
    let arr: Vec<char> = vec!['h', 'e', 'l', 'l', 'o'];
    let greeting: String = arr.iter().collect();
    println!("collect: {}", greeting);
}

常见转换场景

集合类型转换

fn main() {
    // Vec <-> Array
    let vec = vec![1, 2, 3];
    let arr: [i32; 3] = vec.try_into().unwrap(); // Vec -> Array(长度必须匹配)
    println!("Vec -> Array: {:?}", arr);

    let arr = [1, 2, 3];
    let vec: Vec<i32> = arr.to_vec(); // Array -> Vec
    println!("Array -> Vec: {:?}", vec);

    // Vec <-> HashSet
    use std::collections::HashSet;
    let vec = vec![1, 2, 3, 2, 1];
    let set: HashSet<i32> = vec.into_iter().collect(); // 去重
    println!("Vec -> HashSet: {:?}", set);

    let set: HashSet<&str> = ["apple", "banana", "cherry"].into_iter().collect();
    let vec: Vec<&str> = set.into_iter().collect();
    println!("HashSet -> Vec: {:?}", vec);

    // String <-> Vec<&str>
    let s = "hello world rust";
    let words: Vec<&str> = s.split(' ').collect();
    println!("split: {:?}", words); // ["hello", "world", "rust"]

    let joined = words.join("-");
    println!("join: {}", joined); // "hello-world-rust"
}

日期时间解析

use chrono::NaiveDateTime;

fn main() {
    // 2. &str -> DateTime
    let no_timezone = NaiveDateTime::parse_from_str(
        "2015-09-05 23:56:04",
        "%Y-%m-%d %H:%M:%S"
    );
    println!("{:?}", no_timezone); // Ok(2015-09-05T23:56:04)

    // DateTime -> String
    if let Ok(dt) = no_timezone {
        let formatted = dt.format("%Y年%m月%d日 %H:%M").to_string();
        println!("格式化: {}", formatted); // "2015年09月05日 23:56"
    }

    // 当前时间
    let now = chrono::Local::now();
    println!("当前时间: {}", now.format("%Y-%m-%d %H:%M:%S"));

    // 时间戳转换
    let timestamp = 1609459200; // 2021-01-01 00:00:00 UTC
    let dt = chrono::NaiveDateTime::from_timestamp_opt(timestamp, 0).unwrap();
    println!("时间戳 -> DateTime: {}", dt.format("%Y-%m-%d %H:%M:%S"));
}

Option/Result 转换

fn main() {
    // Option -> Result
    let some_val: Option<i32> = Some(42);
    let result: Result<i32, &str> = some_val.ok_or("值为 None");
    println!("ok_or: {:?}", result); // Ok(42)

    let none_val: Option<i32> = None;
    let result: Result<i32, &str> = none_val.ok_or("值为 None");
    println!("ok_or: {:?}", result); // Err("值为 None")

    // Result -> Option
    let ok_result: Result<i32, &str> = Ok(42);
    let opt: Option<i32> = ok_result.ok();
    println!("ok(): {:?}", opt); // Some(42)

    let err_result: Result<i32, &str> = Err("错误");
    let opt: Option<i32> = err_result.ok();
    println!("ok(): {:?}", opt); // None

    // Option 之间的转换
    let opt_str: Option<&str> = Some("42");
    let opt_num: Option<i32> = opt_str.and_then(|s| s.parse().ok());
    println!("and_then: {:?}", opt_num); // Some(42)

    // Result 之间的转换(map_err)
    let result: Result<i32, &str> = "abc".parse::<i32>().map_err(|_| "解析失败");
    println!("map_err: {:?}", result); // Err("解析失败")
}

Box/Rc/Arc 转换

use std::rc::Rc;
use std::sync::Arc;

fn main() {
    // Box<T> -> Rc<T>
    let boxed: Box<i32> = Box::new(42);
    let rc: Rc<i32> = Rc::from(boxed); // 消费 Box,创建 Rc
    println!("Box -> Rc: {}", rc);

    // Box<T> -> Arc<T>
    let boxed: Box<String> = Box::new(String::from("hello"));
    let arc: Arc<String> = Arc::from(boxed);
    println!("Box -> Arc: {}", arc);

    // Rc<T> -> Arc<T>(需要先解包再包装)
    let rc: Rc<Vec<i32>> = Rc::new(vec![1, 2, 3]);
    let arc: Arc<Vec<i32>> = Arc::from((*rc).clone());
    println!("Rc -> Arc: {:?}", arc);

    // &T -> Box<T>
    let x = 42;
    let boxed: Box<i32> = Box::new(x); // 拷贝值到堆上

    // &str -> Box<str>
    let boxed_str: Box<str> = "hello".into();
    println!("&str -> Box<str>: {}", boxed_str);

    // String -> Box<str>
    let s = String::from("hello");
    let boxed: Box<str> = s.into_boxed_str();
    println!("String -> Box<str>: {}", boxed);
}

类型转换的安全性讨论

不同的类型转换方式具有不同的安全级别:

安全级别方式说明
最安全From/Into编译期保证成功,不会丢失数据
安全TryFrom/TryInto运行时检查,失败返回 Result
安全AsRef/AsMut仅借用,零拷贝,不会失败
需注意FromStr/parse运行时检查,失败返回 Result
危险as(数值)可能静默截断、溢出
极危险as(指针)属于 unsafe,可能导致未定义行为

选择建议

fn main() {
    let value: i64 = 300;

    // 推荐:使用 TryFrom,明确处理可能的失败
    match i8::try_from(value) {
        Ok(v) => println!("转换成功: {}", v),
        Err(_) => println!("值 {} 超出 i8 范围", value),
    }

    // 不推荐:使用 as,静默截断
    let truncated = value as i8;
    println!("截断结果: {}", truncated); // 44,可能不是期望的行为

    // 对于确定安全的转换,可以使用 From/Into
    let widened: i64 = 42i32.into(); // i32 -> i64,总是安全
    println!("安全转换: {}", widened);
}

经验法则

  1. 优先使用 From/Into:当转换总是成功时
  2. 使用 TryFrom/TryInto:当转换可能失败时
  3. 使用 AsRef/AsMut:当只需要借用时
  4. 谨慎使用 as:仅在明确知道后果时使用
  5. 避免 transmute:除非你完全理解内存布局

总结

转换方式关键字/trait失败处理典型场景安全性
隐式强制转换coercion不适用Deref coercion安全
From/IntoFrom<T> / Into<T>编译期保证成功自定义类型转换最安全
TryFrom/TryIntoTryFrom<T> / TryInto<T>返回 Result数值范围检查安全
AsRef/AsMutAsRef<T> / AsMut<T>不失败泛型函数参数安全
asas 关键字静默截断数值截断、指针转换需注意
FromStr/parseFromStr / parse返回 Result字符串解析安全
ToStringDisplay / to_string不失败类型转字符串安全

练习题

  1. 温度转换器:定义 CelsiusFahrenheit 两个结构体,使用 From/Into trait 实现双向转换,并编写一个接受 Into<Celsius> 的泛型函数。

  2. 安全数值解析:编写一个函数,接受一个 &str,尝试解析为 u8。如果解析失败或数值超出 u8 范围,返回默认值 0。要求使用 TryFrom 而非 as

  3. 泛型文件读取器:编写一个函数 fn read_lines<P: AsRef<Path>>(path: P) -> Result<Vec<String>, io::Error>,使其能接受 &strStringPathBuf 作为路径参数。

  4. 类型转换链:实现一个函数,将 Vec<u8> 转换为十六进制字符串表示(如 [0xde, 0xad, 0xbe, 0xef] 转为 "deadbeef"),要求正确处理所有转换步骤。

  5. 配置解析器:编写一个简单的配置解析器,从 &str 解析键值对(格式为 "key=value"),将值转换为合适的类型(i32f64boolString),使用 FromStrparse 方法。