I/O 节点流
节点流是指直接提供数据源的流,节点流的构造方式通常是依赖某种直接的外部资源(即文件、网络等)。
仓颉编程语言中常见的节点流包含标准流(StdIn、StdOut、StdErr)、文件流(File)、网络流(Socket)等。
我们本章会着重介绍一下标准流和文件流。
标准流
标准流包含了标准输入流(stdin)、标准输出流(stdout)和标准错误输出流(stderr)。
标准流是我们的程序与外部数据交互的标准接口。程序运行的时候从输入流读取数据,作为程序的输入,程序运行过程中输出的信息被传送到输出流,类似的,错误信息被传送到错误流。
在仓颉编程语言中我们可以使用 Console
类型来分别访问它们。
使用 Console
类型需要导入 console
包:
导入 console 包示例:
import std.console.*
Console
对三个标准流都进行了易用性封装,提供了更方便的基于 String
的扩展操作,并且对于很多常见类型都提供了丰富的重载来优化性能。
其中最重要的是 Console
提供了并发安全的保证,我们可以在任意线程中安全的通过 Console
提供的接口来读写内容。
默认情况下标准输入流来源于键盘输入的信息,例如我们在命令行界面中输入的文本。
当我们需要从标准输入流中获取数据时,可以通过 stdIn
来读取,例如通过 readln
函数来获取命令行的输入。
标准输入流读取示例:
import std.console.*
main() {
let txt = Console.stdIn.readln()
println(txt ?? "")
}
运行上面的代码,在命令行上输入一些文字,然后换行结束,我们就能看到我们输入的内容了!
输出流分为标准输出流和标准错误流,默认情况下,它们都会输出到屏幕,例如我们在命令行界面中看到的文本。
当我们需要往标准输出流中写入数据时,可以通过 stdOut
/stdErr
来写入,例如通过 write
函数来向命令打印内容。
使用 stdOut
和直接使用 print
函数的差别是,stdOut
是并发安全的,并且由于 stdOut
使用了缓存技术,在输入内容较多时拥有更好的性能表现。
需要注意的是,写完数据后我们需要对 stdOut
调用 flush
才能保证内容被写到标准流中。
标准输出流写入示例:
import std.console.*
main() {
for (i in 0..1000) {
Console.stdOut.writeln("hello, world!")
}
Console.stdOut.flush()
}
文件流
仓颉编程语言提供了 fs
包来支持通用文件系统任务。虽然不同的操作系统对于文件系统提供的接口有所不同,但是仓颉编程语言抽象出以下一些共通的功能,通过统一的功能接口,屏蔽不同操作系统之间的差异,来简化我们的使用。
这些常规操作任务包括:创建文件/目录、读写文件、重命名或移动文件/目录、删除文件/目录、复制文件/目录、获取文件/目录元数据、检查文件/目录是否存在。具体 API 可以查阅库文档。
使用文件系统相关的功能需要导入 fs
包:
导入 fs 包示例:
import std.fs.*
本章会着重介绍 File
相关的使用,对于 Path
和 Directory
的使用可以查阅对应的 API 文档。
File
类型在仓颉编程语言中同时提供了常规文件操作和文件流两类功能。
常规文件操作
对于常规的文件操作,我们可以使用一系列静态函数来完成快捷的操作。
例如我们如果要检查某个路径对应的文件是否存在,可以使用 exists
函数。当 exists
函数返回 true
时表示文件存在,反之不存在。
exists 函数使用示例:
import std.fs.*
main() {
let exist = File.exists("./tempFile.txt")
println("exist: ${exist}")
}
移动文件、拷贝文件和删除文件也非常简单,File
同样提供了对应的静态函数 move
、copy
、delete
。
move、copy、delete 函数使用示例:
import std.fs.*
main() {
File.copy("./tempFile.txt", "./tempFile2.txt", false)
File.move("./tempFile2.txt", "./tempFile3.txt", false)
File.delete("./tempFile3.txt")
}
如果我们需要直接将文件的所有数据读出来,或者一次性将数据写入文件里,我们可以使用 File
提供的 readFrom
、writeTo
函数直接读写文件。在数据量较少的情况下它们既简单易用又能提供较好的性能表现,让我们不需要手动处理数据流的事情。
readFrom、writeTo 函数使用示例:
import std.fs.*
main() {
let bytes = File.readFrom("./tempFile.txt") // 一次性读取了所有的数据
File.writeTo("./otherFile.txt", bytes) // 把数据一次性写入另一个文件中
}
文件流操作
除了上述的常规文件操作之外,File
类型也被设计为一种数据流类型,因此 File
类型本身实现了 IOStream
接口。当我们创建了一个 File
的实例,我们就可以把这个实例当成数据流来使用。
File 类定义:
public class File <: Resource & IOStream & Seekable {
...
}
File
提供了两种构造方式,一种是通过两个方便的静态函数 openRead
/create
直接打开文件或创建新文件的实例,另一种是通过构造函数传入完整的打开文件选项来构造新实例。
其中,openRead
打开的文件是只读的,我们不能对实例进行写操作,否则会抛出运行时异常;而 create
创建的文件是只写的,我们不能对实例进行读操作,否则也会抛出运行时异常。
File 构造示例:
// 创建
internal import std.fs.*
main() {
let file = File.create("./tempFile.txt")
file.write("hello, world!".toArray())
// 打开
let file2 = File.openRead("./tempFile.txt")
let bytes = file2.readToEnd() // 读取所有数据
println(bytes)
}
当我们需要更精细的打开选项时,可以使用构造函数传入一个 OpenOption
值。OpenOption
是一个 enum
类型,它提供了丰富的文件打开选项,例如 Append
、Create
、Truncate
、Open
以及其它便捷的复合操作。
File 打开选项使用示例:
// 使用指定选项打开文件
let file = File("./tempFile.txt", OpenOption.Truncate(false))
...
因为我们打开 File
的实例会占用宝贵的系统资源,所以使用完 File
的实例之后需要注意及时关闭 File
,以释放系统资源。
所幸 File
实现了 Resource
接口,我们在大多数时候都可以使用 try-with-resource 语法来简化我们的使用。
try-with-resource 语法使用示例:
try (file2 = File.openRead("./tempFile.txt")) {
...
// 结束使用后自动释放文件
}