当前位置: 首页 > 工具软件 > Racket > 使用案例 >

Racket编程指南——8 输入和输出

毕魁
2023-12-01

输入和输出

一个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)

    #<eof>

  • 进程管道(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-portcurrent-output-portcurrent-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 1/2)

1/2

> (print #\x)

#\x

> (print "hello")

"hello"

> (print #"goodbye")

#"goodbye"

> (print '|pea pod|)

'|pea pod|

> (print '("i" pod))

'("i" pod)

> (print write)

#<procedure:write>

> (write 1/2)

1/2

> (write #\x)

#\x

> (write "hello")

"hello"

> (write #"goodbye")

#"goodbye"

> (write '|pea pod|)

|pea pod|

> (write '("i" pod))

("i" pod)

> (write write)

#<procedure:write>

> (display 1/2)

1/2

> (display #\x)

x

> (display "hello")

hello

> (display #"goodbye")

goodbye

> (display '|pea pod|)

pea pod

> (display '("i" pod))

(i pod)

> (display write)

#<procedure: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))

#<posn>

> (define-values (in out) (make-pipe))
> (write (posn 1 2) out)
> (read in)

pipe::1: 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/portcopy-port,它能够在大量数据可用时有效转移大的块,但如果可以的话也立即转移小数据块。:

> (define o (open-output-string))
> (copy-port (open-input-string "broom") o)
> (get-output-string o)

"broom"

 类似资料: