使用新版本 (2024-07-19 16:10发布的)
1、I/O 流概述
本章我们会介绍基本的 I/O 概念和文件操作。
在仓颉编程语言中,我们将与应用程序外部载体交互的操作
称之为 I/O 操作
。I
对应输入(Input),O
对应输出(Output)。
仓颉编程语言所有的 I/O 机制都是基于数据流进行输入输出,这些数据流表示了字节数据的序列
。数据流是一串连续的数据集合,它就像承载数据的管道,在管道的一端输入数据,在管道的另一端就可以输出数据。
仓颉编程语言将输入输出抽象为流(Stream):
- 将数据从外存中读取到内存中的称为输入流(InputStream),输入端可以一段一段地向管道中写入数据,这些数据段会按先后顺序形成一个长的数据流;
- 将数据从内存写入外存中的称为输出流(OutputStream),输出端也可以一段一段地从管道中读出数据,每次可以读取其中的任意长度的数据(不需要跟输入端匹配),但只能读取先输入的数据,再读取后输入的数据。
有了这一层抽象,仓颉编程语言就可以使用统一的接口来实现与外部数据的交互。
仓颉编程语言将标准输入输出
、文件操作
、网络数据流
、字符串流
、加密流
、压缩流
等等形式的操作,统一用 Stream 描述。
Stream 主要面向处理原始二进制数据
,Stream 中最小的数据单元是 Byte
。
仓颉编程语言将 Stream 定义成了 interface
,它让不同的 Stream 可以用装饰器模式进行组合
,极大地提升了可扩展性。
1.1 输入流
程序从输入流读取数据源
(数据源包括外界的键盘
、文件
、网络
…),即输入流是将数据源读入到程序的通信通道。
仓颉编程语言用 InputStream 接口类型
来表示输入流
,它提供了 read 函数
,这个函数会将可读的数据写入到 buffer 中
,返回值表示了该次读取的字节总数
。
InputStream 接口定义:
interface InputStream {
func read(buffer: Array<Byte>): Int64
}
当我们拥有一个输入流的时候,就可以像下面的代码那样去读取字节数据,读取的数据会被写到 read 的入参数组中。
输入流读取示例:
import std.io.InputStream
main() {
let input: InputStream = ...
let buf = Array<Byte>(256, item: 0)
while (input.read(buf) > 0) {
println(buf)
}
}
1.2 输出流
程序向输出流写入数据
。输出流是将程序中的数据输出到外界(显示器
、打印机
、文件
、网络
等)的通信通道。
仓颉编程语言用 OutputStream 接口类型
来表示输出流
,它提供了 write 函数
,这个函数会将 buffer 中的数据写入到绑定的流中
。
特别的,有一些输出流的 write 不会立即写到外存中
,而是有一定的缓冲策略,只有当符合条件
或主动调用 flush 时
才会真实写入,目的是提高性能
。
为了统一处理这些 flush 操作,在 OutputStream 中有一个 flush 的默认实现,它有助于抹平 API 调用的差异性。
OutputStream 接口定义:
interface OutputStream {
func write(buffer: Array<Byte>): Unit
func flush(): Unit {
// 空实现
}
}
当我们拥有一个输出流时,我们可以像下面的代码那样去写入字节数据。
输出流写入示例:
import std.io.OutputStream
main() {
let output: OutputStream = ...
let buf = Array<Byte>(256, item: 111)
output.write(buf)
output.flush()
}
1.3 数据流分类
按照数据流职责上的差异,我们可以给 Stream 简单分成两类:
- 节点流:直接提供数据源,节点流的构造方式通常是依赖某种直接的外部资源(即文件、网络等)。
- 处理流:只能代理其它数据流进行处理,处理流的构造方式通常是依赖其它的流。
2、I/O 节点流
节点流是指直接提供数据源的流
,节点流的构造方式通常是依赖某种直接的外部资源
(即文件、网络等)。
仓颉编程语言中常见的节点流包含标准流
(StdIn、StdOut、StdErr)、文件流
(File)、网络流
(Socket)等。
我们本章会着重介绍一下标准流和文件流。
2.1 标准流
标准流
包含了标准输入流
(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()
}
2.2 文件流
仓颉编程语言提供了 fs 包
来支持通用文件系统任务
。虽然不同的操作系统对于文件系统提供的接口有所不同,但是仓颉编程语言抽象出以下一些共通的功能
,通过统一的功能接口,屏蔽不同操作系统之间的差异
,来简化我们的使用。
这些常规操作任务包括:创建
文件/目录、读写文件
、重命名或移动
文件/目录、删除
文件/目录、复制
文件/目录、获取
文件/目录元数据、检查文件/目录是否存在
。具体 API 可以查阅库文档。
使用文件系统相关的功能需要导入 fs 包
:
导入 fs 包示例:
import std.fs.*
本章会着重介绍 File 相关的使用
,对于 Path
和 Directory
的使用可以查阅对应的 API 文档。
File 类型
在仓颉编程语言中同时提供了常规文件操作
和文件流
两类功能。
2.2.1 常规文件操作
对于常规的文件操作
,我们可以使用一系列静态函数
来完成快捷的操作。
例如我们如果要检查某个路径对应的文件是否存在,可以使用 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) // 把数据一次性写入另一个文件中
}
// 1. 将文件拷贝到新的位置,不成功则抛异常
// overwrite: Bool - 是否覆盖, 为 true 时覆盖,为false 时不覆盖。
public static func copy(sourcePath: Path, destinationPath: Path, overwrite: Bool): Unit
public static func copy(sourcePath: String, destinationPath: String, overwrite: Bool): Unit
// 2. 根据指定路径读取文件全部内容,以字节数组的形式返回其内容。
public static func readFrom(path: Path): Array<Byte>
public static func readFrom(path: String): Array<Byte>
// 3. 按照 openOption 打开指定路径的文件并将 buffer 写入。
public static func writeTo(path: Path, buffer: Array<Byte>, openOption!: OpenOption = CreateOrAppend): Unit
public static func writeTo(path: String, buffer: Array<Byte>, openOption!: OpenOption = CreateOrAppend): Unit
2.2.2 文件流操作
除了上述的常规文件操作之外,File 类型
也被设计为一种数据流类型
,因此 File 类型本身实现了 IOStream 接口
。当我们创建了一个 File 的实例,我们就可以把这个实例当成数据流来使用。
File 类定义:
public class File <: Resource & IOStream & Seekable {
public init(path: Path, openOption: OpenOption)
public init(path: String, openOption: OpenOption)
}
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")) {
...
// 结束使用后自动释放文件
}
OpenOption
public enum OpenOption {
| Append
| Create(Bool)
| CreateOrAppend
| CreateOrTruncate(Bool)
| Open(Bool, Bool)
| Truncate(Bool)
}
3、I/O 处理流
处理流是指代理其它数据流进行处理的流
。
仓颉编程语言中常见的处理流
包含 BufferedInputStream
、BufferedOutputStream
、StringReader
、StringWriter
、ChainedInputStream
等。
我们本章会着重介绍一下缓冲流和字符串流。
3.1 缓冲流
由于涉及磁盘的 I/O 操作相比内存的 I/O 操作要慢很多,所以对于高频次且小数据量的读写操作
来说,不带缓冲的数据流效率很低
,每次读取和写入数据都会带来大量的 I/O 耗时
。而带缓冲的数据流,可以多次读写数据,但不触发磁盘 I/O 操作,只是先放到内存里。等凑够了缓冲区大小的时候再一次性操作磁盘,这种方式可以显著减少磁盘操作次数,从而提升性能表现。
仓颉编程语言标准库提供了 BufferedInputStream
和 BufferedOutputStream
这两个类型用来提供缓冲功能。
使用 BufferedInputStream 和 BufferedOutputStream 类型需要导入 io 包
。
导入 io 包示例:
import std.io.*
BufferedInputStream 的作用是为另一个输入流添加缓冲的功能。本质上 BufferedInputStream 是通过一个内部缓冲数组实现的
。
当我们通过 BufferedInputStream 来读取流的数据时,BufferedInputStream 会一次性读取整个缓冲区大小的数据,然后我们再使用 read 函数就可以分多次读取更小规模的数据;当缓冲区中的数据被读完之后,输入流就会再次填充缓冲区;如此反复,直到我们读完数据流的所有数据。
构造一个 BufferedInputStream
很简单,我们只需要在构造函数中传入另一个输入流就
可以了。如果我们需要指定缓冲区的大小
,也可以额外传入 capacity 参数
进行指定。
BufferedInputStream 构造示例:
import std.io.*
main(): Unit {
let arr1 = "0123456789".toArray()
let byteArrayStream = ByteArrayStream()
byteArrayStream.write(arr1)
let bufferedInputStream = BufferedInputStream(byteArrayStream)
let arr2 = Array<Byte>(20, item: 0)
/* 读取流中数据,返回读取到的数据的长度 */
let readLen = bufferedInputStream.read(arr2)
println(String.fromUtf8(arr2[..readLen])) // 0123456789
}
public class BufferedInputStream<T> <: InputStream where T <: InputStream {
public init(input: T)
public init(input: T, buffer: Array<Byte>)
public init(input: T, capacity: Int64)
}
ByteArrayStream
// 基于 Array<Byte> 数据类型,提供对字节流的写入、读取等操作。
public class ByteArrayStream <: IOStream & Seekable {
// 创建 ByteArrayStream 实例,默认的初始容量是 32。
public init()
// 创建 ByteArrayStream 实例:capacity - 指定的初始容量。
public init(capacity: Int64)
// 通过 String 类型构造一个 ByteArrayStream。
public static func fromString(data: String): ByteArrayStream
// 获取当前 ByteArrayStream 中未被读取的数据的切片。
// 缓冲区进行读取,写入或重置等修改操作会导致这个切片失效。
// 对切片的修改会影响缓冲区的内容。
public func bytes(): Array<Byte>
// 获取当前缓冲区容量。
public func capacity(): Int64
// 清除当前 ByteArrayStream 中所有数据。
public func clear(): Unit
// 用当前 ByteArrayStream 中的数据来构造一个新的 ByteArrayStream
public func clone(): ByteArrayStream
// 将当前 ByteArrayStream 中未被读取的所有数据拷贝到 output 流中
public func copyTo(output: OutputStream): Unit
// 读取流中的剩余数据,并转换为 String 类型,做 UTF-8 编码检查。
public func readString(): String
// 读取流中的剩余数据,并转换为 String 类型,不做 UTF-8 编码检查。
public unsafe func readStringUnchecked(): String
// 获取当前 ByteArrayStream 中未被读取的数据。
public func readToEnd(): Array<Byte>
// 将缓冲区扩容指定大小。
// 当缓冲区剩余字节数大于等于 additional 时不发生扩容。
// 当缓冲区剩余字节数量小于 additional 时,取(additional + capacity)与(capacity的1.5倍向下取整)两个值中的最大值进行扩容。
public func reserve(additional: Int64): Unit
}
IOStream
public interface InputStream {
func read(buffer: Array<Byte>): Int64
}
public interface OutputStream {
func write(buffer: Array<Byte>): Unit
func flush(): Unit
}
public interface IOStream <: InputStream & OutputStream {}
// 移动光标接口
public interface Seekable {
prop length: Int64 // 返回当前流中的总数据量
prop position: Int64 // 返回当前光标位置
prop remainLength: Int64 // 返回当前流中未读的数据量
func seek(sp: SeekPosition): Int64 // 移动光标到指定的位置,
// 返回流中数据的起点到移动后位置的偏移量(以字节为单位)
}
public enum SeekPosition {
| Begin(Int64) // 表示从起点开始移动
| Current(Int64) // 表示从当前位置开始移动
| End(Int64) // 表示从末尾开始移动
}
BufferedOutputStream 的作用是为另一个输出流添加缓冲的功能。BufferedOutputStream 也是通过一个内部缓冲数组实现的
。
当我们通过 BufferedOutputStream 来向输出流写入数据时,write 的数据会先写入内部缓冲数组中;当缓冲区中的数据被填满之后,BufferedOutputStream 会将缓冲区的数据一次性写入输出流中,然后清空缓冲区再次被写入;如此反复,直到我们写完所有的数据。
需要注意的是,由于我们没写够缓冲区时不会触发输出流的写入操作
,所以当我们往 BufferedOutputStream 写完所有的数据
后,需要额外调用 flush 函数
来最终完成写入。
构造一个 BufferedOutputStream 也很简单
,我们只需要在构造函数中传入另一个输出流
就可以了。如果我们需要指定缓冲区的大小
,也可以额外传入 capacity 参数指定。
BufferedOutputStream 构造示例:
import std.io.*
main(): Unit {
let arr1 = "01234".toArray()
let byteArrayStream = ByteArrayStream()
byteArrayStream.write(arr1)
let bufferedOutputStream = BufferedOutputStream(byteArrayStream)
let arr2 = "56789".toArray()
/* 向流中写入数据,此时数据在外部流的缓冲区中 */
bufferedOutputStream.write(arr2)
/* 调用 flush 函数,真正将数据写入内部流中 */
bufferedOutputStream.flush()
println(String.fromUtf8(byteArrayStream.readToEnd())) // 0123456789
}
3.2 字符串流
由于仓颉编程语言的输入流和输出流是基于字节数据来抽象的(拥有更好的性能),在部分以字符串为主的场景中使用起来不太友好,例如往文件里写入大量的文本内容时,需要将文本内容转换成字节数据,再写入文件。
为了提供友好的字符串操作能力,仓颉编程语言提供了 StringReader
和 StringWriter
来添加字符串处理能力。
使用 StringReader 和 StringWriter 类型需要导入 io 包
:
导入 io 包示例:
import std.io.*
StringReader 提供了按行读
、按筛选条件读
的能力,相比将字节数据读出来再手动转换成字符串,具有更好的性能表现和易用性。
构造 StringReader 很简单,传入另一个输入流就可以了。
StringReader 使用示例:
import std.io.*
main(): Unit {
let arr1 = "012\n346789".toArray()
let byteArrayStream = ByteArrayStream()
byteArrayStream.write(arr1)
let stringReader = StringReader(byteArrayStream)
/* 读取一行数据 */
let line = stringReader.readln()
println(line ?? "error") // 012
}
StringWriter 提供了直接写字符串
、按行直接写字符串
的能力,相比将字节数据手动转换成字符串再写入,具有更好的性能表现和易用性。
构造 StringWriter 也很简单,传入另一个输出流
就可以了。
StringWriter 使用示例:
import std.io.*
main(): Unit {
let byteArrayStream = ByteArrayStream()
let stringWriter = StringWriter(byteArrayStream)
/* 写入字符串 */
stringWriter.write("number")
/* 写入字符串并自动转行 */
stringWriter.writeln(" is:")
/* 写入数字 */
stringWriter.write(100.0f32)
stringWriter.flush()
println(String.fromUtf8(byteArrayStream.readToEnd())) // number is:\n100.000000
}
StringReader<T>
// 提供从 InputStream 输入流中读出数据并转换成字符或字符串的能力。
// StringReader 内部默认有缓冲区,缓冲区容量 4096 个字节。
// StringReader 目前仅支持 UTF-8 编码,暂不支持 UTF-16、UTF-32。
public class StringReader<T> where T <: InputStream {
public init(input: T)
// 获得 StringReader 的行迭代器。相当于循环调用 func readln(),内部遇到非法字符时也会抛出异常。
//* 每行都由换行符进行分隔。
//* 换行符是 \n \r \r\n 之一。
//* 每行不包括换行符。
public func lines(): Iterator<String>
// 获得 StringReader 的 Rune 迭代器。
public func runes(): Iterator<Rune>
// 按字符读取流中的数据。
public func read(): ?Rune
// 从流内读取到使 predicate 返回 true 的字符位置(包含这个字符)或者流结束位置的数据。
public func readUntil(predicate: (Rune)->Bool): Option<String>
// 从流内读取到指定字符(包含指定字符)或者流结束位置的数据。
public func readUntil(v: Rune): Option<String>
// 按行读取流中的数据。
//* 读取的数据会去掉原换行符。
public func readln(): Option<String>
}
// 为 StringReader 实现 Resource 接口,
// 该类型对象可在 try-with-resource 语法上下文中实现自动资源释放。
extend<T> StringReader<T> <: Resource where T <: Resource
// 为 StringReader 实现 Seekable 接口,支持查询数据长度,移动光标等操作。
extend<T> StringReader<T> <: Seekable where T <: Seekable
StringWriter<T>
// 提供将 String 以及一些 ToString 类型转换成指定编码格式和字节序配置的字符串并写入到输出流的能力。
//* StringWriter 内部默认有缓冲区,缓冲区容量 4096 个字节。
//* StringWriter 目前仅支持 UTF-8 编码,暂不支持 UTF-16、UTF-32。
public class StringWriter<T> where T <: OutputStream {
public init(output: T)
// 写入 Bool 类型
public func write(v: Bool): Unit
// 写入 Bool 类型 + 换行符
public func writeln(v: Bool): Unit
// writeln 函数,同理
public func write(v: Float16): Unit
public func write(v: Float32): Unit
public func write(v: Float64): Unit
// writeln 函数,同理
public func write(v: Int16): Unit
public func write(v: Int32): Unit
public func write(v: Int64): Unit
public func write(v: Int8): Unit
public func write(v: UInt16): Unit
public func write(v: UInt32): Unit
public func write(v: UInt64): Unit
public func write(v: UInt8): Unit
// writeln 函数,同理
public func write(v: Rune): Unit
public func write(v: String): Unit
// 写入 ToString 类型
// writeln 函数,同理
public func write<T>(v: T): Unit where T <: ToString
// 写入换行符
public func writeln(): Unit
}
// 为 StringWriter 实现 Resource 接口,
// 该类型对象可在 try-with-resource 语法上下文中实现自动资源释放。
extend<T> StringWriter<T> <: Resource where T <: Resource
// 该接口用来提供具体类型的字符串表示
//* 在 core 包里
public interface ToString {
// 获取实例类型的字符串表示
func toString(): String
}
// 为 StringWriter 实现 Seekable 接口,支持查询数据长度,移动光标等操作。
extend<T> StringWriter<T> <: Seekable where T <: Seekable