Skip to content

LISP - 输入 − 输出

输入和输出 (I/O) 是允许程序与外部世界交互的基本操作。Common Lisp 提供了一套丰富而灵活的工具来处理 I/O,从读取用户输入到将格式化数据写入文件。本教程涵盖了 Common Lisp 中 I/O 的基本函数和现代最佳实践。

在 Lisp 中,所有 I/O 操作都发生在流 (streams) 上。流是数据的源或汇,例如终端、文件,甚至是内存中的字符串。Lisp 提供了几个标准流:

  • *standard-input*:默认的输入流,通常是键盘。
  • *standard-output*:默认的普通程序输出流,通常是屏幕。
  • *error-output*:错误消息的流。
  • *query-io*:用于交互式提示和用户对话的流。

下表提供了一些 Common Lisp 中最常用的输入函数。

排名函数与现代用法
1read &optional input-stream eof-error-p eof-value recursive-p

从流中读取完整的 Lisp 对象(S-表达式)。此功能强大,但如果从不可信来源读取,可能存在安全风险,因为它可能执行代码。最适合用于配置文件或内部数据。
2read-line &optional input-stream eof-error-p eof-value recursive-p

将一行文本作为字符串读取。这是获取用户输入最安全、最常见的方式。
3read-char &optional input-stream eof-error-p eof-value recursive-p

从流中读取单个字符。
4read-from-string string &key …

从字符串而非流中读取 Lisp 对象。对于解析很有用。
5parse-integer string &key :start :end :radix :junk-allowed

安全地从字符串中解析整数。这是将字符串输入转换为数字的推荐方式。
6peek-char &optional peek-type input-stream …

查看流中的下一个字符而不消费它。对于解析复杂的文本格式很有用。
7read-byte binary-input-stream &optional …

从二进制流中读取单个字节。对于处理图像或网络协议等二进制文件至关重要。

读取用户输入的现代方法是使用 read-line 将其作为一行文本读取,然后进行解析。这可以避免 read 函数的安全性和可用性问题。

让我们编写一个程序,询问用户姓名并打印问候语。这演示了清晰的交互式 I/O。

;;; main.lisp
(defun greet-user ()
"询问用户姓名并打印个性化问候语。"
;; 使用 princ 打印不带引号的提示,使用 terpri 换行。
(princ "Please enter your name: ")
;; 确保在等待输入前显示提示。
(finish-output *query-io*)
;; 安全地读取整行输入。
(let ((name (read-line *query-io*)))
(format t "~%Hello, ~a! Welcome to modern Lisp programming.~%" name)))
(greet-user)
Please enter your name: Alice
Hello, Alice! Welcome to modern Lisp programming.

本示例结合了读取文本、将其解析为数字、执行计算和打印格式化输出。

;;; main.lisp
(defun calculate-circle-area ()
"提示输入半径,计算圆形面积,并打印结果。"
(princ "Enter the radius of the circle: ")
(finish-output *query-io*)
(let ((radius-string (read-line *query-io*)))
;; 使用处理器优雅地管理无效输入。
(handler-case
;; 安全地将字符串解析为浮点数。
(let* ((radius (parse-number:parse-number radius-string))
(area (* pi radius radius)))
(format t "A circle with radius ~,2f has an area of ~,2f.~%" radius area))
;; 如果解析失败,则捕获此错误。
(error (c)
(format *error-output* "~%Invalid input: '~a' is not a valid number. ~a~%"
radius-string c)))))
;; 注意:本例假设 `parse-number` 等库可用。
;; 你也可以使用 `(read-from-string radius-string)`,但这安全性较低。
;; 对于仅使用标准库的解决方案,你可以这样做:
;; (let ((radius (read-from-string radius-string))) ...)
(calculate-circle-area)
Enter the radius of the circle: 5.5
A circle with radius 5.50 has an area of 95.03.
Enter the radius of the circle: five
Invalid input: 'five' is not a valid number. Could not parse "five" as a number.

Common Lisp 提供了几个用于打印数据的函数。format 函数无疑是最强大和通用的。

排名函数与描述
1format destination control-string &rest arguments

生成精美格式化输出到流或字符串。这是几乎所有输出的首选函数。
2princ object &optional output-stream

以人类可读格式打印对象(例如,不带引号的字符串)。适用于提示和消息。
3prin1 object &optional output-stream

以机器可读格式打印对象(例如,带引号的字符串)。输出可以被 read 函数读回 Lisp。
4print object &optional output-stream

类似于 prin1,但在对象前添加一个换行符,在对象后添加一个空格。
5pprint object &optional output-stream

“美观打印机”。它格式化复杂的 Lisp 对象(如嵌套列表或结构体)并进行缩进,使其易于阅读。
6write-line string &optional output-stream

将字符串写入流,后跟换行符。
7terpri &optional output-stream

(TERminate PRInt) 输出一个换行符。
8finish-output & force-output &optional output-stream

这些函数管理输出缓冲,确保数据实际发送到其目的地(例如,终端或文件)。

format 函数使用带有特殊指令(每个指令以波浪号 ~ 开头)的控制字符串来格式化其参数。format 的第一个参数是目标:t 表示 *standard-output*,nil 表示将格式化文本作为字符串返回,或者是一个特定的流。

以下是一些最有用的 format 指令:

指令描述
~a美观(人类可读)表示。(类似于 princ)
~s标准(机器可读)表示。(类似于 prin1)
~d十进制整数。
~f定点格式浮点数。~,2f 四舍五入到小数点后两位。
~%输出一个换行符。
~&新行。仅当不在行首时才输出换行符。
~~输出一个字面量波浪号字符。
~t制表。用于创建列。
~?间接。参数本身就是格式化字符串,带有自己的参数。

让我们通过从数据列表中创建一份简单、对齐良好的报告来演示 format 的强大功能。

;;; main.lisp
(defun generate-sales-report (data)
"根据(商品 价格 数量)列表生成格式化的销售报告。"
(format t "~&~va ~20t~va ~30t~va~%" "Item" "Price" "Total")
(format t "~40,,,'-A~%" "") ; 打印分隔线
(dolist (record data)
(let* ((item (first record))
(price (second record))
(quantity (third record))
(total (* price quantity)))
(format t "~va ~20t$~,2f ~30t$~,2f~%" item price total))))
(let ((sales-data '(("Keyboard" 75.00 2)
("Mouse" 25.50 3)
("Monitor" 300.00 1))))
(generate-sales-report sales-data))
Item Price Total
----------------------------------------
Keyboard $75.00 $150.00
Mouse $25.50 $76.50
Monitor $300.00 $300.00

在现代 Lisp 开发中,您经常会与 JSON 或 CSV 等标准格式的文件交互。虽然 Common Lisp 为 Lisp 自己的数据语法提供了内置函数,但对于其他格式,您需要使用库。Quicklisp 是 Common Lisp 事实上的包管理器,它使得安装用于以下任务的库变得容易:

  • 解析 JSON:jonathan 或 cl-json 库。
  • 解析 CSV:cl-csv 库。
  • 文件路径管理:uiop:merge-pathnames* 或 pathname-utils 库用于跨平台路径处理。