Skip to content

LISP - 二进制 I/O

二进制 I/O 涉及逐字节地从数据源读取和写入数据。这对于处理图像、音频文件、网络数据包或与自定义二进制文件格式交互等任务至关重要。本章涵盖了 Common Lisp 中处理二进制数据的现代惯用方式。

在 Common Lisp 中,所有 I/O 操作都在流(streams)上进行。流是一个抽象,代表数据的来源或接收器。打开文件时,必须指定流将处理的数据类型。对于二进制 I/O,我们创建二进制流。

这可以通过向 open 函数或更常用的 with-open-file 宏提供 :element-type 参数来完成。二进制流的典型值是 '(unsigned-byte 8),这表示流将处理 8 位无符号整数(即单个字节)。

Common Lisp 提供了一组简洁的函数用于二进制流操作:

  • read-byte:从二进制输入流中读取单个字节。如果流已到达末尾,它会发出 end-of-file 错误。
  • read-sequence:将二进制流中的字节序列读入提供的数组(如字节向量)。这对于批量数据处理效率更高。
  • write-byte:将单个字节写入二进制输出流。
  • write-sequence:将字节序列(例如,来自字节向量)写入二进制输出流。这是写入数据块的首选方法。

最佳实践:使用 with-open-file 管理文件

Section titled “最佳实践:使用 with-open-file 管理文件”

始终使用 with-open-file 宏来处理文件。它确保文件流自动安全关闭,即使在其主体内发生错误也是如此。这可以防止资源泄漏。

语法是:(with-open-file (stream-variable path-to-file &key :direction :element-type ...))

此示例将两个字节(魔数 255 和版本号 1)写入文件。

;; `with-open-file` 宏确保流自动关闭。
(with-open-file (out "my-data.bin"
:direction :output
:element-type '(unsigned-byte 8)
:if-exists :supersede)
(write-byte 255 out) ; 写入一个“魔数”
(write-byte 1 out)) ; 写入一个“版本号”

在这里,我们读回这两个字节。请注意 read-byte 与可选参数的用法,以优雅地处理文件结束条件而不会引发错误。

(with-open-file (in "my-data.bin"
:direction :input
:element-type '(unsigned-byte 8))
(let ((magic-number (read-byte in nil :eof))
(version (read-byte in nil :eof)))
(unless (or (eq magic-number :eof) (eq version :eof))
(format t "Magic Number: ~d, Version: ~d~%" magic-number version))
;; 如果我们再次读取会发生什么?
(let ((next-byte (read-byte in nil :eof)))
(if (eq next-byte :eof)
(format t "End of file reached as expected.~%")
(format t "Unexpected data found: ~d~%" next-byte)))))
Magic Number: 255, Version: 1
End of file reached as expected.

示例 2:使用序列进行高效的批量 I/O

Section titled “示例 2:使用序列进行高效的批量 I/O”

逐字节地写入和读取对于大量数据来说效率低下。read-sequence 和 write-sequence 是完成此任务的正确工具。

;; 首先,创建一些要写入的数据。
(defvar *my-data* (make-array 4 :element-type '(unsigned-byte 8)
:initial-contents '(72 69 76 79))) ; "HELO"
;; 将整个序列写入文件
(with-open-file (out "message.dat"
:direction :output
:element-type '(unsigned-byte 8)
:if-exists :supersede)
(write-sequence *my-data* out))
;; 现在,读回数据
(with-open-file (in "message.dat"
:direction :input
:element-type '(unsigned-byte 8))
;; 创建一个缓冲区来保存传入的数据。
(let ((buffer (make-array 4 :element-type '(unsigned-byte 8))))
(read-sequence buffer in)
(format t "Read data: ~a~%" buffer)))
Read data: #(72 69 76 79)
  • 字节序(Endianness):Common Lisp 本身不规定字节序。在编写可移植的二进制文件(例如,网络协议)时,如果处理多字节整数,则必须手动管理字节顺序。像 babel 这样的库可以帮助解决这个问题。
  • 错误处理:(read-byte stream nil :eof) 模式对于检测文件末尾而不触发条件至关重要。第二个参数 (nil) 表示“不发出错误”,第三个参数 (:eof) 是代替错误返回的值。
  • 效率:对于任何大于几个字节的数据,始终优先使用 read-sequence 和 write-sequence,而不是在循环中使用 read-byte 和 write-byte。
  • 资源管理:始终使用 with-open-file。这是 Lisp 中安全文件处理最重要的一条实践。