仓颉-IO流和文件操作
I/O 流概述
仓颉编程语言将与应用程序外部载体交互的操作称为 I/O 操作。I 对应输入(Input),O 对应输出(Output)。
仓颉编程语言所有的 I/O 机制都是基于数据流进行输入输出,这些数据流表示了字节数据的序列。数据流是一串连续的数据集合,它就像承载数据的管道,在管道的一端输入数据,在管道的另一端就可以输出数据。
仓颉编程语言将输入输出抽象为流(Stream)。
- 将数据从外存中读取到内存中的称为输入流(InputStream),输入端可以一段一段地向管道中写入数据,这些数据段会按先后顺序形成一个长的数据流。
- 将数据从内存写入外存中的称为输出流(OutputStream),输出端也可以一段一段地从管道中读出数据,每次可以读取其中的任意长度的数据(不需要跟输入端匹配),但只能读取先输入的数据,再读取后输入的数据。
有了这一层抽象,仓颉编程语言就可以使用统一的接口来实现与外部数据的交互。
仓颉编程语言将标准输入输出、文件操作、网络数据流、字符串流、加密流、压缩流等等形式的操作,统一用 Stream 描述。
Stream 主要面向处理原始二进制数据,Stream 中最小的数据单元是 Byte。
仓颉编程语言将 Stream 定义成了 interface,它让不同的 Stream 可以用装饰器模式进行组合,极大地提升了可扩展性。
输入流
程序从输入流读取数据源(数据源包括外界的键盘、文件、网络等),即输入流是将数据源读入到程序的通信通道。
仓颉编程语言用 InputStream 接口类型来表示输入流,它提供了 read 函数,这个函数会将可读的数据写入到 buffer 中,返回值表示了该次读取的字节总数。
InputStream 接口定义:
1 | interface InputStream { |
当拥有一个输入流的时候,就可以像下面的代码那样去读取字节数据,读取的数据会被写到 read 的入参数组中。
输入流读取示例:
1 | import std.io.InputStream |
输出流
程序向输出流写入数据。输出流是将程序中的数据输出到外界(显示器、打印机、文件、网络等)的通信通道。
仓颉编程语言用 OutputStream 接口类型来表示输出流,它提供了 write 函数,这个函数会将 buffer 中的数据写入到绑定的流中。
特别的,有一些输出流的 write 不会立即写到外存中,而是有一定的缓冲策略,只有当符合条件或主动调用 flush 时才会真实写入,目的是提高性能。
为了统一处理这些 flush 操作,在 OutputStream 中有一个 flush 的默认实现,它有助于抹平 API 调用的差异性。
OutputStream 接口定义:
1 | interface OutputStream { |
当拥有一个输出流时,可以写入字节数据。
输出流写入示例:
1 | import std.io.OutputStream |
数据流分类
按照数据流职责上的差异,可以将 Stream 简单分成两类:
- 节点流:直接提供数据源,节点流的构造方式通常是依赖某种直接的外部资源(如文件、网络等)。
- 处理流:只能代理其他数据流进行处理,处理流的构造方式通常是依赖其他的流。
I/O 节点流
节点流是指直接提供数据源的流,节点流的构造方式通常是依赖某种直接的外部资源(如文件、网络等)。
仓颉编程语言中常见的节点流包含标准流(ConsoleReader、ConsoleWriter)、文件流(File)、网络流(Socket)等。
本章介绍标准流和文件流。
标准流
标准流包含标准输入流、标准输出流和标准错误输出流。
标准流是程序与外部数据交互的标准接口。程序运行的时候从输入流读取数据,作为程序的输入,程序运行过程中输出的信息被传送到输出流,类似的,错误信息被传送到错误流。
在仓颉编程语言中可以使用 getStdIn、getStdOut、getStdErr 全局函数来分别获取这三个标准流。
使用上面的函数需要导入 env 包:
导入 env 包示例:
1 | import std.env.* |
env 包内的 ConsoleReader 和 ConsoleWriter 类型对这三个标准流都进行了易用性封装(标准错误输出流也是输出流,所以一共是两个类型),提供了更方便的基于 String 的扩展操作,并且对于很多常见类型都提供了丰富的重载来优化性能。
其中最重要的是 ConsoleReader 和 ConsoleWriter 类型提供了并发安全的保证,可以在任意线程中安全地通过该类型提供的接口来读写内容。
默认情况下标准输入流来源于键盘输入的信息,例如在命令行界面中输入的文本。
当需要从标准输入流中获取数据时,可以通过 getStdIn 全局函数获取 ConsoleReader 类型,再通过该类型的 readln 函数获取命令行的输入。
标准输入流读取示例:
1 | import std.env.* |
运行上面的代码,在命令行上输入一些文字,然后换行结束,即可看到输入的内容。
输出流分为标准输出流和标准错误流,默认情况下,它们都会输出到屏幕,例如在命令行界面中看到的文本。
当需要往标准输出流中写入数据时,可以通过 getStdOut/getStdErr 全局函数获取 ConsoleWriter 来写入,例如通过 write 函数来向控制台打印内容。
使用 ConsoleWriter 和直接使用 print 函数的差别是,ConsoleWriter 是并发安全的,并且由于 ConsoleWriter 使用了缓存技术,在输入内容较多时拥有更好的性能表现。
需要注意的是,写完数据后需要对 ConsoleWriter 调用 flush 才能保证内容被完整写到标准流中。
标准输出流写入示例:
1 | import std.env.* |
文件流
仓颉编程语言提供了 fs 包来支持通用文件系统任务。不同的操作系统对于文件系统提供的接口有所不同。仓颉编程语言抽象出以下一些共通的功能,通过统一的功能接口,屏蔽不同操作系统之间的差异,来简化使用。
常规操作任务包括:创建文件/目录、读写文件、重命名或移动文件/目录、删除文件/目录、复制文件/目录、获取文件/目录元数据、检查文件/目录是否存在。具体 API 可以查阅库文档。
使用文件系统相关的功能需要导入 fs 包:
导入 fs 包示例:
1 | import std.fs.* |
本章会着重介绍 File 相关的使用,对于 Path 和 Directory 的使用可以查阅对应的 API 文档。
File 类型在仓颉编程语言中同时提供了常规文件操作和文件流两类功能。
常规文件操作
对于常规的文件操作,可以使用一系列静态函数来完成快捷的操作。
例如如果要检查某个路径对应的文件是否存在,可以使用 exists 函数。当 exists 函数返回 true 时表示文件存在,反之不存在。
exists 函数使用示例:
1 | import std.fs.* |
移动文件、拷贝文件和删除文件也非常简单,File 同样提供了对应的静态函数 move、copy、delete。
move、copy、delete 函数使用示例:
1 | import std.fs.* |
如果需要直接将文件的所有数据读出来,或者一次性将数据写入文件里,可以使用 File 提供的 readFrom、writeTo 函数直接读写文件。在数据量较少的情况下它们既简单易用又能提供较好的性能表现,无需手动处理数据流。
readFrom、writeTo 函数使用示例:
1 | import std.fs.* |
文件流操作
除了上述的常规文件操作之外,File 类型也被设计为一种数据流类型,因此 File 类型本身实现了 IOStream 接口。当创建了一个 File 的实例,可以把这个实例当成数据流来使用。
File 类定义:
1 | public class File <: Resource & IOStream & Seekable { |
File 提供了两种构造方式,一种是通过方便的静态函数 create 直接创建新文件的实例,另一种是通过构造函数传入完整的打开文件模式来构造新实例。
其中 create 创建的文件是只写的,不能对实例进行读操作,否则会抛出运行时异常。
File 构造示例:
1 | // 创建 |
当需要更精细的打开模式时,可以使用构造函数传入一个 OpenMode 值。OpenMode 是一个 enum 类型,它提供了丰富的文件打开模式,包含 Read、Write、Append 和 ReadWrite 模式。
File 打开模式使用示例:
1 | // 使用指定选项打开模式 |
因为打开 File 的实例会占用宝贵的系统资源,所以使用完 File 的实例之后需要注意及时关闭 File,以释放系统资源。
File 实现了 Resource 接口,在大多数时候都可以使用 try-with-resource 语法来简化使用。
try-with-resource 语法使用示例:
1 | try (file2 = File("./tempFile.txt", Read)) { |
I/O 处理流
处理流是指代理其他数据流进行处理的流。
仓颉编程语言中常见的处理流包含 BufferedInputStream、BufferedOutputStream、StringReader、StringWriter、ChainedInputStream 等。
本章介绍缓冲流和字符串流。
缓冲流
由于涉及磁盘的 I/O 操作相比内存的 I/O 操作要慢很多,所以对于高频次且小数据量的读写操作来说,不带缓冲的数据流效率很低,每次读取和写入数据都会带来大量的 I/O 耗时。而带缓冲的数据流,可以多次读写数据,但不触发磁盘 I/O 操作,只是先放到内存里。等凑够了缓冲区大小的时候再一次性操作磁盘,这种方式可以显著减少磁盘操作次数,从而提升性能表现。
仓颉编程语言标准库提供了 BufferedInputStream 和 BufferedOutputStream 这两个类型用来提供缓冲功能。
使用 BufferedInputStream 和 BufferedOutputStream 类型需要导入 io 包。
导入 io 包示例:
1 | import std.io.* |
BufferedInputStream 的作用是为另一个输入流添加缓冲的功能。本质上 BufferedInputStream 是通过一个内部缓冲数组实现的。
当通过 BufferedInputStream 来读取流的数据时,BufferedInputStream 会一次性读取整个缓冲区大小的数据,再使用 read 函数就可以分多次读取更小规模的数据;当缓冲区中的数据被读完之后,输入流就会再次填充缓冲区;如此反复,直到读完数据流的所有数据。
如果构造一个 BufferedInputStream,只需要在构造函数中传入另一个输入流。如果需要指定缓冲区的大小,也可以额外传入 capacity 参数进行指定。
BufferedInputStream 构造示例:
1 | import std.io.{ByteBuffer, BufferedInputStream} |
BufferedOutputStream 的作用是为另一个输出流添加缓冲的功能。BufferedOutputStream 也是通过一个内部缓冲数组实现的。
当通过 BufferedOutputStream 来向输出流写入数据时,write 的数据会先写入内部缓冲数组中;当缓冲区中的数据被填满之后,BufferedOutputStream 会将缓冲区的数据一次性写入输出流中,然后清空缓冲区再次被写入;如此反复,直到写完所有的数据。
需要注意的是,由于没写够缓冲区时不会触发输出流的写入操作,所以当往 BufferedOutputStream 写完所有的数据后,需要额外调用 flush 函数来最终完成写入。
如果构造一个 BufferedOutputStream,只需要在构造函数中传入另一个输出流。如果需要指定缓冲区的大小,也可以额外传入 capacity 参数指定。
BufferedOutputStream 构造示例:
1 | import std.io.* |
字符串流
由于仓颉编程语言的输入流和输出流是基于字节数据来抽象的(拥有更好的性能),在部分以字符串为主的场景中使用起来不太友好,例如往文件里写入大量的文本内容时,需要将文本内容转换成字节数据,再写入文件。
为了提供友好的字符串操作能力,仓颉编程语言提供了 StringReader 和 StringWriter 来添加字符串处理能力。
使用 StringReader 和 StringWriter 类型需要导入 io 包:
导入 io 包示例:
1 | import std.io.* |
StringReader 提供了按行读、按筛选条件读的能力,相比将字节数据读出来再手动转换成字符串,具有更好的性能表现和易用性。
如果构造 StringReader,传入另一个输入流即可。
StringReader 使用示例:
1 | import std.io.* |
StringWriter 提供了直接写字符串、按行直接写字符串的能力,相比将字节数据手动转换成字符串再写入,具有更好的性能表现和易用性。
如果构造 StringWriter,传入另一个输出流即可。
StringWriter 使用示例:
1 | import std.io.* |