LISP - 输入 − 输出
Lisp - 输入与输出
Section titled “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 中最常用的输入函数。
| 排名 | 函数与现代用法 |
|---|---|
| 1 | read &optional input-stream eof-error-p eof-value recursive-p 从流中读取完整的 Lisp 对象(S-表达式)。此功能强大,但如果从不可信来源读取,可能存在安全风险,因为它可能执行代码。最适合用于配置文件或内部数据。 |
| 2 | read-line &optional input-stream eof-error-p eof-value recursive-p 将一行文本作为字符串读取。这是获取用户输入最安全、最常见的方式。 |
| 3 | read-char &optional input-stream eof-error-p eof-value recursive-p 从流中读取单个字符。 |
| 4 | read-from-string string &key … 从字符串而非流中读取 Lisp 对象。对于解析很有用。 |
| 5 | parse-integer string &key :start :end :radix :junk-allowed 安全地从字符串中解析整数。这是将字符串输入转换为数字的推荐方式。 |
| 6 | peek-char &optional peek-type input-stream … 查看流中的下一个字符而不消费它。对于解析复杂的文本格式很有用。 |
| 7 | read-byte binary-input-stream &optional … 从二进制流中读取单个字节。对于处理图像或网络协议等二进制文件至关重要。 |
读取和处理用户输入
Section titled “读取和处理用户输入”读取用户输入的现代方法是使用 read-line 将其作为一行文本读取,然后进行解析。这可以避免 read 函数的安全性和可用性问题。
示例:一个简单的问候语
Section titled “示例:一个简单的问候语”让我们编写一个程序,询问用户姓名并打印问候语。这演示了清晰的交互式 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.示例:计算圆形面积
Section titled “示例:计算圆形面积”本示例结合了读取文本、将其解析为数字、执行计算和打印格式化输出。
;;; 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)输出(成功运行)
Section titled “输出(成功运行)”Enter the radius of the circle: 5.5A circle with radius 5.50 has an area of 95.03.输出(无效输入)
Section titled “输出(无效输入)”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 函数无疑是最强大和通用的。
| 排名 | 函数与描述 |
|---|---|
| 1 | format destination control-string &rest arguments 生成精美格式化输出到流或字符串。这是几乎所有输出的首选函数。 |
| 2 | princ object &optional output-stream 以人类可读格式打印对象(例如,不带引号的字符串)。适用于提示和消息。 |
| 3 | prin1 object &optional output-stream 以机器可读格式打印对象(例如,带引号的字符串)。输出可以被 read 函数读回 Lisp。 |
| 4 | print object &optional output-stream 类似于 prin1,但在对象前添加一个换行符,在对象后添加一个空格。 |
| 5 | pprint object &optional output-stream “美观打印机”。它格式化复杂的 Lisp 对象(如嵌套列表或结构体)并进行缩进,使其易于阅读。 |
| 6 | write-line string &optional output-stream 将字符串写入流,后跟换行符。 |
| 7 | terpri &optional output-stream (TERminate PRInt) 输出一个换行符。 |
| 8 | finish-output & force-output &optional output-stream 这些函数管理输出缓冲,确保数据实际发送到其目的地(例如,终端或文件)。 |
使用 format 进行格式化输出
Section titled “使用 format 进行格式化输出”format 函数使用带有特殊指令(每个指令以波浪号 ~ 开头)的控制字符串来格式化其参数。format 的第一个参数是目标:t 表示 *standard-output*,nil 表示将格式化文本作为字符串返回,或者是一个特定的流。
以下是一些最有用的 format 指令:
| 指令 | 描述 |
|---|---|
| ~a | 美观(人类可读)表示。(类似于 princ) |
| ~s | 标准(机器可读)表示。(类似于 prin1) |
| ~d | 十进制整数。 |
| ~f | 定点格式浮点数。~,2f 四舍五入到小数点后两位。 |
| ~% | 输出一个换行符。 |
| ~& | 新行。仅当不在行首时才输出换行符。 |
| ~~ | 输出一个字面量波浪号字符。 |
| ~t | 制表。用于创建列。 |
| ~? | 间接。参数本身就是格式化字符串,带有自己的参数。 |
示例:生成报告
Section titled “示例:生成报告”让我们通过从数据列表中创建一份简单、对齐良好的报告来演示 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.00Mouse $25.50 $76.50Monitor $300.00 $300.00现代开发实践
Section titled “现代开发实践”在现代 Lisp 开发中,您经常会与 JSON 或 CSV 等标准格式的文件交互。虽然 Common Lisp 为 Lisp 自己的数据语法提供了内置函数,但对于其他格式,您需要使用库。Quicklisp 是 Common Lisp 事实上的包管理器,它使得安装用于以下任务的库变得容易:
- 解析 JSON:
jonathan或cl-json库。 - 解析 CSV:
cl-csv库。 - 文件路径管理:
uiop:merge-pathnames*或pathname-utils库用于跨平台路径处理。