Skip to content

LISP - 文件 I/O

Common Lisp 中的文件输入/输出(I/O)是通过一种称为流(streams)的抽象来处理的。流是数据的来源或目的地,它可以是文件、终端、网络连接,甚至是字符串。用于读写数据的相同函数,如 format 和 read-line,可以作用于任何类型的流。

本章涵盖了在 Common Lisp 中执行文件操作的现代、安全且地道(idiomatic)的方式。

虽然 Lisp 提供了低级的 open 函数,但推荐的最佳实践是始终使用 with-open-file 宏(macro)。它自动化了关闭文件的关键步骤,即使在处理过程中发生错误也能确保关闭。这可以防止资源泄漏(resource leaks),是健壮编程(robust programming)的核心原则。

with-open-file 的语法是:

(with-open-file (stream-variable filename-or-path &key options...)
;; 在这里编写从流变量读取或写入的代码
...)
;; 文件在此处自动关闭

with-open-file 的关键选项包括:

  • :direction:指定流的用途。常见值有: * :input(默认值):用于从文件读取。 * :output:用于写入文件。 * :io:用于同时读写。
  • :if-exists:当 :direction 为 :output 且文件已存在时如何处理。 * :supersede(默认值):覆盖现有文件。 * :append:将新内容添加到文件末尾。 * :error:发出错误信号。
  • :if-does-not-exist:当文件不存在时如何处理。 * :create(:output 的默认值):创建一个新文件。 * :error(:input 的默认值):发出错误信号。 * nil:从 with-open-file 返回 nil 而不是发出错误信号。
  • :element-type:正在传输的数据类型。对于文本文件,默认为 character。对于二进制文件(binary files),使用 (unsigned-byte 8)。
  • :external-format:字符编码(character encoding)。明确指定此项是良好的实践。现代标准是 :utf-8。

此示例将多行文本写入名为 notes.txt 的文件。如果文件存在,它将被覆盖。

(defun write-notes ()
(with-open-file (stream "notes.txt"
:direction :output
:if-exists :supersede
:external-format :utf-8)
(format stream "Welcome to a modern Lisp tutorial!~%")
(format stream "File I/O is managed safely with with-open-file.~%")
(format stream "The file stream is automatically closed.~%"))
(format t "File 'notes.txt' has been written successfully.~%"))
(write-notes)

运行此代码后,将在同一目录下创建一个名为 notes.txt 的文件,其中包含指定内容。format 中的 ~% 是打印换行符(newline character)的便捷方式。

现在,让我们从 notes.txt 中读取内容。我们将逐行读取文件直到文件末尾。

(defun read-notes ()
(with-open-file (stream "notes.txt" :if-does-not-exist nil)
(if stream
(loop for line = (read-line stream nil)
while line
do (format t "Read line: ~a~%" line))
(format t "File 'notes.txt' does not exist.~%"))))
(read-notes)

在此示例中:

  • :if-does-not-exist nil 可防止文件缺失时报错;with-open-file 将简单地返回 nil。
  • (read-line stream nil) 读取一行文本。第二个参数 nil 告诉它在文件末尾返回 nil 而不是发出错误信号。这个 nil 随后会使 loop 的 while 条件失败,从而优雅地终止循环。

如果你在写入示例之后运行此代码,输出将是:

Read line: Welcome to a modern Lisp tutorial!
Read line: File I/O is managed safely with with-open-file.
Read line: The file stream is automatically closed.

Common Lisp 有一种复杂且平台无关(platform-independent)的方式来处理文件路径,称为路径名(pathnames)。虽然像 "notes.txt" 这样的字符串通常有效,但使用路径名构造器 #p 会更健壮,特别是对于复杂路径。

;; 指定文件路径的更健壮方式
(with-open-file (stream #p"notes.txt" :direction :input)
...)

使用路径名允许 Lisp 自动处理目录分隔符(directory separators)和其他平台特定(platform-specific)的细节。