仓颉之基础I/O操作 2024-09-04 周三

I/O 流概述

  • 在仓颉编程语言中,我们将与应用程序外部载体交互的操作称之为 I/O 操作。I 对应输入(Input),O 对应输出(Output)。

  • 仓颉编程语言所有的 I/O 机制都是基于数据流进行输入输出,这些数据流表示了字节数据的序列。数据流是一串连续的数据集合,它就像承载数据的管道,在管道的一端输入数据,在管道的另一端就可以输出数据。

  • 仓颉编程语言将输入输出抽象为流(Stream)。

  1. 将数据从外存中读取到内存中的称为输入流(InputStream),输入端可以一段一段地向管道中写入数据,这些数据段会按先后顺序形成一个长的数据流。

  2. 将数据从内存写入外存中的称为输出流(OutputStream),输出端也可以一段一段地从管道中读出数据,每次可以读取其中的任意长度的数据(不需要跟输入端匹配),但只能读取先输入的数据,再读取后输入的数据。

  • 仓颉编程语言将标准输入输出、文件操作、网络数据流、字符串流、加密流、压缩流等等形式的操作,统一用 Stream 描述。

  • Stream 主要面向处理原始二进制数据,Stream 中最小的数据单元是 Byte。

  • 仓颉编程语言将 Stream 定义成了 interface,它让不同的 Stream 可以用装饰器模式进行组合,极大地提升了可扩展性。

输入流

  • 程序从输入流读取数据源(数据源包括外界的键盘、文件、网络等),即输入流是将数据源读入到程序的通信通道。

  • 仓颉编程语言用 InputStream 接口类型来表示输入流,它提供了 read 函数,这个函数会将可读的数据写入到 buffer 中,返回值表示了该次读取的字节总数。

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)
    }
}

输出流

  • 程序向输出流写入数据。输出流是将程序中的数据输出到外界(显示器、打印机、文件、网络等)的通信通道。

  • 仓颉编程语言用 OutputStream 接口类型来表示输出流,它提供了 write 函数,这个函数会将 buffer 中的数据写入到绑定的流中。

  • 特别的,有一些输出流的 write 不会立即写到外存中,而是有一定的缓冲策略,只有当符合条件或主动调用 flush 时才会真实写入,目的是提高性能。

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()
}

数据流分类

按照数据流职责上的差异,我们可以给 Stream 简单分成两类:

  1. 节点流:直接提供数据源,节点流的构造方式通常是依赖某种直接的外部资源(即文件、网络等)。

  2. 处理流:只能代理其它数据流进行处理,处理流的构造方式通常是依赖其它的流。

I/O 节点流

  • 节点流是指直接提供数据源的流,节点流的构造方式通常是依赖某种直接的外部资源(即文件、网络等)。

  • 仓颉编程语言中常见的节点流包含标准流(StdIn、StdOut、StdErr)、文件流(File)、网络流(Socket)等。

标准流

  • 标准流包含了标准输入流(stdin)、标准输出流(stdout)和标准错误输出流(stderr)。

  • 标准流是我们的程序与外部数据交互的标准接口。程序运行的时候从输入流读取数据,作为程序的输入,程序运行过程中输出的信息被传送到输出流,类似的,错误信息被传送到错误流。

  • 在仓颉编程语言中我们可以使用 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 包:

import std.fs.*

常规文件操作

  • 对于常规的文件操作,我们可以使用一系列静态函数来完成快捷的操作。

  • 例如我们如果要检查某个路径对应的文件是否存在,可以使用 exists 函数。当 exists 函数返回 true 时表示文件存在,反之不存在。

import std.fs.*

main() {
    let exist = File.exists("./tempFile.txt")
    println("exist: ${exist}")
}
  • 移动文件、拷贝文件和删除文件也非常简单,File 同样提供了对应的静态函数 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 函数直接读写文件。在数据量较少的情况下它们既简单易用又能提供较好的性能表现,让我们不需要手动处理数据流的事情。
import std.fs.*

main() {
    let bytes = File.readFrom("./tempFile.txt") // 一次性读取了所有的数据
    File.writeTo("./otherFile.txt", bytes) // 把数据一次性写入另一个文件中
}

文件流操作

除了上述的常规文件操作之外,File 类型也被设计为一种数据流类型,因此 File 类型本身实现了 IOStream 接口。当我们创建了一个 File 的实例,我们就可以把这个实例当成数据流来使用。

public class File <: Resource & IOStream & Seekable {
    ...
}
  • File 提供了两种构造方式,一种是通过两个方便的静态函数 openRead/create 直接打开文件或创建新文件的实例,另一种是通过构造函数传入完整的打开文件选项来构造新实例。
// 创建
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 以及其它便捷的复合操作。
// 使用指定选项打开文件
let file = File("./tempFile.txt", OpenOption.Truncate(false))
...
  • File 实现了 Resource 接口,我们在大多数时候都可以使用 try-with-resource 语法来简化我们的使用。
try (file2 = File.openRead("./tempFile.txt")) {
    ...
    // 结束使用后自动释放文件
}

try和{}之间加一个()的try-with-resource是特有的,其他语言还没有,可以省去关闭文件的操作

I/O 处理流

  • 处理流是指代理其它数据流进行处理的流。

  • 仓颉编程语言中常见的处理流包含 BufferedInputStream、BufferedOutputStream、StringReader、StringWriter、ChainedInputStream 等。

缓冲流

由于涉及磁盘的 I/O 操作相比内存的 I/O 操作要慢很多,所以对于高频次且小数据量的读写操作来说,不带缓冲的数据流效率很低,每次读取和写入数据都会带来大量的 I/O 耗时。而带缓冲的数据流,可以多次读写数据,但不触发磁盘 I/O 操作,只是先放到内存里。等凑够了缓冲区大小的时候再一次性操作磁盘,这种方式可以显著减少磁盘操作次数,从而提升性能表现。

  • 仓颉编程语言标准库提供了 BufferedInputStream 和 BufferedOutputStream 这两个类型用来提供缓冲功能。

  • 使用 BufferedInputStream 和 BufferedOutputStream 类型需要导入 io 包。

import std.io.*
  • 当我们通过 BufferedInputStream 来读取流的数据时,BufferedInputStream 会一次性读取整个缓冲区大小的数据,然后我们再使用 read 函数就可以分多次读取更小规模的数据;当缓冲区中的数据被读完之后,输入流就会再次填充缓冲区;如此反复,直到我们读完数据流的所有数据。

  • 构造一个 BufferedInputStream 很简单,我们只需要在构造函数中传入另一个输入流就可以了。如果我们需要指定缓冲区的大小,也可以额外传入 capacity 参数进行指定。

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
}
  • BufferedOutputStream 的作用是为另一个输出流添加缓冲的功能。BufferedOutputStream 也是通过一个内部缓冲数组实现的。

  • 当我们通过 BufferedOutputStream 来向输出流写入数据时,write 的数据会先写入内部缓冲数组中;当缓冲区中的数据被填满之后,BufferedOutputStream 会将缓冲区的数据一次性写入输出流中,然后清空缓冲区再次被写入;如此反复,直到我们写完所有的数据。

  • 需要注意的是,由于我们没写够缓冲区时不会触发输出流的写入操作,所以当我们往 BufferedOutputStream 写完所有的数据后,需要额外调用 flush 函数来最终完成写入。

  • 构造一个 BufferedOutputStream 也很简单,我们只需要在构造函数中传入另一个输出流就可以了。如果我们需要指定缓冲区的大小,也可以额外传入 capacity 参数指定。

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
}

字符串流

由于仓颉编程语言的输入流和输出流是基于字节数据来抽象的(拥有更好的性能),在部分以字符串为主的场景中使用起来不太友好,例如往文件里写入大量的文本内容时,需要将文本内容转换成字节数据,再写入文件。

  • 为了提供友好的字符串操作能力,仓颉编程语言提供了 StringReader 和 StringWriter 来添加字符串处理能力。

  • 使用 StringReader 和 StringWriter 类型需要导入 io 包:

import std.io.*
  • 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 也很简单,传入另一个输出流就可以了。

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
}
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,088评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,715评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,361评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,099评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 60,987评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,063评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,486评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,175评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,440评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,518评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,305评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,190评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,550评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,880评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,152评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,451评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,637评论 2 335

推荐阅读更多精彩内容