重学仓颉-14I/O 系统完全指南

引言

在编程世界中,I/O(输入/输出)操作是程序与外部世界交互的桥梁。仓颉编程语言采用了基于流的 I/O 设计理念,将所有的输入输出操作统一抽象为数据流(Stream)的概念。这种设计不仅简化了 API 的使用,还通过装饰器模式提供了强大的扩展能力。

1. I/O 流基础概念

1.1 什么是 I/O 流

仓颉语言将 I/O 操作抽象为数据流的概念。数据流就像承载数据的管道,在管道的一端输入数据,在另一端就可以输出数据。这种抽象使得程序可以用统一的接口来处理各种类型的外部数据交互。

核心特点:

  • 基于字节数据:最小的数据单元是 Byte
  • 统一接口:所有 I/O 操作都通过 Stream 接口
  • 装饰器模式:支持流的组合和扩展

1.2 输入流(InputStream)

输入流负责将外部数据源(键盘、文件、网络等)的数据读入程序。

cangjie 复制代码
// InputStream 接口定义
interface InputStream {
    func read(buffer: Array<Byte>): Int64
}

接口说明:

  • read 函数将可读数据写入指定的 buffer 中
  • 返回值表示本次读取的字节总数
  • 当返回 0 或负数时,通常表示已到达流末尾或发生错误

使用示例:

cangjie 复制代码
package cangjie_blog

import std.io.{InputStream, BufferedInputStream, ByteBuffer}

func getSomeInputStream(): InputStream {
    let inputStream = ByteBuffer()
    let buffer = Array<Byte>(1024, repeat: 0)
    let bufferedStream = BufferedInputStream(inputStream, buffer)
    return bufferedStream
}

main() {
    // 假设我们有一个输入流
    let input: InputStream = getSomeInputStream()
    // 创建缓冲区
    let buf = Array<Byte>(256, repeat: 0)
    // 循环读取数据
    let bytesRead = input.read(buf)
    if (bytesRead > 0) {
        // 处理读取到的数据
        println("读取到 ${bytesRead} 字节数据")
        println(buf[..bytesRead])
    }
}

1.3 输出流(OutputStream)

输出流负责将程序中的数据输出到外部目标(显示器、文件、网络等)。

cangjie 复制代码
// OutputStream 接口定义
interface OutputStream {
    func write(buffer: Array<Byte>): Unit

    func flush(): Unit {
        // 默认空实现,子类可以重写
    }
}

接口说明:

  • write 函数将 buffer 中的数据写入流
  • flush 函数确保数据被立即写入目标,避免缓冲延迟
  • 某些输出流使用缓冲策略提高性能,需要手动调用 flush

使用示例:

cangjie 复制代码
package cangjie_blog

import std.io.{OutputStream, BufferedOutputStream, ByteBuffer}

func getSomeOutputStream(): OutputStream {
    let byteBuffer = ByteBuffer("01234".toArray())
    let bufferedOutputStream = BufferedOutputStream(byteBuffer)
    return bufferedOutputStream
}

main() {
    // 假设我们有一个输出流
    let output: OutputStream = getSomeOutputStream()

    // 准备要写入的数据
    let data = "Hello, Cangjie!".toArray()

    // 写入数据
    output.write(data)

    // 确保数据被立即写入
    output.flush()
}

1.4 数据流分类

仓颉语言的 Stream 按职责分为两类:

  1. 节点流:直接提供数据源,依赖外部资源(文件、网络等)
  2. 处理流:代理其他数据流进行处理,依赖其他流

这种分类为流的组合使用提供了理论基础。

2. 节点流详解

2.1 标准流

标准流是程序与外部交互的标准接口,包括标准输入、标准输出和标准错误输出。

2.1.1 标准输入流

cangjie 复制代码
package cangjie_blog

import std.env.getStdIn

main(): Unit {
    // 获取标准输入流
    let consoleReader = getStdIn()
    // 读取一行输入
    let input = consoleReader.readln()
    // 处理输入(注意 readln 返回 Option<String>)
    match (input) {
        case Some(text) => println("您输入了: ${text}")
        case None => println("没有读取到输入")
    }
}

关键点:

  • getStdIn() 返回 ConsoleReader 类型
  • readln() 返回 Option<String>,需要处理 None 情况
  • ConsoleReader 提供并发安全保证

2.1.2 标准输出流

cangjie 复制代码
package cangjie_blog

import std.env.getStdOut

main() {
    // 获取标准输出流
    let consoleWriter = getStdOut()

    // 写入单行文本
    consoleWriter.write("这是第一行")

    // 写入文本并换行
    consoleWriter.writeln("这是第二行")

    // 写入数字
    consoleWriter.write(42)
    consoleWriter.writeln("")

    // 确保内容被输出
    consoleWriter.flush()
}

性能优势:

  • 相比直接使用 print 函数,ConsoleWriter 使用缓存技术
  • 在输出大量内容时性能更好
  • 并发安全,可在多线程环境中使用

2.1.3 标准错误流

cangjie 复制代码
package cangjie_blog

import std.env.getStdErr

main() {
    // 获取标准错误流
    let errorWriter = getStdErr()

    // 输出错误信息
    errorWriter.writeln("这是一个错误信息")
    errorWriter.writeln("程序执行过程中出现了问题")

    // 确保错误信息被输出
    errorWriter.flush()
}

2.2 文件流

文件流是处理文件 I/O 的核心类型,仓颉语言通过 fs 包提供文件系统支持。

2.2.1 导入必要的包

cangjie 复制代码
import std.fs.*
import std.io.*

2.2.2 常规文件操作

检查文件是否存在:

cangjie 复制代码
package cangjie_blog

import std.fs.exists
import std.env.getWorkingDirectory

main() {
    let workingDirectory = getWorkingDirectory().toString()
    let filePath = workingDirectory + "/example.txt"

    if (exists(filePath)) {
        println("文件 ${filePath} 存在")
    } else {
        println("文件 ${filePath} 不存在")
    }
}

文件移动、复制、删除:

cangjie 复制代码
package cangjie_blog

import std.fs.*
import std.env.getWorkingDirectory

main() {
    let workingDirectory = getWorkingDirectory().toString()
    let sourceFile = workingDirectory + "/source.txt"
    let targetFile = workingDirectory + "/target.txt"
    let backupFile = workingDirectory + "/backup.txt"

    // 复制文件
    copy(sourceFile, to: targetFile, overwrite: false)

    // 重命名文件
    rename(targetFile, to: backupFile, overwrite: false)

    // 删除文件
    remove(backupFile)

    println("文件操作完成")
}

一次性读写文件:

cangjie 复制代码
package cangjie_blog

import std.fs.File
import std.env.getWorkingDirectory

main() {
    let workingDirectory = getWorkingDirectory().toString()
    let inputFile = workingDirectory + "/input.txt"
    let outputFile = workingDirectory + "/output.txt"

    // 一次性读取所有数据
    let content = File.readFrom(inputFile)
    println("读取到 ${content.size} 字节数据")

    // 一次性写入所有数据
    File.writeTo(outputFile, content)
    println("数据已写入 ${outputFile}")
}

2.2.3 文件流操作

File 类的特性:

cangjie 复制代码
// File 类实现了多个接口
public class File <: Resource & IOStream & Seekable {
    // 文件操作的具体实现
}

创建新文件:

cangjie 复制代码
package cangjie_blog

import std.fs.File
import std.env.getWorkingDirectory

main() {
    let workingDirectory = getWorkingDirectory().toString()
    // 创建新文件(只写模式)
    let newFile = File.create(workingDirectory + "/newfile.txt")

    // 写入数据
    let data = "Hello, Cangjie File System!".toArray()
    newFile.write(data)

    // 关闭文件
    newFile.close()
}

打开现有文件:

cangjie 复制代码
package cangjie_blog

import std.fs.{File, OpenMode}
import std.env.getWorkingDirectory

main() {
    let workingDirectory = getWorkingDirectory().toString()
    // 以读取模式打开文件
    let readFile = File(workingDirectory + "/example.txt", Read)
    // 读取数据
    let buffer = Array<Byte>(1024, repeat: 0)
    let bytesRead = readFile.read(buffer)

    if (bytesRead > 0) {
        let content = String.fromUtf8(buffer[..bytesRead])
        println("文件内容:\n${content}")
    }
    // 关闭文件
    readFile.close()
}

文件打开模式:

cangjie 复制代码
package cangjie_blog

import std.fs.{File, OpenMode}
import std.env.getWorkingDirectory

main() {
    let workingDirectory = getWorkingDirectory().toString()
    let filePath = workingDirectory + "/example.txt"

    // 不同的打开模式
    let readFile = File(filePath, OpenMode.Read) // 只读
    let writeFile = File(filePath, OpenMode.Write) // 只写
    let appendFile = File(filePath, OpenMode.Append) // 追加
    let rwFile = File(filePath, OpenMode.ReadWrite) // 读写

    // 使用完后关闭
    readFile.close()
    writeFile.close()
    appendFile.close()
    rwFile.close()
}

使用 try-with-resource 语法:

cangjie 复制代码
package cangjie_blog

import std.fs.{File, OpenMode}
import std.env.getWorkingDirectory

main() {
    let workingDirectory = getWorkingDirectory().toString()
    // 自动资源管理,文件会在使用完后自动关闭
    try (file = File(workingDirectory + "/example.txt", OpenMode.Read)) {
        let buffer = Array<Byte>(1024, repeat: 0)
        let bytesRead = file.read(buffer)

        if (bytesRead > 0) {
            println("读取到 ${bytesRead} 字节")
        }
    } // 文件在这里自动关闭

    println("文件已自动关闭")
}

3. 处理流详解

3.1 缓冲流

缓冲流通过内部缓冲区减少实际的 I/O 操作次数,显著提升性能。

3.1.1 缓冲输入流(BufferedInputStream)

cangjie 复制代码
package cangjie_blog

import std.io.{ByteBuffer, BufferedInputStream}

main(): Unit {
    // 创建源数据
    let sourceData = "0123456789".toArray()
    let sourceStream = ByteBuffer()
    sourceStream.write(sourceData)
    // 创建缓冲输入流
    let bufferedInput = BufferedInputStream(sourceStream, 4)
    // 创建读取缓冲区
    let readBuffer = Array<Byte>(20, repeat: 0)
    // 读取数据
    let bytesRead = bufferedInput.read(readBuffer)
    if (bytesRead > 0) {
        let content = String.fromUtf8(readBuffer[..bytesRead])
        println("读取到的内容: ${content}")
    }
}

工作原理:

  • 一次性读取整个缓冲区大小的数据到内部缓冲区
  • 后续的 read 操作从内部缓冲区读取,避免频繁的底层 I/O
  • 当内部缓冲区为空时,自动重新填充

3.1.2 缓冲输出流(BufferedOutputStream)

cangjie 复制代码
package cangjie_blog
import std.io.{ByteBuffer, BufferedOutputStream, readToEnd}
main() {
    // 创建目标流
    let targetStream = ByteBuffer()
    // 创建缓冲输出流,缓冲区大小为 4 字节
    let bufferedOutput = BufferedOutputStream(targetStream, 4)
    // 写入数据
    let data1 = "Hello".toArray()
    let data2 = "World".toArray()
    bufferedOutput.write(data1)
    bufferedOutput.write(data2)
    // 重要:必须调用 flush 确保数据被写入
    bufferedOutput.flush()
    // 验证结果
    let result = readToEnd(targetStream)
    let content = String.fromUtf8(result)
    println("最终内容: ${content}") // 输出: HelloWorld
}

关键注意点:

  • 数据先写入内部缓冲区,不会立即触发底层 I/O
  • 必须调用 flush() 确保数据被完全写入
  • 缓冲区大小影响性能和内存使用

3.2 字符串流

字符串流提供了基于字符串的 I/O 操作,比字节流更易用。

3.2.1 字符串读取器(StringReader)

cangjie 复制代码
package cangjie_blog

import std.io.{ByteBuffer, StringReader}

main() {
    // 创建包含多行文本的源数据
    let sourceText = "第一行内容\n第二行内容\n第三行内容".toArray()
    let sourceStream = ByteBuffer()
    sourceStream.write(sourceText)
    // 创建字符串读取器
    let stringReader = StringReader(sourceStream)
    // 逐行读取
    let line1 = stringReader.readln()
    let line2 = stringReader.readln()
    let line3 = stringReader.readln()

    // 处理读取结果
    match ((line1, line2, line3)) {
        case (Some(l1), Some(l2), Some(l3)) =>
            println("第一行: ${l1}")
            println("第二行: ${l2}")
            println("第三行: ${l3}")
        case _ => println("读取行数不足")
    }
}

优势:

  • 自动处理字符串编码转换
  • 提供按行读取的便利方法
  • 性能优于手动字节转换

3.2.2 字符串写入器(StringWriter)

cangjie 复制代码
package cangjie_blog

import std.io.{ByteBuffer, StringWriter, readToEnd}

main() {
    // 创建目标流
    let targetStream = ByteBuffer()
    // 创建字符串写入器
    let stringWriter = StringWriter(targetStream)
    // 写入不同类型的数据
    stringWriter.write("用户ID: ")
    stringWriter.writeln(12345)
    stringWriter.write("用户名: ")
    stringWriter.writeln("张三")
    stringWriter.write("年龄: ")
    stringWriter.write(25)
    stringWriter.writeln(" 岁")
    // 确保数据被写入
    stringWriter.flush()
    // 验证结果
    let result = readToEnd(targetStream)
    let content = String.fromUtf8(result)
    println("写入的内容:")
    println(content)
}

特性:

  • 支持多种数据类型的自动转换
  • writeln 自动添加换行符
  • 提供便捷的字符串操作接口

4. 实际应用场景

4.1 文件复制工具

cangjie 复制代码
package cangjie_blog

import std.fs.{File, OpenMode, exists, removeIfExists}
import std.io.{BufferedInputStream, BufferedOutputStream}
import std.env.getWorkingDirectory

func copyFile(sourcePath: String, targetPath: String): Bool {
    try (sourceFile = File(sourcePath, OpenMode.Read)) {
        removeIfExists(targetPath)
        try (targetFile = File.create(targetPath)) {
            // 使用缓冲流提高性能
            let bufferedSource = BufferedInputStream(sourceFile, 8192)
            let bufferedTarget = BufferedOutputStream(targetFile, 8192)

            let buffer = Array<Byte>(4096, repeat: 0)
            var totalBytes = 0

            // 循环读写
            while (true) {
                let bytesRead = bufferedSource.read(buffer)
                if (bytesRead <= 0) {
                    break
                }

                bufferedTarget.write(buffer[..bytesRead])
                totalBytes += bytesRead
            }

            // 确保所有数据被写入
            bufferedTarget.flush()

            println("文件复制完成,共复制 ${totalBytes} 字节")
            return true
        }
    }

    return false
}

main() {
    let workingDirectory = getWorkingDirectory().toString()
    let success = copyFile(workingDirectory + "/source.txt", workingDirectory + "/target.txt")
    if (success) {
        println("文件复制成功")
    } else {
        println("文件复制失败")
    }
}

4.2 日志记录器

cangjie 复制代码
package cangjie_blog

import std.fs.{File, OpenMode}
import std.env.{ConsoleWriter, getStdOut, getWorkingDirectory}
import std.time.DateTime

class Logger {
    private let file: File
    private let console: ConsoleWriter

    init(logFilePath: String) {
        this.file = File(logFilePath, OpenMode.Append)
        this.console = getStdOut()
    }

    func log(level: String, message: String) {
        let timestamp = DateTime.now()
        let logEntry = "[${timestamp}] [${level}] ${message}\n"

        // 写入文件
        file.write(logEntry.toArray())
        file.flush()

        // 同时输出到控制台
        console.write(logEntry)
        console.flush()
    }

    func info(message: String) {
        log("INFO", message)
    }

    func error(message: String) {
        log("ERROR", message)
    }

    func close() {
        file.close()
    }
}

main() {
    let workingDirectory = getWorkingDirectory().toString()
    let logger = Logger(workingDirectory + "/app.log")
    logger.info("应用程序启动")
    logger.info("开始处理用户请求")
    logger.error("发生了一个错误")
    logger.info("应用程序关闭")
    logger.close()
}

4.3 配置文件读取器

cangjie 复制代码
package cangjie_blog

import std.fs.{exists, File, OpenMode}
import std.io.StringReader
import std.collection.HashMap
import std.env.getWorkingDirectory
import std.unicode.UnicodeStringExtension

class ConfigReader {
    private let configPath: String

    init(configPath: String) {
        this.configPath = configPath
    }

    func readConfig(): HashMap<String, String> {
        let config = HashMap<String, String>()
        if (!exists(configPath)) {
            println("配置文件不存在: ${configPath}")
            return config
        }
        try (file = File(configPath, OpenMode.Read)) {
            let stringReader = StringReader(file)
            // 逐行读取配置
            while (true) {
                let line = stringReader.readln()
                match (line) {
                    case Some(text) =>
                        // 跳过空行和注释行
                        let trimmed = text.trim()
                        if (!trimmed.isEmpty() && !trimmed.startsWith("#")) {
                            // 解析 key=value 格式
                            let parts = trimmed.split("=")
                            if (parts.size == 2) {
                                let key = parts[0].trim()
                                let value = parts[1].trim()
                                config.add(key, value)
                            }
                        }
                    case None => break
                }
            }
        }

        return config
    }
}

main() {
    let workingDirectory = getWorkingDirectory().toString()
    let configReader = ConfigReader(workingDirectory + "/config.ini")
    let config = configReader.readConfig()

    println("配置内容:")
    for ((key, value) in config) {
        println("${key} = ${value}")
    }
}

5. 性能优化建议

5.1 选择合适的缓冲区大小

cangjie 复制代码
// 小文件使用较小的缓冲区
let smallBuffer = Array<Byte>(1024, repeat: 0)

// 大文件使用较大的缓冲区
let largeBuffer = Array<Byte>(8192, repeat: 0)

// 网络传输使用中等缓冲区
let networkBuffer = Array<Byte>(4096, repeat: 0)

5.2 合理使用缓冲流

cangjie 复制代码
package cangjie_blog

import std.fs.File
import std.io.{BufferedInputStream, BufferedOutputStream, ByteBuffer}
import std.env.getWorkingDirectory

main() {
    let sourceStream = ByteBuffer()
    let targetStream = ByteBuffer()
    let workingDirectory = getWorkingDirectory().toString()
    // 对于频繁的小量 I/O 操作,使用缓冲流
    let bufferedInput = BufferedInputStream(sourceStream, 8192)
    let bufferedOutput = BufferedOutputStream(targetStream, 8192)

    // 对于一次性大量 I/O 操作,直接使用原始流
    let content = File.readFrom(workingDirectory + "/largefile.txt")
}

5.3 及时释放资源

cangjie 复制代码
// 使用 try-with-resource 自动管理资源
try (file = File("./example.txt", Read)) {
    // 文件操作
} // 自动关闭

// 或者手动关闭
let file = File("./example.txt", Read)
// ... 文件操作
file.close()

6. 常见问题和解决方案

6.1 文件编码问题

cangjie 复制代码
import std.fs.{File, OpenMode}
import std.io.readToEnd

extend String {
    public static func fromUtf16(utf16Data: Array<UInt8>): String {
        println(utf16Data)
        return "待实现的方法,从utf16Data创建字符串"
    }

    public static func fromAscii(asciiData: Array<UInt8>): String {
        println(asciiData)
        return "待实现的方法,从asciiData创建字符串"
    }
}

// 处理不同编码的文件
func readFileWithEncoding(filePath: String, encoding: String): String {
    try (file = File(filePath, Read)) {
        let bytes = readToEnd(file)
        match (encoding) {
            case "UTF-8" => return String.fromUtf8(bytes)
            case "UTF-16" => return String.fromUtf16(bytes)
            case "ASCII" => return String.fromAscii(bytes)
            case _ => return String.fromUtf8(bytes) // 默认使用 UTF-8
        }
    }

    return ""
}

6.2 大文件处理

cangjie 复制代码
import std.fs.{File, OpenMode}
// 分块处理大文件
func processLargeFile(filePath: String, chunkSize: Int64) {
    // 使用嵌套函数封装块处理逻辑
    func processChunk(chunk: Array<Byte>, chunkNumber: Int64) {
        println("处理第 ${chunkNumber} 块,大小: ${chunk.size} 字节")
        // 具体的块处理逻辑
    }

    try (file = File(filePath, OpenMode.Read)) {
        let buffer = Array<Byte>(chunkSize, repeat: 0)
        var chunkNumber = 1

        while (true) {
            let bytesRead = file.read(buffer)
            if (bytesRead <= 0) {
                break
            }

            // 处理当前块
            processChunk(buffer[..bytesRead], chunkNumber)
            chunkNumber += 1
        }
    }
}

7. 总结

仓颉语言的 I/O 系统设计体现了以下几个重要特点:

7.1 设计优势

  1. 统一抽象:所有 I/O 操作都通过 Stream 接口,简化了 API 设计
  2. 装饰器模式:支持流的组合使用,提供了强大的扩展能力
  3. 性能优化:通过缓冲流等机制,在易用性和性能之间找到平衡
  4. 类型安全:充分利用仓颉语言的类型系统,提供编译时检查

7.2 使用建议

  1. 选择合适的流类型:根据具体需求选择节点流或处理流
  2. 合理使用缓冲:对于频繁的小量 I/O 操作,使用缓冲流提升性能
  3. 及时释放资源:使用 try-with-resource 语法自动管理资源
  4. 处理异常情况:考虑文件不存在、权限不足等异常情况

7.3 扩展思考

仓颉语言的 I/O 系统为未来的扩展留下了充足的空间:

  • 可以轻松添加新的节点流类型(如数据库流、压缩流)
  • 可以创建新的处理流(如加密流、压缩流、转换流)
  • 通过接口组合,可以实现复杂的 I/O 处理管道

参考资料

相关推荐
森之鸟7 小时前
开发中使用——鸿蒙CoreSpeechKit语音识别
华为·语音识别·harmonyos
爱笑的眼睛118 小时前
HarmonyOS 应用开发:基于API 12+的现代开发实践
华为·harmonyos
安卓开发者9 小时前
鸿蒙NEXT表单选择组件详解:Radio与Checkbox的使用指南
华为·harmonyos
爱笑的眼睛119 小时前
HarmonyOS 应用开发深度实践:深入 Stage 模型与 ArkTS 声明式 UI
华为·harmonyos
爱笑的眼睛119 小时前
HarmonyOS应用开发深度解析:基于Stage模型与ArkTS的现代实践
华为·harmonyos
HarderCoder9 小时前
重学仓颉-13并发编程完全指南
harmonyos
开发小能手嗨啊9 小时前
「鸿蒙系统的编程基础」——探索鸿蒙开发
harmonyos·鸿蒙·鸿蒙开发·开发教程·纯血鸿蒙·南向开发·北向开发
HarderCoder9 小时前
重学仓颉-12错误处理完全指南
harmonyos
Georgewu19 小时前
【 HarmonyOS 】错误描述:The certificate has expired! 鸿蒙证书过期如何解决?
harmonyos