第七章 輸入輸出
===============
Scheme的輸入/輸出程序可以使你從輸入端口讀取或者將寫入到輸出端口。端口可以關聯到控制臺,文件和字符串。
## 7.1 讀取
Scheme的讀取程序帶有一個可選的輸入端口參數。如果端口沒有特別指定,則假設為當前端口(一般是控制臺)。
讀取的內容可以是一個字符,一行數據或是S表達式。當每次執行讀取時,端口的狀態就會改變,因此下一次就會讀取當前已讀取內容后面的內容。如果沒有更多的內容可讀,讀取程序將返回一個特殊的數據——文件結束符或EOF對象。這個對象只能用`eof-object?`函數來判斷。
`read-char`程序會從端口讀取下一個字符。`read-line`程序會讀取下一行數據,并返回一個字符串(不包括最后的換行符),`read`程序則會讀取下一個S表達式。
## 7.2 寫入
Scheme的寫入程序接受一個要被寫入的對象和一個可選的輸出端口參數。如果未指定端口,則假設為當前端口(一般為控制臺)。
寫入的對象可以是字符或是S表達式。
`write-char`程序可以向輸出端口寫入一個給定的字符(不包括`#\`)。
`write`和`display`程序都可以向端口寫入一個給定的S表達式,唯一的區別是:`write`程序會使用機器可讀型的格式而`display`程序卻不用。例如,`write`用雙引號表示字符串,用`#\`句法表示字符,但`display`卻不這么做。
`newline`程序會在輸出端口輸出一個換行符。
## 7.3 文件端口
如果端口是標準的輸入和輸出端口,Scheme的I/O程序就不需要端口參數。但是,如果你明確需要這些端口,則`current-input-port`和`current-output-port`這些零參數程序會提供這個功能,例如:
```scheme
(display 9)
(display 9 (current-output-port))
```
擁有相同的效果。
一個端口通過打開文件和這個文件關聯在一起。`open-input-file`程序會接受一個文件名作為參數并返回一個和這個文件關聯的新的輸入端口。`open-output-file`程序會接受一個文件名作為參數并返回一個和這個文件關聯的新的輸出端口。如果打開一個不存在的輸入文件,或者打開一個已經存在的輸出文件,程序都會出錯。
當你已經在一個端口執行完輸入或輸出后,你需要使用`close-input-port`或`close-output-port`程序將它關閉。
在下述例子中,假如文件`hello.txt`文件只包含一個單詞`hello`。
```scheme
(define i (open-input-file "hello.txt"))
(read-char i)
=> #\h
(define j (read i))
j
=> ello
```
假如文件`greeting.txt`在下述程序運行前不存在:
```scheme
(define o (open-output-file "greeting.txt"))
(display "hello" o)
(write-char #\space o)
(display 'world o)
(newline o)
(close-output-port o)
```
現在`Greeting.txt`文件將會包含這樣一行:
```
hello world
```
### 7.3.1 文件端口的自動打開和關閉
Scheme提供了`call-with-input-file`和`call-with-output-file`過程,這些過程會照顧好打開的端口并在你使用完后將端口關閉。
`call-with-input-file`程序接受一個文件名參數和一個過程。這個過程被應用在一個已打開的文件輸入端口。當程序結束時,它的結果會在保證端口關閉后返回。
```scheme
(call-with-input-file "hello.txt"
(lambda (i)
(let* ((a (read-char i))
(b (read-char i))
(c (read-char i)))
(list a b c))))
=> (#\h #\e #\l)
```
`call-with-output-file`程序會對輸出文件提供類似的服務。
## 7.4 字符串端口
一般來說將字符串與端口相關聯是很方便的。因此,`open-input-string`程序將一個給定的字符串和一個端口關聯起來。讀取這個端口的程序將讀出下述字符串:
```scheme
(define i (open-input-string "hello world"))
(read-char i)
=> #\h
(read i)
=> ello
(read i)
=> world
```
`open-output-string`創建了一個輸出端口,最終可以用于創建一個字符串:
```scheme
(define o (open-output-string))
(write 'hello o)
(write-char #\, o)
(display " " o)
(display "world" o)
```
現在你可以使用`get-output-string`程序得到保留在字符串端口`o`中的字符串:
```scheme
(get-output-string o)
=> "hello, world"
```
字符串端口不需要顯式地去關閉。
## 7.5 加載文件
我們已將看到`load`程序可以加載包含`Scheme`代碼的文件。`load`一個文件意味著按順序求值文件中每一個Scheme表達式。`load`中的路徑參數是相對當前Scheme工作目錄計算的,該工作目錄一般是調用Scheme可執行文件時的目錄。
一個文件可以加載其他的文件,這在包含許多文件的大項目中十分有用。但是,除非使用絕對路徑,否則`load`參數中的文件位置將依賴于執行Scheme的當前目錄。而提供絕對路徑名并不是很方便,因為我們更愿意把項目文件作為一個單元(保留它們的相對路徑名)在很多不同機器中運行。
Mzscheme提供了`load-relative`程序,可以很好的解決這個問題。`load-relative`,和`load`相似,帶有一個路徑名參數。當在`foo.scm`文件中出現`load-relative`調用時,它的參數的路徑將根據文件`foo.scm`所在目錄的路徑來計算。特別注意的是,這個路徑名和執行Scheme的當前目錄無關,因此也就可以方便地進行多文件程序的開發。