1
- 12-I/O
1
+ 12-IO和文件系统
2
2
======
3
- [ I/O模块] ( #121-io%E6%A8%A1%E5%9D%97 )
4
- [ 文件模块] ( #122-%E6%96%87%E4%BB%B6%E6%A8%A1%E5%9D%97 )
5
- [ 路径模块] ( #123-%E8%B7%AF%E5%BE%84%E6%A8%A1%E5%9D%97 )
6
- [ 进程和组长] ( #124-%E8%BF%9B%E7%A8%8B%E5%92%8C%E7%BB%84%E9%95%BF )
7
- [ * iodata* 和* chardata* ] ( #125-iodata%E5%92%8Cchardata )
8
-
9
- 本章简单介绍Elixir的输入、输出机制以及相关的模块,
10
- 如[ IO] ( http://elixir-lang.org/docs/stable/elixir/IO.html ) ,
11
- [ 文件] ( http://elixir-lang.org/docs/stable/elixir/File.html )
12
- 和[ 路径] ( http://elixir-lang.org/docs/stable/elixir/Path.html ) 。
13
-
14
- 现在介绍I/O似乎有点早,但是I/O系统可以让我们一窥Elixir哲学,满足我们对该语言以及VM的好奇心。
15
-
16
- ##12 .1-IO模块
17
- IO模块是Elixir语言中读写标准输入、输出、标准错误、文件、设备的主要机制。
18
- 使用该模块的方法颇为直接:
3
+
4
+ 本章简单介绍Elixir的输入、输出机制,文件系统相关的任务,
5
+ 以及涉及到的模块,如[ ``` IO ``` ] ( http://elixir-lang.org/docs/stable/elixir/IO.html ) ,
6
+ [ ``` File ``` ] ( http://elixir-lang.org/docs/stable/elixir/File.html )
7
+ 和[ ``` Path ``` ] ( http://elixir-lang.org/docs/stable/elixir/Path.html ) 。
8
+
9
+ 我们曾经在早期的文章中说现在介绍IO似乎有点早。
10
+ 但是,我们注意到IO系统其实提供了一窥Elixir和虚拟机的设计哲学和精妙的绝佳机会。
11
+
12
+ > “早期的文章”:现在介绍I/O似乎有点早,但是I/O系统可以让我们一窥Elixir哲学,满足我们对该语言以及VM的好奇心。
13
+
14
+ ## ``` IO ``` 模块
15
+
16
+ ``` IO ``` 模块提供了Elixir语言中读写标准输入/输出(``` :stdio ``` )、
17
+ 标准错误(``` :stderr ``` )、文件和其他IO设备的主要机制。
18
+
19
+ 该模块的使用方法颇为直白:
20
+
19
21
``` elixir
20
22
iex> IO .puts " hello world"
21
23
" hello world"
@@ -25,18 +27,21 @@ yes or no? yes
25
27
" yes\n "
26
28
```
27
29
28
- IO模块中的函数默认使用标准输入输出。
29
- 我们也可以传递``` :stderr ``` 来指示将错误信息写到标准错误设备上:
30
+ 默认情况下,IO模块中的函数从标准输入中读取,向标准输出中写入。
31
+ 我们可以传递参数,比如``` :stderr ``` ,来指示将信息写到标准错误上:
32
+
30
33
``` elixir
31
34
iex> IO .puts :stderr , " hello world"
32
35
" hello world"
33
36
:ok
34
37
```
35
38
36
- #12 .2-文件模块
37
- 文件模块包含了可以让我们读写文件的函数。
38
- 默认情况下文件是以二进制模式打开,它要求程序员使用特殊的``` IO.binread/2 ```
39
- 和``` IO.binwrite/2 ``` 函数来读写文件:
39
+ ## ``` File ``` 模块
40
+
41
+ ``` File ``` 模块包含的函数可以让我们打开文件作为IO设备。
42
+ 默认情况下文件是以二进制模式打开,
43
+ 它要求程序员使用特定的``` IO.binread/2 ``` 和``` IO.binwrite/2 ``` 函数来读写文件:
44
+
40
45
``` elixir
41
46
iex> {:ok , file} = File .open " hello" , [:write ]
42
47
{:ok , # PID<0.47.0>}
@@ -48,22 +53,23 @@ iex> File.read "hello"
48
53
{:ok , " world" }
49
54
```
50
55
51
- 文件可以使用``` :utf8 ``` 编码打开,然后就可以被IO模块中其他函数使用了:
56
+ 文件可以使用``` :utf8 ``` 编码方式打开,这让``` File ``` 模块以UTF-8编码方式解析文件中读取的字节:
57
+
52
58
``` elixir
53
59
iex> {:ok , file} = File .open " another" , [:write , :utf8 ]
54
60
{:ok , # PID<0.48.0>}
55
61
```
56
62
57
- 除了打开、读写文件的函数,文件模块还有许多函数来操作文件系统 。
58
- 这些函数根据Unix功能相对应的命令命名 。
59
- 如``` File.rm/1 ``` 用来删除文件; ``` File.mkdir/1 ```
60
- 用来创建目录; ``` File.mkdir_p/1 ``` 创建目录并保证其父目录一并创建;
61
- 还有 ``` File.cp_r/2 ``` 和``` File.rm_rf/2 ``` 用来递归地复制和删除整个目录 。
63
+ 除了打开、读写文件的函数,文件模块还提供了许多函数来操作文件系统 。
64
+ 这些函数根据Unix平台上功能相对应的命令来命名 。
65
+ 如``` File.rm/1 ``` 用来删除文件, ``` File.mkdir/1 ``` 用来创建目录,
66
+ ``` File.mkdir_p/1 ``` 创建目录并保证其父目录一并创建。
67
+ 甚至还有 ``` File.cp_r/2 ``` 和``` File.rm_rf/1 ``` 用来递归地复制或删除整个目录 。
62
68
69
+ 你会注意到``` File ``` 模块的函数有两种变体,一个普通的和一个名称末尾有``` ! ``` (bang)的。
70
+ 例如在上面的例子里,我们在读取“hello”文件时,用的是不带``` ! ``` 的版本。
71
+ 相对地,我们也可以使用``` File.read!/1 ``` :
63
72
64
- 你还会注意到文件模块中,一般函数都有一个名称类似的版本。区别是名称上一个有!(bang)一个没有。
65
- 例如,上面的例子我们在读取“hello”文件时,用的是不带!号的版本。
66
- 下面用例子演示下它们的区别:
67
73
``` elixir
68
74
iex> File .read " hello"
69
75
{:ok , " world" }
@@ -75,76 +81,107 @@ iex> File.read! "unknown"
75
81
** (File .Error ) could not read file unknown: no such file or directory
76
82
```
77
83
78
- 注意看,当文件不存在时,带!号的版本会报错。就是说不带!号的版本能照顾到模式匹配出来的不同情况。
79
- 但有的时候,你就是希望文件在那儿,!使得它能报出有意义的错误。
84
+ 注意看,当文件不存在时,带!号的版本会抛出一个错误。
85
+ 而不带!号的版本适用于你对不同结果进行模式匹配的情形:
86
+
87
+ ``` elixir
88
+ case File .read (file) do
89
+ {:ok , body} -> # do something with the `body`
90
+ {:error , reason} -> # handle the error caused by `reason`
91
+ end
92
+ ```
93
+
94
+ 但有的时候,你就是希望文件在那儿,!号变体更加适用,因为它能报出有意义的错误。
95
+ 例如,如果这么写:
80
96
81
- 因此,不要写:
82
97
``` elixir
83
98
{:ok , body} = File .read (file)
84
99
```
85
100
86
- 相反地,应该这么写:
101
+ 当遇到文件不存在的情况时,函数返回``` {:error, reason} ``` ,然后导致在跟左侧元祖做模式匹配时失败。
102
+ 失败依然会抛出一个异常,但是该异常的错误信息是描述一次模式匹配失败,而不是文件的什么错误。
103
+ 从而在一定程度上掩盖了真正的失败原因。
104
+
105
+ 可以这么写:
106
+
87
107
``` elixir
88
108
case File .read (file) do
89
109
{:ok , body} -> # handle ok
90
110
{:error , r} -> # handle error
91
111
end
92
112
```
93
- 或者
113
+
114
+ 当然,更推荐的写法:
115
+
94
116
``` elixir
95
117
File .read! (file)
96
118
```
97
119
98
- ## 12.3-路径模块
99
- 文件模块中绝大多数函数都以路径作为参数。通常这些路径都是二进制,可以被路径模块提供的函数操作:
120
+ ## ``` Path ``` 模块
121
+
122
+ ``` File ``` 模块中的绝大多数函数都以各种路径作为参数。
123
+ 通常,这些路径都是二进制串(binaries)。
124
+ ``` Path ``` 模块提供了操作这些路径的函数:
125
+
100
126
``` elixir
101
127
iex> Path .join (" foo" , " bar" )
102
128
" foo/bar"
103
129
iex> Path .expand (" ~/hello" )
104
130
" /Users/jose/hello"
105
131
```
106
132
133
+ 推荐使用``` Path ``` 模块提供的函数而不是直接手动操作代表路径的二进制串。
134
+ 因为``` Path ``` 模块考虑了不同操作系统的区别,使得各种处理是对“操作系统”透明的。
135
+ 最后,记住在Windows平台上处理文件操作时,Elixir会自动将斜杠(/)转换成反斜杠(\)。
136
+
137
+ 有了上面介绍的模块和函数,我们已经能对文件系统进行基本的操作。
138
+ 下面将讨论有关IO的一些高级话题。这部分并不是写Elixir程序必须掌握的,可以跳过不看。
139
+ 但是如果你浏览一下,可以大致了解IO是如何在VM上实现的,以及其它一些有趣的内容。
107
140
108
- 有了以上介绍的几个模块和函数,我们已经能对文件系统进行基本的IO操作。
109
- 下面将讨论I/O模块中令人好奇的高级话题。这部分不是写Elixir程序必须掌握的,可以跳过不看。
110
- 但是如果你大概地浏览一下,可以了解IO是如何在VM上实现以及其它一些有趣的内容。
141
+ ## 进程(Processes)和组长(group leaders)
142
+
143
+ 你可能已经注意到 ``` File.open/2 ``` 函数返回类似 ``` {:ok, pid} ``` 的元祖:
111
144
112
- ## 12.4-进程和组长
113
- 你可能已经发现,``` File.open/2 ``` 函数返回了一个包含PID的元祖:
114
145
``` elixir
115
146
iex> {:ok , file} = File .open " hello" , [:write ]
116
147
{:ok , # PID<0.47.0>}
117
- ```
148
+ ```
149
+
150
+ ``` IO ``` 模块实际上是和进程(Process)一起协同工作的。
151
+ 当你调用``` IO.write(pid, binary) ``` 时,``` IO ``` 模块将发送一条消息给``` pid ``` 标示的进程,
152
+ 附上所期望进行的操作。
153
+
154
+ 如果我们自己用进程来描述这个过程:
118
155
119
- I/O模块实际上是同进程协同工作的。当你调用``` IO.write(pid, binary) ``` 时,
120
- I/O模块将发送一条消息给执行操作的进程。
121
- 让我们用自己的代码表述下这个过程:
122
156
``` elixir
123
157
iex> pid = spawn fn ->
124
158
.. .> receive do: (msg - > IO .inspect msg)
125
159
.. .> end
126
160
# PID<0.57.0>
127
161
iex> IO .write (pid, " hello" )
128
- {:io_request , # PID<0.41.0>, #PID <0.57.0 >, {:put_chars, :unicode, "hello"}}
162
+ {:io_request , # PID<0.41.0>, #Reference <0.0.8.91 >, {:put_chars, :unicode, "hello"}}
129
163
** (ErlangError ) erlang error: :terminated
130
164
```
131
165
132
- 调用``` IO.write/2 ``` 之后,可以看见打印出了发给IO模块的请求。
133
- 然而因为我们没有提供某些东西,这个请求失败了。
166
+ 调用``` IO.write/2 ``` 之后,可以看到``` IO ``` 模块发出的请求(四个元素的元祖)被打印了出来。
167
+ 不久后,因为我们并未提供``` IO ``` 模块期待的某种结果,这个请求失败了。
168
+
169
+ [ ``` StringIO ``` 模块] ( http://elixir-lang.org/docs/stable/elixir/StringIO.html )
170
+ 提供了基于字符串的``` IO ``` 设备消息处理功能(将字符串看做IO设备):
134
171
135
- [ StringIO模块] ( http://elixir-lang.org/docs/stable/elixir/StringIO.html )
136
- 提供了一个基于字符串的IO实现:
137
172
``` elixir
138
173
iex> {:ok , pid} = StringIO .open (" hello" )
139
174
{:ok , # PID<0.43.0>}
140
175
iex> IO .read (pid, 2 )
141
176
" he"
142
177
```
143
178
144
- Erlang虚拟机用进程给I/O设备建模,允许同一个网络中的不同节点通过交换文件进程,
145
- 实现节点间的文件读写。在所有IO设备之中,有一个特殊的进程,称作组长(group leader)。
179
+ Erlang虚拟机通过利用进程给IO设备建模,使得同一个网络中的不同节点可以通过交换文件进程,
180
+ 实现跨节点的文件读写。而在所有IO设备之中,有一个特殊的进程,称作* 组长(group leader)* 。
181
+
182
+ 当你写东西到标准输入输出(``` :stdio ``` ),实际上是发送了一条消息给进程组长,
183
+ 它把内容写给标准输出的文件描述符:
146
184
147
- 当你写东西到标准输出,实际上是发送了一条消息给组长,它把内容写给* STDIO文件表述者* :
148
185
``` elixir
149
186
iex> IO .puts :stdio , " hello"
150
187
hello
@@ -154,16 +191,19 @@ hello
154
191
:ok
155
192
```
156
193
157
- 组长可为每个进程做相应配置,用于处理不同的情况 。
158
- 例如,当在远程终端执行代码时,它能保证远程机器的消息可以被重定向到发起操作的终端上 。
194
+ 在不同的应用场景下,让哪个进程作为组长是可以配置的 。
195
+ 例如,当在远程终端执行代码时,通过配置组长,可以使得远程节点上打印的消息可以被重定向到发起操作(你)的终端上 。
159
196
160
- ## 12.5-* iodata* 和* chardata*
161
- 在以上所有例子中,我们都用的是二进制/字符串方式读写文件。
162
- 在“二进制、字符串和字符列表”那章里,我们注意到字符串就是普通的bytes,
163
- 而字符列表是code points(字符码)的列表。
197
+ ## ``` iodata ``` 和``` chardata ```
198
+
199
+ 在以上所有例子中,我们都用的是二进制串格式写入文件。
200
+ 在“二进制串、字符串和字符列表”那章里,我们提到过字符串(string)就是普通的bytes,
201
+ 而字符列表(char list)就是字符编码(code point,如“Uxxxx”、“049”)的列表。
202
+
203
+ ``` IO ``` 模块和``` File ``` 模块中的函数还可以接受列表作为参数。
204
+ 不光如此,它们还接受混合类型的列表,里面内容可以是列表(如``` 'ab c' ``` )、
205
+ 整形(如``` 49 ``` ,``` ?A ``` --- 返回65)和二进制串(如``` "ab c" ``` ):
164
206
165
- I/O模块和文件模块中的函数接受列表作为参数。
166
- 还可以接受混合类型的列表,里面内容是整形、二进制都行:
167
207
``` elixir
168
208
iex> IO .puts ' hello world'
169
209
hello world
@@ -173,12 +213,18 @@ hello world
173
213
:ok
174
214
```
175
215
216
+ > ``` ? ``` 返回后面字符的编码(code point),如``` ?A ``` 默认情况下返回65。
217
+
176
218
尽管如此,有些地方还是要注意。一个列表可能表示一串byte,或者一串字符。
177
- 用哪一种需要看I/O设备是怎么编码的。
219
+ 用哪一种需要看IO设备是怎么编码的。
220
+ 如果不指明编码,文件就以raw模式打开,
221
+ 这时候只能用``` IO ``` 模块里``` bin* ``` 开头(二进制版本)的函数对其进行操作。
222
+ 这些函数接受``` iodata ``` 作为参数。也就是说,它们期待一个整数值的列表,用来表示bytes或二进制串。
223
+
224
+ On the other hand, : stdio and files opened with : utf8 encoding work with the remaining functions in the IO module. Those functions expect a char_data as an argument, that is, a list of characters or strings.
178
225
179
- 如果不指明编码,文件就以raw模式打开,这时候只能用文件模块里bin* 开头(二进制版)
180
- 的函数对其进行操作。
181
- 这些函数接受* iodata* 作为参数,即,它们期待一个整数值的列表,用来表示byte或二进制。
226
+ 另一边,使用``` :utf8 ``` 打开的``` :stdio ``` 和文件使用``` IO ``` 模块里剩下来其它的函数。
227
+ 这些函数期待``` char_data ``` 作为参数,即一串字符或字符串的列表。
182
228
183
- 尽管只是细微的差别,如果你打算传递列表给那些函数,你需要考虑那些细节 。
184
- 底层的bytes可以表示二进制,这种表示就是raw的 。
229
+ 尽管差别比较精妙,但只是当你想要传递列表给那些函数的时候,才用担心一下细节问题 。
230
+ 而二进制串(binaries)表示了底层的bytes字节列表,这种表示已经是raw的了 。
0 commit comments