不灭的焱

革命尚未成功,同志仍须努力 下载Java21

作者:AlbertWen  添加时间:2026-07-19 23:51:09  修改时间:2026-07-26 15:38:27  分类:01.Rust编程  编辑

目录

Rust PathBuf 使用详解

PathBuf 是 Rust 标准库中用于表示和操作文件路径、目录路径的类型。

use std::path::PathBuf;

可以把它简单理解为:

PathBuf 就像路径专用的 String,拥有路径数据,并且可以修改。

Rust 标准库同时提供了 PathPathBuf

字符串类型 路径类型 特点
String PathBuf 拥有数据、可修改
&str &Path 借用数据、通常只读

官方将 PathBuf 定义为“拥有所有权且可修改的路径”,并且它实现了到 Path 的解引用,因此 Path 上的大多数检查方法也能直接用于 PathBuf。(Rust 文档)

一、为什么不直接使用 String

路径表面上看起来是字符串:

C:\Users\Albert\config.toml

或者:

/home/albert/config.toml

但路径具有一些普通字符串没有的特性:

  • Windows 和 Linux/macOS 的路径分隔符不同;
  • 路径可能不是合法的 UTF-8;
  • 路径包含父目录、文件名、扩展名、根目录等结构;
  • 路径需要进行拼接、拆分、替换文件名等操作。

Rust 的 PathBufPath 是对 OsStringOsStr 的封装,可以按照当前操作系统的路径规则处理数据,而不是假设路径一定是 UTF-8 字符串。(Rust 文档)

例如,不建议这样手动拼接:

let base = "data";
let file = "config.toml";

let path = format!("{}/{}", base, file);

因为 Windows 常用 \,Unix 系统使用 /

更推荐:

use std::path::PathBuf;

let mut path = PathBuf::from("data");
path.push("config.toml");

println!("{}", path.display());

在不同操作系统上,Rust 会按照对应平台的路径规则处理分隔符。

二、创建 PathBuf

1. 使用 PathBuf::new()

创建一个空路径:

use std::path::PathBuf;

fn main() {
    let path = PathBuf::new();

    println!("{:?}", path);
}

此时 path 是空路径。

通常配合 push() 动态构建路径:

use std::path::PathBuf;

fn main() {
    let mut path = PathBuf::new();

    path.push("data");
    path.push("users");
    path.push("user.json");

    println!("{}", path.display());
}

Linux/macOS 上可能输出:

data/users/user.json

Windows 上可能输出:

data\users\user.json

2. 使用 PathBuf::from()

这是最常见的创建方式:

use std::path::PathBuf;

fn main() {
    let path = PathBuf::from("config/app.toml");

    println!("{}", path.display());
}

也可以从 String 创建:

use std::path::PathBuf;

fn main() {
    let path_string = String::from("config/app.toml");

    let path = PathBuf::from(path_string);

    println!("{}", path.display());
}

这里 path_string 的所有权会被移动给 PathBuf

3. Windows 路径建议使用原始字符串

Windows 路径中包含大量反斜杠:

C:\Users\Albert\Documents

普通字符串需要转义:

let path = PathBuf::from("C:\\Users\\Albert\\Documents");

更推荐使用原始字符串:

use std::path::PathBuf;

fn main() {
    let path = PathBuf::from(r"C:\Users\Albert\Documents");

    println!("{}", path.display());
}

r"..." 中的反斜杠不需要转义。

三、PathBufPath 的区别

PathBuf

拥有路径数据,可以修改:

use std::path::PathBuf;

let mut path = PathBuf::from("data");
path.push("config.toml");

Path

通常以 &Path 形式出现,只是借用路径:

use std::path::Path;

let path: &Path = Path::new("data/config.toml");

可以类比:

PathBuf  对应 String
&Path    对应 &str

例如,下面这个函数只需要读取路径,不需要拥有或修改它,因此参数使用 &Path

use std::path::Path;

fn print_path(path: &Path) {
    println!("{}", path.display());
}

fn main() {
    let path = Path::new("data/config.toml");

    print_path(path);
}

而需要构建并返回路径时,可以返回 PathBuf

use std::path::PathBuf;

fn build_config_path() -> PathBuf {
    let mut path = PathBuf::from("config");
    path.push("app.toml");
    path
}

fn main() {
    let path = build_config_path();

    println!("{}", path.display());
}

四、使用 push() 拼接路径

push() 会修改原来的 PathBuf

use std::path::PathBuf;

fn main() {
    let mut path = PathBuf::from("data");

    path.push("users");
    path.push("10001");
    path.push("profile.json");

    println!("{}", path.display());
}

结果类似:

data/users/10001/profile.json

push() 的参数支持字符串、StringPathPathBuf 等,因为它接收实现了 AsRef<Path> 的类型。

注意:追加绝对路径会覆盖原路径

use std::path::PathBuf;

fn main() {
    let mut path = PathBuf::from("/home/albert");

    path.push("/etc/config.toml");

    println!("{}", path.display());
}

结果不是:

/home/albert/etc/config.toml

而是:

/etc/config.toml

因为追加的路径是绝对路径时,push() 会替换原路径;join() 也具有类似行为。(Rust 文档)

这个行为在接收用户输入时尤其要注意:

fn build_upload_path(file_name: &str) -> PathBuf {
    let mut path = PathBuf::from("/data/uploads");
    path.push(file_name);
    path
}

如果 file_name 是:

/etc/passwd

原来的 /data/uploads 会被替换。

因此,不应该直接把不可信的完整路径传给 push()

五、使用 join() 拼接路径

join()push() 都可以拼接路径,但区别是:

  • push() 修改原路径;
  • join() 返回一个新的 PathBuf,不修改原路径。

push() 示例

use std::path::PathBuf;

fn main() {
    let mut base = PathBuf::from("data");

    base.push("config.toml");

    println!("{}", base.display());
}

base 被修改为:

data/config.toml

join() 示例

use std::path::PathBuf;

fn main() {
    let base = PathBuf::from("data");

    let config_path = base.join("config.toml");

    println!("base: {}", base.display());
    println!("config: {}", config_path.display());
}

输出:

base: data
config: data/config.toml

原来的 base 没有改变。

连续使用 join()

use std::path::PathBuf;

fn main() {
    let path = PathBuf::from("data")
        .join("users")
        .join("10001")
        .join("profile.json");

    println!("{}", path.display());
}

Path::join() 会创建新的 PathBuf;当被拼接路径是绝对路径时,它也会替换前面的路径。(Rust 文档)

六、使用 pop() 删除最后一级路径

pop() 类似于返回上一级目录。

use std::path::PathBuf;

fn main() {
    let mut path = PathBuf::from("data/users/10001/profile.json");

    path.pop();

    println!("{}", path.display());
}

输出:

data/users/10001

再次调用:

path.pop();

结果:

data/users

pop() 返回 bool

  • 成功删除最后一级,返回 true
  • 已经无法继续删除,返回 false。(Rust 文档)

完整示例:

use std::path::PathBuf;

fn main() {
    let mut path = PathBuf::from("data/users/profile.json");

    while path.pop() {
        println!("{}", path.display());
    }
}

可能输出:

data/users
data

七、读取路径中的各个部分

由于 PathBuf 可以使用 Path 的方法,因此可以直接调用:

  • parent()
  • file_name()
  • file_stem()
  • extension()
  • is_absolute()
  • is_relative()

1. 获取父目录:parent()

use std::path::PathBuf;

fn main() {
    let path = PathBuf::from("data/users/profile.json");

    match path.parent() {
        Some(parent) => {
            println!("父目录:{}", parent.display());
        }
        None => {
            println!("没有父目录");
        }
    }
}

输出:

父目录:data/users

parent() 返回:

Option<&Path>

因为某些路径可能不存在父路径,例如根目录。(Rust 文档)

2. 获取文件名:file_name()

use std::path::PathBuf;

fn main() {
    let path = PathBuf::from("data/users/profile.json");

    if let Some(file_name) = path.file_name() {
        println!("文件名:{}", file_name.to_string_lossy());
    }
}

输出:

文件名:profile.json

file_name() 返回:

Option<&OsStr>

不是 Option<&str>,因为操作系统路径不一定是合法 UTF-8。

3. 获取不含扩展名的文件名:file_stem()

use std::path::PathBuf;

fn main() {
    let path = PathBuf::from("data/archive.tar.gz");

    if let Some(stem) = path.file_stem() {
        println!("文件主体:{}", stem.to_string_lossy());
    }
}

输出:

文件主体:archive.tar

注意:

archive.tar.gz

最后一个扩展名是:

gz

因此 file_stem() 是:

archive.tar

官方文档中同样说明,file_stem() 取最后一个点之前的部分。(Rust 文档)

4. 获取扩展名:extension()

use std::path::PathBuf;

fn main() {
    let path = PathBuf::from("data/archive.tar.gz");

    match path.extension() {
        Some(extension) => {
            println!("扩展名:{}", extension.to_string_lossy());
        }
        None => {
            println!("没有扩展名");
        }
    }
}

输出:

扩展名:gz

另一个例子:

let path = PathBuf::from("main.rs");

结果:

rs

5. 一次性获取完整信息

use std::path::PathBuf;

fn main() {
    let path = PathBuf::from("data/reports/2026/report.pdf");

    println!("完整路径:{}", path.display());

    if let Some(parent) = path.parent() {
        println!("父目录:{}", parent.display());
    }

    if let Some(file_name) = path.file_name() {
        println!("文件名:{}", file_name.to_string_lossy());
    }

    if let Some(file_stem) = path.file_stem() {
        println!("文件主体:{}", file_stem.to_string_lossy());
    }

    if let Some(extension) = path.extension() {
        println!("扩展名:{}", extension.to_string_lossy());
    }
}

输出:

完整路径:data/reports/2026/report.pdf
父目录:data/reports/2026
文件名:report.pdf
文件主体:report
扩展名:pdf

八、修改文件名

使用 set_file_name()

use std::path::PathBuf;

fn main() {
    let mut path = PathBuf::from("data/config/app.toml");

    path.set_file_name("system.toml");

    println!("{}", path.display());
}

输出:

data/config/system.toml

父目录保持不变,只替换最后一级路径。

如果路径没有文件名,set_file_name() 的效果类似于 push()。(Rust 文档)

九、修改扩展名

使用 set_extension()

use std::path::PathBuf;

fn main() {
    let mut path = PathBuf::from("data/report.txt");

    path.set_extension("pdf");

    println!("{}", path.display());
}

输出:

data/report.pdf

原来没有扩展名

let mut path = PathBuf::from("data/report");

path.set_extension("pdf");

结果:

data/report.pdf

删除扩展名

传入空字符串:

use std::path::PathBuf;

fn main() {
    let mut path = PathBuf::from("data/report.pdf");

    path.set_extension("");

    println!("{}", path.display());
}

输出:

data/report

set_extension() 会替换最后一个扩展名,传入空字符串则删除扩展名;如果路径没有文件名,它返回 false。(Rust 文档)

tar.gz 的情况

use std::path::PathBuf;

fn main() {
    let mut path = PathBuf::from("backup.tar.gz");

    path.set_extension("zip");

    println!("{}", path.display());
}

输出:

backup.tar.zip

因为 set_extension() 只替换最后一个扩展名。

十、追加扩展名:add_extension()

在较新的 Rust 版本中,可以使用 add_extension()

use std::path::PathBuf;

fn main() {
    let mut path = PathBuf::from("backup.tar");

    path.add_extension("gz");

    println!("{}", path.display());
}

结果:

backup.tar.gz

区别:

let mut path = PathBuf::from("backup.tar");

path.set_extension("gz");
// backup.gz

而:

let mut path = PathBuf::from("backup.tar");

path.add_extension("gz");
// backup.tar.gz

add_extension() 在当前标准库中用于追加扩展名,而不是替换已有扩展名。(Rust 文档)

十一、判断绝对路径和相对路径

is_absolute()

use std::path::PathBuf;

fn main() {
    let path = PathBuf::from("/home/albert/config.toml");

    println!("{}", path.is_absolute());
}

Linux/macOS 上输出:

true

is_relative()

use std::path::PathBuf;

fn main() {
    let path = PathBuf::from("config/app.toml");

    println!("{}", path.is_relative());
}

输出:

true

Windows 上需要注意:

C:\Windows

是绝对路径,而:

C:Windows

不是完整的绝对路径。Rust 官方文档明确区分了 Windows 的驱动器前缀和根目录。(Rust 文档)

十二、检查文件或目录是否存在

1. exists()

use std::path::PathBuf;

fn main() {
    let path = PathBuf::from("Cargo.toml");

    if path.exists() {
        println!("路径存在");
    } else {
        println!("路径不存在");
    }
}

2. is_file()

if path.is_file() {
    println!("这是普通文件");
}

3. is_dir()

if path.is_dir() {
    println!("这是目录");
}

需要注意,exists()is_file()is_dir() 会把部分文件系统错误压缩成 false。需要区分“不存在”和“无权限访问”时,可以使用 try_exists()std::fs::metadata()。(Rust 文档)

例如:

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

fn check_path(path: &Path) -> io::Result<()> {
    match path.try_exists()? {
        true => println!("路径存在"),
        false => println!("路径不存在"),
    }

    Ok(())
}

十三、把路径转换成字符串

1. 推荐用于打印:display()

use std::path::PathBuf;

fn main() {
    let path = PathBuf::from("data/config.toml");

    println!("{}", path.display());
}

display() 返回一个可以通过 {} 输出的辅助对象,适合日志和提示信息。路径可能包含非 Unicode 内容,因此这种显示在部分平台上可能采用有损转换。(Rust 文档)

2. 使用 to_str()

use std::path::PathBuf;

fn main() {
    let path = PathBuf::from("data/config.toml");

    match path.to_str() {
        Some(value) => println!("{value}"),
        None => println!("路径不是合法的 UTF-8"),
    }
}

返回值是:

Option<&str>

因为路径可能不是合法 UTF-8。(Rust 文档)

3. 使用 to_string_lossy()

use std::path::PathBuf;

fn main() {
    let path = PathBuf::from("data/config.toml");

    let text = path.to_string_lossy();

    println!("{text}");
}

返回类型:

Cow<'_, str>

如果路径包含无效 UTF-8 字节,会使用替换字符 代替。(Rust 文档)

不建议直接 unwrap()

不推荐:

let text = path.to_str().unwrap();

更安全:

if let Some(text) = path.to_str() {
    println!("{text}");
}

或者仅用于显示:

println!("{}", path.display());

十四、PathBuf 转换成 &Path

使用 as_path()

use std::path::{Path, PathBuf};

fn print_path(path: &Path) {
    println!("{}", path.display());
}

fn main() {
    let path_buf = PathBuf::from("data/config.toml");

    print_path(path_buf.as_path());
}

不过由于 Rust 会自动解引用,通常可以直接写:

print_path(&path_buf);

PathBuf::as_path() 会把 PathBuf 借用为 &Path,不会复制路径。(Rust 文档)

十五、遍历路径组成部分

使用 components()

use std::path::PathBuf;

fn main() {
    let path = PathBuf::from("/home/albert/project/src/main.rs");

    for component in path.components() {
        println!("{component:?}");
    }
}

在 Unix 系统上可能输出:

RootDir
Normal("home")
Normal("albert")
Normal("project")
Normal("src")
Normal("main.rs")

路径组件可能包括:

Component::Prefix(...)
Component::RootDir
Component::CurDir
Component::ParentDir
Component::Normal(...)

components() 会忽略重复分隔符、非开头的 . 和末尾分隔符,但不会自动解析普通路径中的 ..,也不会解析符号链接。(Rust 文档)

例如:

use std::path::Path;

fn main() {
    let path = Path::new("data/./users/../config.toml");

    for component in path.components() {
        println!("{component:?}");
    }
}

.. 仍会作为 ParentDir 存在。

十六、获取规范化绝对路径

可以使用 canonicalize()

use std::io;
use std::path::PathBuf;

fn main() -> io::Result<()> {
    let path = PathBuf::from("./src/../Cargo.toml");

    let absolute_path = path.canonicalize()?;

    println!("{}", absolute_path.display());

    Ok(())
}

它会:

  • 返回绝对路径;
  • 解析 ...
  • 解析符号链接;
  • 实际访问文件系统。

因此,目标路径通常必须存在,否则会返回错误。(Rust 文档)

不要误以为:

PathBuf::from("a/b/../c")

创建时就会自动变成:

a/c

PathBuf 默认不会进行这种完整规范化。

十七、配合 std::fs 操作文件

PathBuf 只负责表示路径,本身不会自动创建文件或目录。

示例一:创建目录

use std::fs;
use std::io;
use std::path::PathBuf;

fn main() -> io::Result<()> {
    let path = PathBuf::from("data")
        .join("uploads")
        .join("images");

    fs::create_dir_all(&path)?;

    println!("目录创建成功:{}", path.display());

    Ok(())
}

最终创建:

data/uploads/images

示例二:写入文件

use std::fs;
use std::io;
use std::path::PathBuf;

fn main() -> io::Result<()> {
    let directory = PathBuf::from("data");
    fs::create_dir_all(&directory)?;

    let file_path = directory.join("config.toml");

    fs::write(&file_path, "port = 8080\n")?;

    println!("文件已写入:{}", file_path.display());

    Ok(())
}

示例三:读取文件

use std::fs;
use std::io;
use std::path::PathBuf;

fn main() -> io::Result<()> {
    let file_path = PathBuf::from("data").join("config.toml");

    let content = fs::read_to_string(&file_path)?;

    println!("文件路径:{}", file_path.display());
    println!("文件内容:\n{content}");

    Ok(())
}

十八、函数参数推荐使用 AsRef<Path>

如果一个函数需要接收路径,可以这样写:

use std::path::Path;

fn print_file_name<P: AsRef<Path>>(path: P) {
    let path = path.as_ref();

    if let Some(file_name) = path.file_name() {
        println!("{}", file_name.to_string_lossy());
    }
}

fn main() {
    print_file_name("data/config.toml");
    print_file_name(String::from("data/user.json"));
}

还可以传入 PathBuf

use std::path::PathBuf;

let path = PathBuf::from("data/report.pdf");

print_file_name(path);

这种写法的优点是调用者可以传入:

  • &str
  • String
  • &Path
  • PathBuf
  • &PathBuf

例如,一个通用文件读取函数:

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

fn read_text<P: AsRef<Path>>(path: P) -> io::Result<String> {
    fs::read_to_string(path)
}

fn main() -> io::Result<()> {
    let content = read_text("Cargo.toml")?;

    println!("{content}");

    Ok(())
}

十九、实战:构建用户头像保存路径

假设需要生成:

data/uploads/users/10001/avatar.png

可以这样写:

use std::path::{Path, PathBuf};

fn build_avatar_path(base_dir: &Path, user_id: u64) -> PathBuf {
    base_dir
        .join("uploads")
        .join("users")
        .join(user_id.to_string())
        .join("avatar.png")
}

fn main() {
    let base_dir = Path::new("data");

    let avatar_path = build_avatar_path(base_dir, 10001);

    println!("{}", avatar_path.display());
}

输出:

data/uploads/users/10001/avatar.png

这里的设计方式比较符合 Rust 的习惯:

  • 输入参数借用 &Path
  • 函数内部生成新路径;
  • 返回拥有所有权的 PathBuf

二十、实战:修改上传文件的扩展名

use std::path::{Path, PathBuf};

fn change_to_backup_file(path: &Path) -> PathBuf {
    let mut result = path.to_path_buf();

    result.set_extension("bak");

    result
}

fn main() {
    let source = Path::new("data/config.toml");

    let backup = change_to_backup_file(source);

    println!("原文件:{}", source.display());
    println!("备份文件:{}", backup.display());
}

输出:

原文件:data/config.toml
备份文件:data/config.bak

to_path_buf() 类似于:

&str.to_string()

它会从借用的 &Path 创建一个拥有所有权的 PathBuf。(Rust 文档)

二十一、实战:只允许指定扩展名

use std::path::Path;

fn is_allowed_file(path: &Path) -> bool {
    matches!(
        path.extension().and_then(|ext| ext.to_str()),
        Some("jpg" | "jpeg" | "png" | "gif")
    )
}

fn main() {
    let paths = [
        Path::new("avatar.png"),
        Path::new("photo.jpg"),
        Path::new("document.pdf"),
        Path::new("README"),
    ];

    for path in paths {
        println!(
            "{}:{}",
            path.display(),
            if is_allowed_file(path) {
                "允许"
            } else {
                "不允许"
            }
        );
    }
}

输出:

avatar.png:允许
photo.jpg:允许
document.pdf:不允许
README:不允许

需要忽略大小写时:

use std::path::Path;

fn is_allowed_file(path: &Path) -> bool {
    path.extension()
        .and_then(|ext| ext.to_str())
        .map(|ext| {
            matches!(
                ext.to_ascii_lowercase().as_str(),
                "jpg" | "jpeg" | "png" | "gif"
            )
        })
        .unwrap_or(false)
}

不过扩展名只能作为辅助判断,不能可靠证明文件的真实内容类型。

二十二、实战:防止简单的路径穿越

假设用户上传文件名:

../../etc/passwd

不应该直接这样做:

let path = PathBuf::from("uploads").join(user_input);

可以先要求输入只能包含一个普通文件名:

use std::path::{Component, Path, PathBuf};

fn safe_upload_path(base: &Path, file_name: &str) -> Option<PathBuf> {
    let input = Path::new(file_name);

    let mut components = input.components();

    match (components.next(), components.next()) {
        (Some(Component::Normal(name)), None) => Some(base.join(name)),
        _ => None,
    }
}

fn main() {
    let base = Path::new("uploads");

    let normal = safe_upload_path(base, "avatar.png");
    let dangerous = safe_upload_path(base, "../../etc/passwd");

    println!("正常路径:{normal:?}");
    println!("危险路径:{dangerous:?}");
}

输出类似:

正常路径:Some("uploads/avatar.png")
危险路径:None

这段代码只接受单个普通路径组件,不接受:

  • 绝对路径;
  • ..
  • 子目录;
  • 多级路径。

二十三、常见错误

错误一:使用字符串手动拼接

不推荐:

let path = format!("{}\\{}", base, file_name);

推荐:

let path = PathBuf::from(base).join(file_name);

错误二:直接把路径转换为 String

不推荐:

let value = path.to_str().unwrap().to_string();

打印时推荐:

println!("{}", path.display());

确实需要字符串时:

let value = path.to_string_lossy().into_owned();

错误三:误以为 join() 会修改原路径

use std::path::PathBuf;

fn main() {
    let base = PathBuf::from("data");

    base.join("config.toml");

    println!("{}", base.display());
}

输出仍然是:

data

因为 join() 返回新的 PathBuf,需要接收结果:

let config_path = base.join("config.toml");

错误四:忽略绝对路径覆盖行为

let path = PathBuf::from("/data/uploads").join("/etc/passwd");

结果可能是:

/etc/passwd

而不是:

/data/uploads/etc/passwd

错误五:把 PathBuf 当成真实文件

let path = PathBuf::from("data/config.toml");

这只是创建了一个路径值,不会创建:

  • data 目录;
  • config.toml 文件。

真正创建文件需要:

std::fs::write(&path, "内容")?;

二十四、常用方法汇总

方法 作用
PathBuf::new() 创建空路径
PathBuf::from() 从字符串等数据创建路径
push() 在原路径后追加路径
join() 返回拼接后的新路径
pop() 删除最后一级路径
parent() 获取父路径
file_name() 获取文件名
file_stem() 获取不含最后一个扩展名的文件名
extension() 获取扩展名
set_file_name() 修改文件名
set_extension() 修改或删除扩展名
add_extension() 追加扩展名
is_absolute() 是否为绝对路径
is_relative() 是否为相对路径
exists() 路径是否存在
try_exists() 检查路径是否存在并保留错误
is_file() 是否为文件
is_dir() 是否为目录
components() 遍历路径组成部分
canonicalize() 获取规范化绝对路径
display() 用于输出路径
to_str() 尝试转换为 &str
to_string_lossy() 有损转换为字符串
as_path() 借用为 &Path
to_path_buf() &Path 创建拥有所有权的 PathBuf

二十五、核心理解

记住下面几点,基本就掌握了 PathBuf

PathBuf ≈ 路径版本的 String
&Path   ≈ 路径版本的 &str

需要保存、构建、修改、返回路径时使用:

PathBuf

只需要读取、检查或作为函数参数时,优先使用:

&Path

动态修改原路径:

path.push("config.toml");

生成新路径:

let config_path = base.join("config.toml");

安全打印路径:

println!("{}", path.display());

推荐的函数签名通常是:

use std::path::{Path, PathBuf};

fn build_path(base: &Path) -> PathBuf {
    base.join("config.toml")
}