- 人工智能
- AI 技能
- 文档
- 开发工具
【免费下载链接】DocAgent
导读:本文以std.fs文件系统包中的三个核心结构体为线索,系统讲解 Cangjie 标准库中路径(Path)、文件元数据(FileInfo)与文件句柄(FileDescriptor)的完整用法与底层原理。读完本文,你将掌握路径拆分、拼接、规范化与判等技巧,能够查询和修改文件属性与权限,并能正确处理 Windows / Linux / macOS 平台差异及并发修改引发的竞争条件问题。
std.fs(file system)包提供对文件、文件夹、路径、文件元数据信息的一系列操作函数,目前支持 Linux、macOS、Windows 和 HarmonyOS 平台。包内定义了 4 个类(Directory、File、HardLink、SymbolicLink)、1 个枚举(OpenMode)、3 个结构体和 1 个异常类(FSException)。其中三个结构体分别对应文件系统编程中三个最基础的概念:路径的抽象表示、文件元数据和操作系统级文件句柄,详见 std.fs 包总览。
一、三个结构体的定位与关系
| 结构体 | 功能 | 典型使用场景 |
|---|---|---|
| Path | 路径的字符串抽象与解析 | 路径拼接、拆分、规范化、判等 |
| FileInfo | 文件系统中的文件元数据 | 查询/设置文件属性、权限、判断文件类型 |
| FileDescriptor | 获取文件句柄信息 | 获取打开文件的系统级句柄 |
三者常配合使用:File类的info属性返回FileInfo,fileDescriptor属性返回FileDescriptor;Directory.readFrom返回Array<FileInfo>用于遍历目录;Path则是所有文件操作 API 的通用入参类型。具体类的 API 可参考 fs 包类文档。
二、Path 结构体:路径的抽象与解析
2.1 类型声明与基本约定
public struct Path <: Equatable<Path> & Hashable & ToString { public static const Separator: String = PATH_SEPARATOR public static const ListSeparator: String = PATH_LISTSEPARATOR public init(rawPath: String) }Path用来表示本地路径,已实现Equatable<Path>、Hashable、ToString三个接口,因此可以直接参与判等、哈希集合与字符串输出。需要特别注意以下几点:
- 长度限制:路径的字符串最大支持4096 个字节(包括结束符
\0)。 - 平台支持:Windows 平台已支持 DOS 设备路径和 UNC 路径,长度限制跟随系统。
- 构造函数不校验:
init(rawPath: String)创建Path实例时不会检查路径字符串是否合法,同时支持绝对路径和相对路径。
2.2 静态常量:Separator 与 ListSeparator
| 常量 | 含义 | Windows | 非 Windows(Linux/macOS/ohos) |
|---|---|---|---|
Separator | 路径分隔符,用于分隔多级目录 | "\\" | "/" |
ListSeparator | 路径列表分隔符,用于分隔路径列表中的不同路径 | ";" | ":" |
实际开发中可以直接通过Path.Separator、Path.ListSeparator引用,避免硬编码平台差异。
2.3 核心属性:路径拆分
Path提供 4 个属性,将一条路径字符串拆解为不同部分:
| 属性 | 返回类型 | 功能 | 典型示例(以./NewFile.txt为例) |
|---|---|---|---|
parent | Path | 父路径 | Path("./") |
fileName | String | 文件名(含扩展名) | "NewFile.txt" |
fileNameWithoutExtension | String | 文件名(不含扩展名) | "NewFile" |
extensionName | String | 文件扩展名 | "txt" |
拆分规则细节:
- 整个路径字符串被划分为
parent和fileName两部分,以最后一个有效文件分隔符(末尾的分隔符会被忽略)作为分界;parent和fileName都不包含末尾分隔符,parent保留表示根目录的分隔符;无父目录时返回空字符串构造的Path实例。 fileName根据最后一个r'.'被划分为fileNameWithoutExtension和extensionName两部分,无扩展名时返回空字符串。- 该属性不会访问文件系统,也不会消除特殊名称,如需要可搭配
normalize()使用。
所有平台通用示例(来自 Path 示例文档):
| 路径 | parent | fileName | extensionName | fileNameWithoutExtension |
|---|---|---|---|---|
/a/b/c | Path("/a/b") | c | ``(空) | c |
/a/b/ | Path("/a") | b | `` | b |
/a/b/c.cj | Path("/a/b") | c.cj | cj | c |
/a | Path("/") | a | `` | a |
/ | Path("/") | (空) | | `` | |
./a/b/c | Path("./a/b") | c | `` | c |
./a/b/ | Path("./a") | b | `` | b |
./a/b/c.cj | Path("./a/b") | c.cj | cj | c |
./ | Path("") | . | | | |
. | Path("") | . | | | |
123. | Path("") | 123. | `` | 123 |
Windows 平台特有示例(卷名与 UNC 路径):
fileName不含卷名部分:"c:\a.txt"→"a.txt";"c:"→"";"\\Server\Share\a.txt"→"a.txt";"\\?\C:a\b.txt"→"b.txt"。parent包含卷名和目录名:"C:"→Path("C:");"C:\a\b"→Path("C:\a");"\\Server\Share\xx\yy"→Path("\\Server\Share\xx");"\\?\UNC\Server\Share\xx\yy"→Path("\\?\UNC\Server\Share\xx")。
注意:以上属性在路径为空或包含字符串结束符时会抛出IllegalArgumentException。
2.4 常用函数:判断、拼接、规范化与判等
Path的成员函数可分为四类:
(1)路径性质判断
| 函数 | 返回 | 说明 |
|---|---|---|
isAbsolute() | Bool | 是否是绝对路径。在 Unix 中,以/开头的路径为绝对路径 |
isRelative() | Bool | 是否是相对路径,结果与isAbsolute()相反 |
isEmpty() | Bool | 是否为空路径 |
hashCode() | Int64 | 获得Path的哈希值(配合Hashable接口使用) |
toString() | String | 获得路径字符串 |
(2)路径拼接join
join(path: Path): Path与join(path: String): Path在当前路径后拼接另一个路径形成新路径:
Path("a/b").join("c")→Path("a/b/c")Path("a").join("b/c")→Path("a/b/c")
若入参
path是绝对路径则抛出FSException;当前路径为空、或当前路径/入参路径非法时抛出IllegalArgumentException。
(3)规范化normalize()
normalize(): Path将路径字符串规范化,仅做字符串解析,不会进行 I/O 操作。规范化规则如下:
- 将连续的多个路径分隔符替换为单个路径分隔符;
- 删除末尾的路径分隔符(不删除作为根目录的路径分隔符或卷名中的字符);
- 删除每一个
.路径名元素(代表当前目录); - 删除每一个路径内的
..路径名元素(代表父目录)和它前面的非..路径名元素; - 删除开始于根路径的
..路径名元素,即将路径开始处的/..替换为/(Windows 系统中还会将\..替换为\); - 相对路径保留开头的
../(Windows 系统中还将保留..\); - 最后如果得到空路径,返回
Path(".")。
特别地,Windows 文件系统中,卷名部分仅做分隔符转换,即/转换为\。
(4)判等==
operator func ==(that: Path): Bool判断两个Path是否相等。判等时会对两个Path进行规范化,规范化后的字符串相等则认为相等(规范化规则同normalize())。因此Path("./a/../b") == Path("./b")会成立。
2.5 与 canonicalize 函数的区别
Path.normalize()是纯字符串解析,而包级函数 canonicalize 会访问文件系统:它会处理所有的中间引用和软链接,返回绝对路径形式的规范化路径。例如对路径/foo/test/../test/bar.txt,canonicalize会返回/foo/test/bar.txt。注意 UNC 路径下的软链接无法被规范化;路径不存在或无法规范化时抛出FSException。
2.6 实战示例:Path 属性展示与拼接规范化
以下代码(节选自 path_samples.md)演示了parent、fileName、extensionName、fileNameWithoutExtension与绝对/相对路径判断的完整输出:
import std.fs.Path main() { let pathStrArr: Array<String> = [ "/a/b/c", "/a/b/", "/a/b/c.cj", "/a", "/", "./a/b/c", "./a/b/", "./a/b/c.cj", "./", ".", "123." ] for (i in 0..pathStrArr.size) { let path: Path = Path(pathStrArr[i]) println("Path${i}: ${path.toString()}") println("Path.parent: ${path.parent}") println("Path.fileName: ${path.fileName}") println("Path.extensionName: ${path.extensionName}") println("Path.fileNameWithoutExtension: ${path.fileNameWithoutExtension}") println("Path.isAbsolute: ${path.isAbsolute()}; Path.isRelative: ${path.isRelative()}") println() } return 0 }拼接、判等与canonicalize规范化组合使用(节选自 path_samples.md):
import std.fs.* main() { let dirPath: Path = Path("./a/b/c") if (!exists(dirPath)) { Directory.create(dirPath, recursive: true) } let filePath: Path = dirPath.join("d.cj") // ./a/b/c/d.cj if (filePath == Path("./a/b/c/d.cj")) { println("filePath.join: success") } if (!exists(filePath)) { File.create(filePath).close() } let curCanonicalizedPath: Path = canonicalize(Path(".")) let fileCanonicalizedPath: Path = canonicalize( Path("././././a/./../a/b/../../a/b/c/.././../../a/b/c/d.cj")) if (fileCanonicalizedPath == canonicalize(filePath) && fileCanonicalizedPath.toString() == curCanonicalizedPath.toString() + "/a/b/c/d.cj") { println("canonicalize filePath: success") } remove(dirPath, recursive: true) return 0 }运行结果:
filePath.join: success canonicalize filePath: success可以看到,Path(".")经过canonicalize后得到当前工作目录的绝对路径,包含大量./、../的复杂相对路径最终被解析为同一目标文件路径。
三、FileInfo 结构体:文件元数据查询与权限管理
3.1 类型声明与核心设计
public struct FileInfo <: Equatable<FileInfo> { public init(path: Path) public init(path: String) }FileInfo对应文件系统中的文件元数据——文件名、文件大小、创建时间、修改时间、访问时间、文件权限、文件所有者等信息。它实现了Equatable<FileInfo>,可通过==判断两个实例是否对应同一文件。
两个构造函数的重载:init(path: Path)与init(path: String)。两者在路径非法时抛出FSException;当路径为空或包含字符串结束符时抛出IllegalArgumentException。
⚠️ 重要设计:底层不缓存文件属性
FileInfo的底层实现没有直接缓存文件属性,每次通过FileInfo的 API 都是现场获取最新的文件属性。
这意味着:对于同一个FileInfo实例,如果在两次获取文件属性操作期间,对应文件实体被其他用户或进程修改或替换,后一次获取的可能不是期望的文件属性。如果有特殊文件操作需求,建议通过设置文件权限或给关键文件操作加锁的方式来保证一致性。
3.2 属性查询:时间、名称、路径与大小
| 属性 | 返回类型 | 功能 | 异常 |
|---|---|---|---|
creationTime | DateTime | 创建时间 | 底层系统接口出错时抛FSException |
lastAccessTime | DateTime | 最后访问时间 | 同上 |
lastModificationTime | DateTime | 最后修改时间 | 同上 |
name | String | 文件名或目录名 | - |
parentDirectory | Option<FileInfo> | 父级目录元数据 | - |
path | Path | 当前文件路径 | - |
size | Int64 | 文件大小 | 底层系统接口出错时抛FSException |
细节说明:
name与this.path.fileName等价,路径解析规则详见上文Path.fileName。parentDirectory以Option<FileInfo>形式返回:有父级返回Some(v),否则返回None(该类型定义见 core 包 Option 枚举)。size语义区分文件与目录:当前是文件时,表示单个文件占用磁盘空间的大小;当前是目录时,表示当前目录的所有文件占用磁盘空间的大小(不包含子目录)。- 时间属性返回的
DateTime类型来自 time 包(当前仓库 std 目录未包含 time 包源码,实际项目中通过依赖引入)。
3.3 权限与类型判断函数
权限判断(针对当前用户):
| 函数 | 文件语义 | 目录语义 | Windows 平台行为 |
|---|---|---|---|
canExecute() | 是否有执行权限 | 是否有进入目录权限 | 文件执行权限由扩展名决定;目录始终有权限,函数不生效返回true |
canRead() | 是否有读取权限 | 是否有浏览目录权限 | 用户始终拥有文件和目录的可读权限,函数不生效返回true |
canWrite() | 是否有写入权限 | 是否有删除、移动、创建目录内文件权限 | 文件可写权限正常使用;目录始终可写,函数不生效返回true |
isReadOnly() | 是否只读 | - | 文件只读权限正常使用;目录始终拥有删除修改权限,函数不生效返回false |
以上函数在 Linux 和 macOS 环境下正常使用;底层系统接口错误时抛出FSException。
文件类型判断:
| 函数 | 返回true的含义 | 异常 |
|---|---|---|
isDirectory() | 是目录 | 系统接口错误抛FSException |
isRegular() | 是普通文件 | 同上 |
isSymbolicLink() | 是软链接 | 同上 |
isHidden() | 是隐藏文件 | 无 |
3.4 权限设置函数
| 函数 | 参数 | 返回值 | 说明 |
|---|---|---|---|
setExecutable(executable: Bool) | 是否可执行 | Bool | 成功返回true,失败返回false |
setReadable(readable: Bool) | 是否可读 | Bool | 成功返回true,失败返回false |
setWritable(writable: Bool) | 是否可写 | Bool | 成功返回true,失败返回false |
使用要点:
- 对当前实例对应的文件设置当前用户权限;当前用户没有权限修改时抛出异常。
- Windows 平台差异:文件执行权限由扩展名决定、目录执行权限始终拥有(
setExecutable不生效返回false);文件和目录可读权限不可更改(readable为true返回true,为false返回false);目录可写权限不可更改(setWritable不生效返回false)。 - ⚠️竞争条件(Race Condition):如果在此函数调用期间,该
FileInfo对应的文件实体被其他用户或进程修改,有可能因为竞争条件导致其他修改不能生效。
3.5 判等:operator ==
operator func ==(that: FileInfo): Bool判断当前FileInfo与另一个FileInfo是否对应同一文件,返回true表示是同一文件,false表示不是。这也是FileInfo实现Equatable<FileInfo>接口的具体体现。
3.6 实战示例:FileInfo 基础操作
以下代码(节选自 fileinfo_samples.md)演示了创建临时文件、获取元数据、判断文件类型与权限、修改权限的完整流程:
import std.fs.* import std.time.DateTime main() { // 在当前目录下创建一个临时文件,以便下面 FileInfo 的演示 let curDirPath: Path = canonicalize(Path("./")) let file: File = File.createTemp(curDirPath) file.write("123456789\n".toArray()) let fileInfo: FileInfo = file.info file.close() /* 获得这个文件父级目录的 FileInfo,这个文件的父目录是当前目录 */ let parentDirectory: Option<FileInfo> = fileInfo.parentDirectory checkResult(parentDirectory == Some(FileInfo(curDirPath)), "The 'parentFileInfo' is obtained successfully.") /* 判断这个文件是否是软链接、普通文件、目录 */ checkResult(fileInfo.isSymbolicLink(), "The file is a symbolic link.") checkResult(fileInfo.isRegular(), "The file is a regular file.") checkResult(fileInfo.isDirectory(), "The file is a directory.") /* 判断这个文件对于当前用户是否是只读、隐藏、可执行、可读、可写 */ checkResult(fileInfo.isReadOnly(), "This file is read-only.") checkResult(fileInfo.isHidden(), "The file is hidden.") checkResult(fileInfo.canExecute(), "The file is executable.") checkResult(fileInfo.canRead(), "The file is readable.") checkResult(fileInfo.canWrite(), "The file is writable.") /* 修改当前用户对这个文件的权限,这里设置为对当前用户只读 */ checkResult(fileInfo.setExecutable(false), "The file was successfully set to executable.") checkResult(fileInfo.setReadable(true), "The file was successfully set to readable.") checkResult(fileInfo.setWritable(false), "The file was successfully set to writable.") checkResult(fileInfo.isReadOnly(), "This file is now read-only.") return 0 } func checkResult(result: Bool, message: String): Unit { if (result) { println(message) } }运行结果:
The 'parentFileInfo' is obtained successfully. The file is a regular file. The file is readable. The file is writable. The file was successfully set to executable. The file was successfully set to readable. The file was successfully set to writable. This file is now read-only.示例要点解读:
file.info是File类提供的元数据入口,返回FileInfo实例(见 File 类文档)。File.createTemp(curDirPath)在指定目录创建临时文件,文件名为tmpFileXXXXXX形式,不使用的临时文件需手动删除。parentDirectory与FileInfo(curDirPath)通过==判等成功,验证了"父级目录元数据"的语义。- 权限设置链
setExecutable(false)→setReadable(true)→setWritable(false)使文件进入只读状态,最终isReadOnly()返回true。
3.7 FileInfo 的典型获取途径
除直接构造外,FileInfo常通过以下途径获取:
File.info:获取已打开文件的元数据(File 类)。Directory.readFrom(path):返回目录下所有子项目的Array<FileInfo>,子项目顺序取决于文件在系统中的排序(Directory 类)。Directory.walk(path, f):非递归遍历目录子项目,对每个子项目回调(FileInfo) -> Bool,回调返回false时提前结束遍历。
四、FileDescriptor 结构体:操作系统文件句柄
4.1 类型声明与唯一属性
public struct FileDescriptorFileDescriptor用于获取文件句柄信息,仅提供一个只读属性:
public prop fileHandle: IntNativeIntNative是平台相关的有符号整型,其长度与当前系统的位宽一致(详见 core 包 intrinsics 文档)。
4.2 文件句柄(File Handle)概念
文件句柄是操作系统为了跟踪文件而分配的一种数据结构,用于标识一个打开文件的实例。文件句柄包含了文件的元数据信息(如文件名、路径、大小、修改时间等)以及文件数据在磁盘上的物理位置等信息。不同操作系统中文件句柄的形式有所不同:
- Unix 和 Linux 系统:文件句柄通常是一个非负整数,由操作系统内核分配,并在打开文件时返回给应用程序。
- Windows 系统:文件句柄通常是一个指向文件对象的指针,由操作系统内核分配,并在打开文件时返回给应用程序。
无论形式如何,应用程序都可以使用文件句柄执行文件的读取、写入、修改等操作。
4.3 获取方式:File.fileDescriptor
FileDescriptor本身不可直接构造(文档未提供公开构造函数),通常通过 File 类的 fileDescriptor 属性获取:
let file: File = File(Path("./test.txt"), OpenMode.Read) let fd: FileDescriptor = file.fileDescriptor // 获取操作系统级文件句柄 let handle: IntNative = fd.fileHandle file.close()使用
File对象时注意:创建的File对象会默认打开对应的文件,使用结束后需要及时调用close()关闭,否则会造成资源泄露。
五、异常处理与最佳实践
5.1 关联异常体系
结构体相关 API 主要抛出两类异常:
| 异常 | 抛出场景 |
|---|---|
| FSException | 底层系统接口出错、路径非法、文件不存在或无法访问等 |
| IllegalArgumentException | 路径为空、包含空字符/字符串结束符等参数非法情况 |
FSException继承自 IO 流异常类IOException,提供init()(无提示信息)与init(message: String)(带错误信息)两个构造函数。
5.2 非法路径的定义
多个 API 中反复提到"非法路径",指以下情况之一:
- 路径中包含非法字符,例如空格、制表符、换行符等;
- 路径中包含不合法的字符,例如特殊字符、控制字符等;
- 路径中包含不存在的目录或文件;
- 路径中包含无法访问的目录或文件,例如权限不足或被锁定等。
在输入路径时,应该避免使用非法字符,确保路径的合法性,以便正确地访问目标文件或目录。
5.3 竞争条件与一致性保障
FileInfo不缓存文件属性、每次现场获取,叠加setExecutable/setReadable/setWritable等权限设置函数,都可能遭遇其他进程并发修改同一文件实体的问题。实际工程中建议:
- 关键文件操作加锁:使用文件锁或业务级互斥保证同一时刻只有一个写入方;
- 权限收敛:对关键文件设置最小必要权限,降低被外部修改的风险;
- 以最新查询结果为准:不要在获取
FileInfo后长时间持有并使用其属性做后续判断,尤其是跨进程协作场景。
六、速查表与总结
Path 一句话速记:
- 拆路径用
parent/fileName/fileNameWithoutExtension/extensionName; - 拼路径用
join; - 清洗路径用
normalize(纯字符串)或canonicalize(访问文件系统、解析软链接、返回绝对路径); - 判等用
==(基于规范化后字符串比较)。
FileInfo 一句话速记:
- 查时间用
creationTime/lastAccessTime/lastModificationTime; - 查大小用
size(文件为自身大小,目录为该目录内所有文件大小); - 判类型用
isDirectory/isRegular/isSymbolicLink/isHidden; - 查/设权限用
canXxx/setXxx,注意 Windows 平台上目录权限相关函数多数不生效; - 始终记住:属性是现场获取的,不缓存。
FileDescriptor 一句话速记:
- 通过
File.fileDescriptor.fileHandle拿到IntNative类型的操作系统文件句柄,用于与底层系统交互。
进一步阅读:本文涉及的完整示例代码可参考 FileInfo 示例、Path 示例、File 示例 与 Directory 示例;API 全量清单见 fs 包 API 文档。
- 人工智能
- AI 技能
- 文档
- 开发工具
【免费下载链接】DocAgent
相关推荐
仓颉 std.fs 包结构体详解:FileDescriptor、FileInfo 与 Path 的路径操作及文件元数据实践
仓颉 std.fs 包结构体详解:FileDescriptor、FileInfo 与 Path 的路径操作及文件元数据实践 仓颉标准库 std.fs 包提供了对
文档知识库教程OpenHarmonyZerolang 文件系统编程指南:std.fs 模块能力体系与实战详解
Zerolang 文件系统编程指南:std.fs 模块能力体系与实战详解 std.fs 是 Zerolang(Zero)标准库中面向“宿主(hosted)”环境
仓颉(Cangjie)std.fs Path 使用详解:属性解析、拼接规范化与文件目录创建实战
仓颉(Cangjie)std.fs Path 使用详解:属性解析、拼接规范化与文件目录创建实战 Path 是仓颉标准库 std.fs 包中用于表示文件系统路径的
编程语言语言运行时标准库垃圾回收并发编程
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考