一个Racket端口对应一个流的Unix概念(不要与racket/stream的流混淆)。
一个Racket端口(port)代表一个数据源或数据池,诸如一个文件、一个终端、一个TCP连接或者一个内存字符串。端口提供顺序的访问,在那里数据能够被分批次地读或写,而不需要数据被一次性接受或生成。更具体地,一个输入端口(input port)代表一个程序能从中读取数据的一个源,一个输出端口(output port)代表一个程序能够向其中写入数据的一个池。
8.1 端口的种类 |
8.2 默认端口 |
8.3 读写Racket数据 |
8.4 数据类型和序列化 |
8.5 字节、字符和编码 |
8.6 I/O模式 |
8.1 端口的种类
不同的函数创建不同类型的端口,这里有一些例子:
文件(Files):open-output-file函数打开供写入的一个文件,而open-input-file打开供读取的一个文件。
Examples:
> (define out (open-output-file "data")) > (display "hello" out) > (close-output-port out) > (define in (open-input-file "data")) > (read-line in) "hello"
> (close-input-port in)
如果一个文件已经存在,那open-output-file默认情况下引发一个异常。提供一个如#:exists 'truncate或#:exists 'update的选项来重写或更新这个文件。
Examples:
> (define out (open-output-file "data" #:exists 'truncate)) > (display "howdy" out) > (close-output-port out)
而不是不得不用关闭调用去匹配打开调用,绝大多数Racket程序员会使用call-with-input-file和call-with-output-file函数接收一个函数去调用以实施预期的操作。这个函数作为端口的唯一参数,它为操作被自动打开与关闭。
Examples:
> (call-with-output-file "data" #:exists 'truncate (lambda (out) (display "hello" out)))
> (call-with-input-file "data" (lambda (in) (read-line in))) "hello"
字符串(Strings):open-output-string函数创建一个将数据堆入一个字符串的一个端口,并且get-output-string提取累加字符串。open-input-string函数创建一个端口用于从字符串读取。
Examples:
> (define p (open-output-string)) > (display "hello" p) > (get-output-string p) "hello"
> (read-line (open-input-string "goodbye\nfarewell")) "goodbye"
TCP连接(TCP Connections):tcp-connect函数为一个TCP通信的客户端一侧既创建了一个输入端口也创建了一个输出端口。tcp-listen函数创建一个服务器,它通过tcp-accept接受连接。
Examples:
> (define server (tcp-listen 12345)) > (define-values (c-in c-out) (tcp-connect "localhost" 12345)) > (define-values (s-in s-out) (tcp-accept server)) > (display "hello\n" c-out) > (close-output-port c-out) > (read-line s-in) "hello"
> (read-line s-in) #
进程管道(Process Pipes):subprocess函数运行在操作系统级的一个新的进程并返回与对应子进程的stdin、stdout和stderr的端口。(这首先的三个参数可以将某些现有端口直接连接到子进程,而不是创建新端口。)
Examples:
> (define-values (p stdout stdin stderr) (subprocess #f #f #f "/usr/bin/wc" "-w")) > (display "a b c\n" stdin) > (close-output-port stdin) > (read-line stdout) " 3"
> (close-input-port stdout) > (close-input-port stderr)
内部管道(Internal Pipes):make-pipe函数返回作为管道末端的两个端口。这种类型的管道属于Racket内部的,并且与用于不同进程之间通信的OS级管道无关。
Examples:
> (define-values (in out) (make-pipe)) > (display "garbage" out) > (close-output-port out) > (read-line in) "garbage"
8.2 默认端口
对于大多数简单的I/O函数,这个目标端口是一个可选参数,并且这个默认值是当前输入端口(current input port)。此外,错误信息被写入当前错误端口(current error port),这是一个输出端口。current-input-port、current-output-port和current-error-port函数返回对应的当前端口。
Examples:
> (display "Hi") Hi
> (display "Hi" (current-output-port)) ; 同样 Hi
如果你在终端启动racket程序,那么当前输入、输出及错误端口都被连接到终端。更一般地,它们被连接到系统级的stdin、stdout和stderr。在本指南中,示例用紫色显示写入stdout的输出,用红色斜体显示写入stderr的输出。
Examples:
(define (swing-hammer) (display "Ouch!" (current-error-port))) > (swing-hammer) Ouch!
当前端口函数实际上是参数(parameters),它代表它们的值能够用parameterize设置。
参见《动态绑定:parameterize》以获得对parameters的一个说明。
Example:
> (let ([s (open-output-string)]) (parameterize ([current-error-port s]) (swing-hammer) (swing-hammer) (swing-hammer)) (get-output-string s)) "Ouch!Ouch!Ouch!"
8.3 读写Racket数据
就像贯穿始终的《内置的数据类型》,Racket提供三种方式打印一个内建值的一个实例:
print, 它以用于打印一个REPL结果的相同方式打印一个值;以及
write, 它以在输出上的read产生值的这样一种方式打印一个值;以及
display, 它趋向于将一个值缩小到它的字符或字节内容——至少对于那些主要关于字符或字节的数据类型——否则它会回到与write相同的输出。
这里有一些每个使用的例子:
|
|
|
|
|
总的来说,print对应Racket语法的表达层,write对应阅读层,display大致对应字符层。
printf函数支持数据与文本的简单格式。在printf支持的格式字符串中,~a display下一个参数,~s write下一个参数,而~v print下一个参数。
Examples:
(define (deliver who when what) (printf "Items ~a for shopper ~s: ~v" who when what)) > (deliver '("list") '("John") '("milk")) Items (list) for shopper ("John"): '("milk")
使用write后,与display或print不同的是,许多数据的表可以通过read重新读入。被print的相同值也能被read解析,但是这个结果也许有额外的引号表,因为一个print的表意味着类似于一个表达式那样被读入。
Examples:
> (define-values (in out) (make-pipe)) > (write "hello" out) > (read in) "hello"
> (write '("alphabet" soup) out) > (read in) '("alphabet" soup)
> (write #hash((a . "apple") (b . "banana")) out) > (read in) '#hash((a . "apple") (b . "banana"))
> (print '("alphabet" soup) out) > (read in) ''("alphabet" soup)
> (display '("alphabet" soup) out) > (read in) '(alphabet soup)
8.4 数据类型和序列化
预制的(prefab)结构类型(查看《预制结构类型》)自动支持序列化(serialization):它们可被写入一个输出流同时一个副本可以从输入流中读回:
> (define-values (in out) (make-pipe)) > (write #s(sprout bean) out) > (read in) '#s(sprout bean)
被struct创建的其它结构类型,提供较预制的结构类型更多的抽象,通常write既使用#<....>记号(对于不透明结构类型)也使用#(....)矢量记号(对于透明结构类型)。不论在那种情况下这个结果都不能作为结构类型的一个实例被读回。
> (struct posn (x y)) > (write (posn 1 2)) #
> (define-values (in out) (make-pipe)) > (write (posn 1 2) out) > (read in) UNKNOWN::0: read: bad syntax `#<'
> (struct posn (x y) #:transparent) > (write (posn 1 2)) #(struct:posn 1 2)
> (define-values (in out) (make-pipe)) > (write (posn 1 2) out) > (define v (read in)) > v '#(struct:posn 1 2)
> (posn? v) #f
> (vector? v) #t
serializable-struct表定义一个结构类型,它能够被serialize为一个值,这个值可使用write被打印并通过read读入。serialize的结果可被deserialize为原始结构类的一个实例。序列化表和函数通过racket/serialize库提供。
Examples:
> (require racket/serialize) > (serializable-struct posn (x y) #:transparent) > (deserialize (serialize (posn 1 2))) (posn 1 2)
> (write (serialize (posn 1 2))) ((3) 1 ((#f . deserialize-info:posn-v0)) 0 () () (0 1 2))
> (define-values (in out) (make-pipe)) > (write (serialize (posn 1 2)) out) > (deserialize (read in)) (posn 1 2)
除了被struct绑定的名字外,serializable-struct绑定一个具有反序列化信息的标识符,并且它会自动从一个模块上下文provide这个反序列化标识符。当一个值被反序列化时这个反序列化标识符被反射地访问。
8.5 字节、字符和编码
类似read-line、read、display和write的函数都根据字符(character)(它对应于Unicode标量值)工作。概念上来说,它们根据read-char和write-char被实现。
更初级一点,端口读和写字节(byte)而不是字符(character)。函数read-byte与write-byte读和写原始字节。其它函数,比如read-bytes-line,建立在字节操作的顶层而不是字符操作。
事实上,read-char和write-char函数概念上根据read-byte和write-byte被实现。当一个单一字节的值小于128时,那么它对应于一个ASCII字符。任何其它的字节被视为一个UTF-8序列的一部分,其中UTF-8是以字节为单位的编码Unicode标量值的一个特殊标准方式(它具有ASCII字符作为它们自身编码的优良属性)。此外,一个单个read-char可能调用read-byte多次,并且一个单个write-char可能生成多个输出字节。
read-char和write-char操作总使用一个UTF-8编码。如果你有一个使用一个不同编码的文本流,或者如果你想在一个不同编码中生成一个文本流,使用reencode-input-port或reencode-output-port。reencode-input-port函数从一个你指定为一个UTF-8流的编码中转换一个输入流;以这种方式,read-char明白UTF-8编码,即使这个源文件使用了一个不同的编码。但要小心,那个read-byte也明白这个重编码数据,而不是原始字节流。
8.6 I/O模式
如果你想处理一个文件的单个行,那么你可以用in-lines使用for:
> (define (upcase-all in) (for ([l (in-lines in)]) (display (string-upcase l)) (newline)))
> (upcase-all (open-input-string (string-append "Hello, World!\n" "Can you hear me, now?")))
HELLO, WORLD!
CAN YOU HEAR ME, NOW?
如果你想确定是否“hello”出现在一个文件中,那你可以搜索独立的行,但是更简便的方法是对这个流简单应用一个正则表达式(参见《正则表达式》):
> (define (has-hello? in) (regexp-match? #rx"hello" in)) > (has-hello? (open-input-string "hello")) #t
> (has-hello? (open-input-string "goodbye")) #f
如果你想拷贝一个端口至另一个,使用来自racket/port的copy-port,它能够在大量数据可用时有效转移大的块,但如果可以的话也立即转移小数据块。:
> (define o (open-output-string)) > (copy-port (open-input-string "broom") o) > (get-output-string o) "broom"