Rust~String、str、&str、&String、Box<str> 或 Box<&str>

发布于:2025-03-04 ⋅ 阅读:(8) ⋅ 点赞:(0)

Rust语言圣经中定义

str

Rust 语言类型大致分为两种:基本类型和标准库类型,前者由语言特性直接提供,后者在标准库中定义

str 是唯一定义在 Rust 语言特性中的字符串,但也是几乎不会用到的字符串类型
str 字符串是 DST 动态大小类型,编译器无法在编译期知道 str 类型的大小,只有到了运行期才能动态获知
这对于强类型、强安全的 Rust 语言来说不可接受

let string: str = "banana";

创建一个 str 类型的字符串,编译会报错:

error[E0277]: the size for values of type `str` cannot be known at compilation time
 --> src/main.rs:4:9
  |
4 |     let string: str = "banana";
  |         ^^^^^^ doesn't have a size known at compile-time

原因:
所有的切片都是动态类型,无法直接被使用,str 是字符串切片,[u8] 是数组切片
str 是 String 和 &str 的底层数据类型

String

str 类型是硬编码进可执行文件,无法被修改
String 是一个可增长、可改变且具有所有权的 UTF-8 编码的字符串
Rust 提到字符串时,往往指的是 String 类型和 &str 字符串切片类型,这两个类型都是 UTF-8 编码

示例

在这里插入图片描述
在这里插入图片描述

其他

Rust 的标准库还提供了其他类型的字符串,例如 OsString, OsStr, CsString 和 CsStr 等
这些名字都以 String 或者 Str 结尾,它们分别对应的是具有所有权和被借用的变量

具体区分

String

类型本质:String 是一个可变的、拥有所有权的字符串类型,存储在堆上,并且可以动态增长
内存管理:由 Rust 的所有权系统管理,当 String 离开作用域时,其占用的内存会被自动释放
使用场景:当需要对字符串进行修改(如追加、删除字符)时使用

fn main() {
    let mut s = String::from("hello");
    s.push_str(", world!");
    println!("{}", s);
}

str

类型本质:str 是一个不可变的、无固定大小的字符串切片类型,通常被称为 “字符串切片”。没有所有权,只是对存储在其他地方的字符串数据的引用
内存管理:由于 str 是无固定大小的类型,不能直接使用,通常以引用的形式 &str 出现
使用场景:用于表示一个字符串的一部分,如字符串的子串

&str

类型本质:&str 是对 str 类型的引用,是一个不可变的字符串切片。它指向一个连续的 UTF - 8 编码的字节序列
内存管理:是一个借用类型,不拥有底层数据的所有权,只要借用的对象存在,&str 就有效
使用场景:当只需要读取字符串内容而不需要修改它时,使用 &str 作为函数参数可以接受多种字符串类型(如 String 和字面量字符串)

fn print_string(s: &str) {
    println!("{}", s);
}

fn main() {
    let s1 = String::from("hello");
    print_string(&s1);
    print_string("world");
}

&String

类型本质:&String 是对 String 类型的引用
内存管理:是一个借用类型,不拥有 String 的所有权,只要 String 对象存在,&String 就有效
使用场景:通常在已经有 String 类型的变量,而函数参数要求引用时使用

fn print_string_ref(s: &String) {
    println!("{}", s);
}

fn main() {
    let s = String::from("hello");
    print_string_ref(&s);
}

Box<str>

类型本质:Box<str> 是将 str 类型装箱到堆上的类型。它拥有底层 str 的所有权
内存管理:当 Box<str> 离开作用域时,底层的 str 数据会被自动释放
使用场景:当需要在堆上存储 str 数据,并且希望拥有其所有权时使用

fn main() {
    let s: Box<str> = "hello".into();
    println!("{}", s);
}

Box<&str>

类型本质:Box<&str> 不是一个常见用法,&str 本身是引用类型,将其装箱没意义。Box<&str> 会在堆上存储一个 &str 引用
内存管理:Box<&str> 拥有这个引用的所有权,但引用指向的数据本身的生命周期需要单独管理
使用场景:一般不建议使用,容易造成混淆

OsString 和 OsStr

类型本质:OsString 是一个拥有所有权的字符串类型,用于表示操作系统相关的字符串,如文件路径。OsStr 是 OsString 的不可变视图,类似于 str 是 String 的不可变视图
内存管理:OsString 管理自己的内存,OsStr 是借用类型
使用场景:在处理与操作系统交互的字符串时使用,如文件系统操作

use std::ffi::OsString;
use std::path::PathBuf;

fn main() {
    let os_string = OsString::from("example.txt");
    let path = PathBuf::from(os_string);
    println!("{:?}", path);
}

注意:OsString 和 String 的内存管理不一样

CsString 和 CsStr

类型本质:CsString 和 CsStr 是 cstr_core crate 中的类型,用于处理以空字符结尾的 C 风格字符串。CsString 拥有字符串数据,CsStr 是不可变视图
内存管理:CsString 管理自己的内存,CsStr 是借用类型
使用场景:在与 C 代码进行交互时,需要处理 C 风格字符串时使用

use cstr_core::CStr;
use std::ffi::CString;

fn main() {
    let c_string = CString::new("hello").unwrap();
    let c_str = c_string.as_c_str();
    println!("{:?}", c_str);
}

注意:CsString 和 String 的内存管理不一样

如何查看字符串的大小

String

fn main() {
	// String 内部是以 UTF - 8 字节序列存储
	// String 是 Rust 标准库中用于表示可增长、拥有所有权的 UTF - 8 编码字符串的类型
    let s = String::from("Hello, 世界");

    // 获取字节长度
    let byte_length = s.len();
    // 13
    println!("Byte length: {}", byte_length);

    // 获取字符数量
    let char_count = s.chars().count();
    // 9
    println!("Character count: {}", char_count);
}

CsString

use cstr_core::CString;

fn main() {
    let cs_string = CString::new("Hello, C-style").expect("Failed to create CString");

    // 获取不包含空字符的字节长度
    let byte_length_without_nul = cs_string.as_c_str().to_bytes().len();
    // 14
    println!("Byte length without null terminator: {}", byte_length_without_nul);

    // 获取包含空字符的字节长度
    let byte_length_with_nul = cs_string.as_c_str().to_bytes_with_nul().len();
    // 15
    println!("Byte length with null terminator: {}", byte_length_with_nul);
}

OsString

可以将 OsString 转换为 OsStr,然后根据操作系统的编码方式将其转换为 &str 或字节切片来获取长度

use std::ffi::OsString;

fn main() {
    let os_string = OsString::from("Hello, OS");

    // 尝试将 OsString 转换为 &str 并获取长度
    if let Some(s) = os_string.to_str() {
        let byte_length = s.len();
        // 9
        println!("Byte length: {}", byte_length);
    }
}

在上述代码中,os_string.to_str() 尝试将 OsString 转换为 &str,如果转换成功,则可以使用 len() 方法获取其字节长度。需要注意的是,to_str() 可能会失败,因为 OsString 可能包含非 UTF - 8 编码的数据。如果需要处理非 UTF - 8 数据,可以使用 os_string.into_vec() 方法将其转换为字节向量,然后获取向量的长度。
综上所述,不同类型的字符串获取长度的方法有所不同,需要根据具体类型和需求选择合适的方法。

std::ffi::OsString的os_string.to_str()

pub fn to_str(&self) -> Option<&str>

&OsString转为Option<&str>

pub trait Deref {
    type Target: ?Sized;

    // Required method
    fn deref(&self) -> &Self::Target;
}

// 
impl ops::Deref for OsString {
    type Target = OsStr;

    #[inline]
    fn deref(&self) -> &OsStr {
        &self[..]
    }
}

to_str() 是 OsStr 类型的一个方法,OsString 可以通过自动解引用转换为 OsStr 来调用这个方法。
to_str() 方法的作用是尝试将 OsStr 转换为 &str
它的实现原理是检查 OsStr 内部存储的字节序列是否是有效的 UTF - 8 编码。如果是有效的 UTF - 8 编码,就返回一个 Some(&str);如果不是有效的 UTF - 8 编码,则返回 None

Deref 机制主要用于在需要某个类型的引用时,自动将一个类型的引用转换为另一个类型的引用。
String 实现了 Deref<Target = str>,意味着当有一个 &String 类型的变量时,Rust 会自动将其转换为 &str,以便可以调用 str 类型的方法。

os_string.to_str() 并不是依赖 Deref 来完成从 OsString 到 &str 的转换。它是通过检查 OsStr 内部字节序列的 UTF - 8 有效性来进行转换的。

自动解引用

以 OsString 转换 OsStr 为例
在 Rust 中,OsString 可以自动解引用转换为 OsStr,这种转换主要发生在以下几种场景:

调用 OsStr 方法时

当调用一个 OsStr 类型的方法,而实际操作的是 OsString 实例时,Rust 会自动进行解引用转换。
这是因为 OsString 实现了 Deref<Target = OsStr> 特征,该特征允许 Rust 在需要 OsStr 引用的地方使用 OsString 引用

use std::ffi::OsString;

fn main() {
    let os_string = OsString::from("example.txt");
    // 调用 OsStr 的 to_str 方法,这里自动将 OsString 转换为 OsStr
    if let Some(s) = os_string.to_str() {
        println!("Converted to &str: {}", s);
    }
}

在上述代码中,os_string 是 OsString 类型,但 to_str 是 OsStr 类型的方法

作为函数参数传递时

当一个函数的参数类型是 &OsStr,而传递的是 &OsString 时,Rust 会自动进行解引用转换。

use std::ffi::{OsStr, OsString};

fn print_os_str(os_str: &OsStr) {
    if let Some(s) = os_str.to_str() {
        println!("{}", s);
    }
}

fn main() {
    let os_string = OsString::from("test.txt");
    // 自动将 &OsString 转换为 &OsStr 传递给函数
    print_os_str(&os_string);
}

赋值给 &OsStr 类型变量时

当将一个 &OsString 赋值给一个 &OsStrc类型的变量时,也会发生自动解引用转换。

use std::ffi::OsString;

fn main() {
    let os_string = OsString::from("file.txt");
    // 自动将 &OsString 转换为 &OsStr
    let os_str: &OsStr = &os_string;
    if let Some(s) = os_str.to_str() {
        println!("{}", s);
    }
}

这里,&os_string&OsString 类型,而 os_str 是 &OsStr 类型,Rust 会自动完成转换。

既然可以通过Defef自动转换,那还要as_os_str干嘛

pub fn as_os_str(&self) -> &OsStr

显式表达意图

代码的可读性和可维护性在软件开发中至关重要。使用 as_os_str 方法可以更清晰地表达想要将 OsString 转换为 &OsStr 的意图。相比自动解引用,显式调用方法能让阅读代码的人一眼就明白正在进行类型转换操作。

use std::ffi::OsString;

fn main() {
    let os_string = OsString::from("example.txt");
    // 显式转换,意图清晰
    let os_str = os_string.as_os_str(); 
    if let Some(s) = os_str.to_str() {
        println!("{}", s);
    }
}

避免潜在的混淆

在某些复杂的代码场景中,自动解引用可能会导致代码的行为变得难以理解。自动解引用是 Rust 编译器在背后自动完成的,当代码中有多个类型实现了 Deref 特征时,可能会引发混淆。使用 as_os_str 可以避免这种潜在的混淆,让代码的行为更加明确。

与其他类型转换保持一致性

在 Rust 标准库中,很多类型都提供了显式的类型转换方法,比如 String 有 as_str 方法用于转换为 &strVec<T> 有 as_slice 方法用于转换为 &[T]。OsString 的 as_os_str 方法与这些设计保持一致,使得代码的风格更加统一。

代码审查和调试

在代码审查过程中,显式的类型转换方法更容易被审查人员识别和理解。同时,在调试代码时,显式调用方法可以让调试者更清楚地看到类型转换的位置和过程,有助于快速定位问题。

总结

尽管 Deref 自动转换提供了便利,但 as_os_str 方法通过显式表达意图、避免混淆、保持一致性以及方便代码审查和调试等方面,为代码的质量和可维护性提供了保障。