news 2026/10/2 10:09:04

【仓颉语言入门 · 第22课】

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
【仓颉语言入门 · 第22课】

【仓颉语言入门 · 第22课】文件与目录 IO:让程序的数据持久化

前 21 课的数据都活在内存里,程序一关就没了。本课带你掌握std.fs 文件系统库:读写文本文件、遍历目录、处理路径拼接,最后把第 21 课的订单数据保存到文件,重启程序还能读出来。

本文所有代码均在仓颉 SDK 1.2.0 下逐行实测编译运行。


目录(系列导航)

整套路线共7 个模块、30 课:

模块课次内容
一、环境与入门01~05环境搭建与 Hello World、变量与基本类型、运算符与输入输出、分支、循环
二、常用类型与数据组织06~10字符串、数组与区间、ArrayList/HashMap/HashSet、可空类型、错误处理
三、函数与函数式11~14函数、Lambda 与高阶函数、闭包、迭代器与惰性序列
四、面向对象与类型系统15~20struct/class、构造与属性、接口、枚举与 match 模式匹配、泛型、扩展
五、工程化与标准库21~25cjpm 包管理与多文件、文件 IO、JSON 处理、网络编程、单元测试
六、并发编程26~28线程、Channel 通道与同步原语、并发实战
七、项目实战29~30命令行小工具、GeoJSON 数据处理实战
  1. 环境搭建与第一个仓颉程序
  2. 变量与常量:let / var 与基本数据类型
  3. 运算符与标准输入输出
  4. 分支结构:if 与 match 表达式
  5. 循环结构:while / for / Range
  6. 字符串详解与字符串插值
  7. 数组 Array 与区间 Range
  8. 集合框架:ArrayList、HashMap、HashSet
  9. 可空类型?与 Option
  10. 错误处理:异常机制与 Result
  11. 函数定义、参数与返回值
  12. Lambda 与高阶函数
  13. 闭包、作用域与函数类型
  14. 迭代器 Iterator 与 Sequence
  15. 结构体 struct 与类 class
  16. 构造函数、属性与方法
  17. 接口 interface 与实现
  18. 枚举 enum、代数数据类型与 match 模式匹配
  19. 泛型编程
  20. 扩展、类型别名与可见性控制
  21. cjpm 包管理与多文件项目组织
  22. 文件与目录 IO(本文)
  23. JSON 处理(结合 stdx 扩展库)
  24. 网络编程入门
  25. 单元测试
  26. 并发基础:线程的创建与等待
  27. Channel 通道与同步原语
  28. 并发实战:多线程任务处理
  29. 实战一:带文件持久化的命令行小工具
  30. 实战二:GeoJSON 数据处理程序

一、为什么需要文件 IO

回顾第 21 课的订单系统:

main(): Int64 { let orders = [ Order("NO.1001", 1250), Order("NO.1002", 800) ] // 处理订单... return 0 }

问题很明显:订单数据硬编码在代码里,每次改数据都要重新编译。真实场景应该是:

  • 启动时从文件加载订单列表;
  • 用户新增/修改订单后保存回文件;
  • 下次启动时读取最新的数据。

文件 IO 就是让程序能读写磁盘上的文件,实现数据持久化。

仓颉标准库提供std.fs模块处理文件系统操作,核心类型有三个:

类型作用常用场景
Path路径抽象(不含文件内容)拼接路径、判断文件是否存在
File文件读写读文本、写文本、追加内容
Directory目录操作创建目录、遍历子文件、删除目录

二、Path:处理文件路径

2.1 创建 Path 对象

import std.fs.Path main(): Int64 { // 从字符串创建路径 let p1 = Path("data/orders.txt") // 拼接路径(自动处理分隔符) let p2 = Path("data").join("2024").join("orders.txt") println("p1 = ${p1}") println("p2 = ${p2}") return 0 }

运行输出(Windows 下):

p1 = data/orders.txt p2 = data\2024\orders.txt

注意:Path构造函数保留原始分隔符(/不会转成\),但join()会用系统分隔符拼接。

2.2 判断路径是否存在

判断存在用的是std.fs的包级函数exists()(不是 Path 的成员方法):

import std.fs.{Path, exists} main(): Int64 { let filePath = Path("test.txt") let dirPath = Path("data") println("test.txt 存在? ${exists(filePath)}") println("data 目录存在? ${exists(dirPath)}") return 0 }

如果test.txt和data都不存在,输出:

test.txt 存在? false data 目录存在? false

注意:要区分"是文件"还是"是目录",需要用FileInfo(见 4.2 节)。

2.3 获取文件名、父目录、扩展名

import std.fs.Path main(): Int64 { let p = Path("data/2024/orders.txt") println("文件名:${p.fileName}") // orders.txt println("父目录:${p.parent}") // data/2024 println("扩展名:${p.extensionName}") // txt println("不含扩展名的文件名:${p.fileNameWithoutExtension}") // orders println("是否绝对路径:${p.isAbsolute()}") // false println("是否相对路径:${p.isRelative()}") // true return 0 }

运行输出:

文件名:orders.txt 父目录:data/2024 扩展名:txt 不含扩展名的文件名:orders 是否绝对路径:false 是否相对路径:true

注意:fileName、parent、extensionName是属性(无括号),isAbsolute()、isRelative()是函数(有括号)。parent返回空字符串表示没有父目录。


三、File:读写文本文件

3.1 写入文本(覆盖模式)

import std.fs.{Path, File, OpenMode} main(): Int64 { let path = Path("hello.txt") let content = "你好,仓颉!\n这是第二行。\n" // 以写模式打开(不存在则创建,存在则截断为0字节) let file = File(path, OpenMode.Write) // 写入字符串(需转 Array<UInt8>) file.write(content.toArray()) // 关闭文件(释放资源) file.close() println("写入完成:${path}") return 0 }

运行后会在当前目录生成hello.txt,内容:

你好,仓颉! 这是第二行。

注意:File.create()也能创建文件,但如果文件已存在会抛异常。覆盖写已有文件请用File(path, OpenMode.Write)。

3.2 读取文本

import std.fs.{Path, File, exists} main(): Int64 { let path = Path("hello.txt") if (!exists(path)) { println("文件不存在") return 1 } // 读取全部字节并转字符串 let bytes = File.readFrom(path) let content = String.fromUtf8(bytes) println("文件内容:") println(content) return 0 }

运行输出:

文件内容: 你好,仓颉! 这是第二行。

注意:File.readFrom(path)是静态方法,直接返回Array<UInt8>,无需手动 open/close。也可以用File(path, OpenMode.Read)+file.read(buffer)分块读取大文件。

3.3 追加内容(不清空原文件)

import std.fs.{Path, File, OpenMode} main(): Int64 { let path = Path("log.txt") // 追加模式打开(不存在则创建,存在则在末尾追加) let file = File(path, OpenMode.Append) file.write("[2024-10-01 10:00] 程序启动\n".toArray()) file.write("[2024-10-01 10:05] 处理订单\n".toArray()) file.close() println("日志已追加") return 0 }

运行两次后,log.txt内容:

[2024-10-01 10:00] 程序启动 [2024-10-01 10:05] 处理订单 [2024-10-01 10:00] 程序启动 [2024-10-01 10:05] 处理订单

3.4 逐行读取

仓颉没有内置的readLine(),逐行读取需要先读全部内容,再按行分割:

import std.fs.{Path, File} main(): Int64 { let path = Path("data.txt") // 读全部内容 let bytes = File.readFrom(path) let content = String.fromUtf8(bytes) // 按行分割 let lines = content.split("\n") var lineNum = 1 for (line in lines) { if (line == "") { continue // 跳过空行 } println("第 ${lineNum} 行:${line}") lineNum += 1 } return 0 }

假设data.txt内容:

苹果 香蕉 橙子

运行输出:

第 1 行:苹果 第 2 行:香蕉 第 3 行:橙子

四、Directory:目录操作

4.1 创建目录

import std.fs.{Path, Directory, exists} main(): Int64 { let dirPath = Path("output/2024/logs") // 递归创建目录(包括所有父目录) if (!exists(dirPath)) { Directory.create(dirPath, recursive: true) } println("目录已创建:${dirPath}") return 0 }

运行后会创建output/2024/logs/三层目录。

注意:Directory.create默认不递归,传recursive: true才能一次创建多层;如果目录已存在会抛异常,通常先判断exists()。

4.2 遍历目录下的文件

Directory.readFrom(path)返回Array<FileInfo>,FileInfo提供name、isRegular()、isDirectory()等:

import std.fs.{Path, Directory, exists} main(): Int64 { let dirPath = Path("data") if (!exists(dirPath)) { println("目录不存在") return 1 } // 获取目录下的所有条目(文件 + 子目录) let entries = Directory.readFrom(dirPath) for (entry in entries) { if (entry.isRegular()) { println("文件:${entry.name}") } else if (entry.isDirectory()) { println("目录:${entry.name}") } } return 0 }

假设data/目录结构:

data/ ├── orders.txt ├── users.txt └── backup/ └── old.txt

运行输出:

文件:orders.txt 文件:users.txt 目录:backup

注意:Directory.readFrom()只列出直接子项,不会递归进子目录。要递归遍历,见 4.4 节。

4.3 删除文件或目录

删除用的是std.fs的包级函数remove(path, recursive:):

import std.fs.{Path, remove, exists} main(): Int64 { let filePath = Path("temp.txt") let dirPath = Path("temp_dir") // 删除文件 if (exists(filePath)) { remove(filePath, recursive: false) println("文件已删除") } // 删除空目录(如果目录非空会报错) if (exists(dirPath)) { remove(dirPath, recursive: false) println("目录已删除") } return 0 }

注意:recursive: false时,目录非空会抛异常;传recursive: true可递归删除非空目录(谨慎使用)。也可以用removeIfExists(path, recursive:),它返回Bool,且路径不存在时不抛异常。

4.4 递归遍历目录

import std.fs.{Path, Directory, exists} // 递归打印目录树 func printTree(dir: Path, indent: String): Unit { let entries = Directory.readFrom(dir) for (entry in entries) { println("${indent}${entry.name}") if (entry.isDirectory()) { printTree(entry.path, indent + " ") } } } main(): Int64 { let root = Path("data") if (!exists(root)) { println("目录不存在") return 1 } println(root.fileName) printTree(root, " ") return 0 }

假设data/结构:

data/ ├── orders.txt └── backup/ ├── 2023.txt └── 2024/ └── old.txt

运行输出:

data orders.txt backup 2023.txt 2024 old.txt

注意:递归遍历中通过entry.path(FileInfo的完整路径属性)继续深入子目录,而不是直接用entry(类型是FileInfo,不是Path)。


五、实战:把订单数据保存到文件

我们把第 21 课的订单系统改造成从文件加载、保存到文件。

5.1 数据文件格式

用简单的文本格式(每行一个订单):

NO.1001|1250|已支付 NO.1002|800|待支付 NO.1003|99|已支付

字段用|分隔:订单号|金额(分)|状态。

5.2 代码实现

目录结构:

ordersys/ ├── cjpm.toml └── src/ ├── main.cj ├── models/ │ └── order.cj └── storage/ └── file_store.cj

src/models/order.cj(和第 21 课相同):

package ordersys.models public class Order { public let id: String private var paid: Bool = false private let amountFen: Int64 public init(id: String, amountFen: Int64) { this.id = id this.amountFen = amountFen } public func pay(): Unit { this.paid = true } public func isPaid(): Bool { return this.paid } public func getAmountFen(): Int64 { return this.amountFen } }

src/storage/file_store.cj(新增,负责文件读写):

package ordersys.storage import std.fs.{Path, File, Directory, OpenMode, exists} import std.collection.ArrayList import std.convert.* import ordersys.models.Order // 把订单列表保存到文件 public func saveOrders(orders: Array<Order>, filePath: String): Unit { let path = Path(filePath) // 确保父目录存在 let parent = path.parent if (parent.toString() != "" && !exists(parent)) { Directory.create(parent, recursive: true) } // OpenMode.Write:不存在则创建,存在则截断覆盖 let file = File(path, OpenMode.Write) for (order in orders) { let status = if (order.isPaid()) { "已支付" } else { "待支付" } let line = "${order.id}|${order.getAmountFen()}|${status}\n" file.write(line.toArray()) } file.close() } // 从文件加载订单列表 public func loadOrders(filePath: String): Array<Order> { let path = Path(filePath) let result = ArrayList<Order>() if (!exists(path)) { println("文件不存在,返回空列表:${filePath}") return result.toArray() } // 读取全部内容 let bytes = File.readFrom(path) let content = String.fromUtf8(bytes) // 按行分割 let lines = content.split("\n") for (line in lines) { if (line == "") { continue } // 解析行:NO.1001|1250|已支付 let parts = line.split("|") if (parts.size < 3) { continue // 跳过格式错误的行 } let id = parts[0] let amountFen = Int64.parse(parts[1]) let paid = parts[2] == "已支付" let order = Order(id, amountFen) if (paid) { order.pay() } result.add(order) } return result.toArray() }

src/main.cj:

package ordersys import ordersys.models.Order import ordersys.storage.{saveOrders, loadOrders} main(): Int64 { let filePath = "data/orders.txt" // 从文件加载订单 var orders = loadOrders(filePath) if (orders.size == 0) { println("首次运行,创建测试数据...") orders = [ Order("NO.1001", 1250), Order("NO.1002", 800), Order("NO.1003", 99) ] } // 显示订单列表 println("=== 订单列表 ===") for (o in orders) { let status = if (o.isPaid()) { "已支付" } else { "待支付" } println("订单(${o.id}) ${o.getAmountFen()} 分 [${status}]") } // 模拟支付第一个订单 if (orders.size > 0) { orders[0].pay() println("\n订单 ${orders[0].id} 已支付") } // 保存回文件 saveOrders(orders, filePath) println("\n数据已保存到 ${filePath}") return 0 }

5.3 运行效果

第一次运行(文件不存在):

文件不存在,返回空列表:data/orders.txt 首次运行,创建测试数据... === 订单列表 === 订单(NO.1001) 1250 分 [待支付] 订单(NO.1002) 800 分 [待支付] 订单(NO.1003) 99 分 [待支付] 订单 NO.1001 已支付 数据已保存到 data/orders.txt

同时生成data/orders.txt文件,内容:

NO.1001|1250|已支付 NO.1002|800|待支付 NO.1003|99|待支付

第二次运行(从文件读取):

=== 订单列表 === 订单(NO.1001) 1250 分 [已支付] 订单(NO.1002) 800 分 [待支付] 订单(NO.1003) 99 分 [待支付] 订单 NO.1001 已支付 数据已保存到 data/orders.txt

订单NO.1001的支付状态被记住了!


六、常用 API 速查

功能API示例
创建路径Path(str)Path("data/orders.txt")
拼接路径path.join(sub)Path("data").join("orders.txt")
判断存在exists(path)if (exists(path)) { ... }
判断是文件entry.isRegular()if (entry.isRegular()) { ... }
判断是目录entry.isDirectory()if (entry.isDirectory()) { ... }
写文件(覆盖)File(path, OpenMode.Write)File(Path("a.txt"), OpenMode.Write)
写文件(追加)File(path, OpenMode.Append)File(Path("log.txt"), OpenMode.Append)
静态读文件File.readFrom(path)File.readFrom(Path("a.txt"))
静态写文件File.writeTo(path, bytes)File.writeTo(path, str.toArray())
逐行读先readFrom再split("\n")见 3.4 节
写字符串file.write(bytes)file.write("hello\n".toArray())
关闭文件file.close()file.close()
创建目录Directory.create(path, recursive: true)Directory.create(p, recursive: true)
列出目录内容Directory.readFrom(path)Directory.readFrom(Path("data"))
删除文件/目录remove(path, recursive: Bool)remove(path, recursive: false)
安全删除removeIfExists(path, recursive:)removeIfExists(path, recursive: false)

七、常见问题 FAQ

Q1:OpenMode.Write和OpenMode.Append有什么区别?

  • OpenMode.Write:覆盖模式,如果文件已存在会清空内容;
  • OpenMode.Append:追加模式,在文件末尾追加内容,不会清空已有数据。

另外注意:File.create(path)只能创建新文件,文件已存在时会抛异常。

Q2:忘记file.close()会怎样?

文件句柄会一直占用,直到程序退出。如果程序长时间运行且频繁打开文件不关闭,会导致"文件句柄耗尽"错误。建议每次打开文件后都记得关闭。

Q3:路径用/还是\?

仓颉的Path会自动处理分隔符,推荐用/(跨平台兼容),Windows 下也能正常工作。

Q4:读写文件出错了怎么办?

文件操作会抛出异常(如文件不存在、权限不足)。可以用try-catch捕获(第 10 课讲过):

try { let bytes = File.readFrom(Path("data.txt")) println(String.fromUtf8(bytes)) } catch (e: Exception) { println("读取文件失败:${e.message}") }

Q5:文本文件和二进制文件有什么区别?

本课讲的都是文本文件(能用记事本打开的.txt、.csv)。如果是图片、视频等二进制文件,同样用File.readFrom()读字节数组、file.write(bytes)写字节,只是不做 UTF-8 字符串转换。

Q6:Directory.readFrom()会递归子目录吗?

不会。它只列出直接子项。要递归遍历,需要自己写递归函数(见 4.4 节)。


八、课后练习

  1. 写一个程序,创建notes/目录,在里面生成note1.txt、note2.txt、note3.txt三个文件,内容分别是"笔记一"、“笔记二”、“笔记三”。
  2. 在第 1 题的基础上,遍历notes/目录,打印每个文件的文件名和内容。
  3. 写一个日志记录器函数log(message: String),每次调用把消息追加到app.log文件,格式:[时间] 消息内容。
  4. 把第 21 课的订单系统改造成:启动时从data/orders.txt加载订单,用户输入命令(如pay NO.1001)修改订单状态后保存回文件。
  5. 挑战:写一个递归函数countFiles(dir: Path): Int64,统计目录下(包括所有子目录)有多少个文件(不含目录)。

下节预告

文本文件虽然能用,但格式太简陋(NO.1001|1250|已支付),不适合复杂数据。第 23 课JSON 处理将讲解:如何用stdx.json库把订单对象序列化成 JSON、从 JSON 反序列化成对象,让数据格式更通用、更易读。


系列说明:本系列基于 Windows 平台 + CIDE + 仓颉 SDK(1.2.0)编写,所有代码均已实际编译运行通过。如遇 SDK 版本差异导致的细节出入,以你本地版本为准,欢迎评论区交流。


📥 工具下载

本系列全程使用的仓颉 IDE ——CIDE(免费开源、社区版):

  • GitCode 仓库 / 安装包下载:https://gitcode.com/wp_upala/cide
  • 打开页面后进入发行版(Releases),两种包任选其一:
    • 安装版:下载CIDE-<版本>-x64-Setup.exe,双击安装,适合日常长期使用;
    • 免安装版(Portable):下载CIDE-<版本>-x64-Portable.zip,解压到任意目录即用,不写注册表、不留安装痕迹,拷到 U 盘也能在别的电脑直接运行(包内附《使用说明.txt》)。适合先试用、或在受限电脑上学习本系列课程。
  • 仓颉 SDK 请前往仓颉编程语言官网下载:https://cangjie-lang.cn
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/2 10:08:51

AI助教配置实战:从答非所问到精准辅助的三层模型

1. 为什么你的AI助教总是“答非所问”很多人第一次把AI助教接进项目里&#xff0c;满心欢喜地以为终于可以解放双手了&#xff0c;结果用了不到三天就发现&#xff1a;这玩意儿怎么像个刚入职的实习生&#xff0c;问啥啥不知道&#xff0c;让它改个代码能把整个文件删了&#x…

作者头像 李华
网站建设 2026/10/2 10:08:14

Python爬虫反爬虫策略分层解析与Selenium自动化抗检测实战

先说一个我最近接到的真实需求。对方要爬一个中等规模的电商站点&#xff0c;页面结构看似平淡无奇&#xff0c;用requests两三行就能拿到首屏HTML。可等我真正写下去却发现&#xff0c;翻到第二页时接口返回的是一段被加密过的JSON&#xff0c;再往下请求直接出现403&#xff…

作者头像 李华
网站建设 2026/10/2 10:07:35

HER算法详解:用后见之明破解强化学习稀疏奖励难题

第一次看到Hindsight Experience Replay&#xff08;HER&#xff09;这个名字&#xff0c;我就觉得这篇论文把“hindsight&#xff08;后见之明&#xff09;”用得太妙了。在目标条件强化学习&#xff08;goal-conditioned RL&#xff09;里&#xff0c;最折磨人的往往不是算法…

作者头像 李华
网站建设 2026/10/2 10:06:55

端侧AI系统工程实战:从模型选型到监控迭代的闭环设计

1. 端侧 AI 系统工程到底在解决什么问题1.1 从“能跑起来”到“跑得久”的认知转变很多人第一次接触端侧 AI&#xff0c;脑子里想的都是“把模型塞进设备里跑通就行”。我刚开始做这块的时候也是这个心态&#xff0c;拿一个开源模型&#xff0c;转成 ONNX&#xff0c;量化一下&…

作者头像 李华
网站建设 2026/10/2 10:06:40

风光火储联合调频Simulink仿真:一次调频与AGC建模实战

1. 风光火储联合调频&#xff0c;为什么突然成了Simulink仿真的主战场做电力系统仿真这行的人&#xff0c;最近几年感受应该都很明显&#xff1a;以前搭个"发电机负荷"的简化频率响应模型就够写论文了&#xff0c;现在不行了。风电、光伏占比一路往上走&#xff0c;电…

作者头像 李华
网站建设 2026/10/2 10:06:15

西门子S7-1200 PLC与变频器Modbus通讯在溢流水循环系统中的实战应用

接手这个项目的时候,厂里的需求一句话就能说清:冷却水池的溢流水不能再哗哗排地沟了,得收回蓄水箱,再用变频泵打到循环用水点。但真做起来,水位稳不稳、泵会不会烧、变频器跟PLC怎么对得上话、触摸屏怎么让操作工一眼看懂——每一件事都在考验方案的细节。这套系统我用的硬件是…

作者头像 李华