企业🤖AI智能体构建引擎,智能编排和调试,一键部署,支持私有化部署方案 广告
12-I/O ====== [I/O模块](#121-io%E6%A8%A1%E5%9D%97) [文件模块](#122-%E6%96%87%E4%BB%B6%E6%A8%A1%E5%9D%97) [路径模块](#123-%E8%B7%AF%E5%BE%84%E6%A8%A1%E5%9D%97) [进程和组长](#124-%E8%BF%9B%E7%A8%8B%E5%92%8C%E7%BB%84%E9%95%BF) [*iodata*和*chardata*](#125-iodata%E5%92%8Cchardata) 本章简单介绍Elixir的输入、输出机制以及相关的模块, 如[IO](http://elixir-lang.org/docs/stable/elixir/IO.html), [文件](http://elixir-lang.org/docs/stable/elixir/File.html) 和[路径](http://elixir-lang.org/docs/stable/elixir/Path.html)。 现在介绍I/O似乎有点早,但是I/O系统可以让我们一窥Elixir哲学,满足我们对该语言以及VM的好奇心。 ##12.1-IO模块 IO模块是Elixir语言中读写标准输入、输出、标准错误、文件、设备的主要机制。 使用该模块的方法颇为直接: ```elixir iex> IO.puts "hello world" "hello world" :ok iex> IO.gets "yes or no? " yes or no? yes "yes\n" ``` IO模块中的函数默认使用标准输入输出。 我们也可以传递```:stderr```来指示将错误信息写到标准错误设备上: ```elixir iex> IO.puts :stderr, "hello world" "hello world" :ok ``` #12.2-文件模块 文件模块包含了可以让我们读写文件的函数。 默认情况下文件是以二进制模式打开,它要求程序员使用特殊的```IO.binread/2``` 和```IO.binwrite/2```函数来读写文件: ```elixir iex> {:ok, file} = File.open "hello", [:write] {:ok, #PID<0.47.0>} iex> IO.binwrite file, "world" :ok iex> File.close file :ok iex> File.read "hello" {:ok, "world"} ``` 文件可以使用```:utf8```编码打开,然后就可以被IO模块中其他函数使用了: ```elixir iex> {:ok, file} = File.open "another", [:write, :utf8] {:ok, #PID<0.48.0>} ``` 除了打开、读写文件的函数,文件模块还有许多函数来操作文件系统。 这些函数根据Unix功能相对应的命令命名。 如```File.rm/1```用来删除文件;```File.mkdir/1``` 用来创建目录;```File.mkdir_p/1```创建目录并保证其父目录一并创建; 还有```File.cp_r/2```和```File.rm_rf/2```用来递归地复制和删除整个目录。 你还会注意到文件模块中,一般函数都有一个名称类似的版本。区别是名称上一个有!(bang)一个没有。 例如,上面的例子我们在读取“hello”文件时,用的是不带!号的版本。 下面用例子演示下它们的区别: ```elixir iex> File.read "hello" {:ok, "world"} iex> File.read! "hello" "world" iex> File.read "unknown" {:error, :enoent} iex> File.read! "unknown" ** (File.Error) could not read file unknown: no such file or directory ``` 注意看,当文件不存在时,带!号的版本会报错。就是说不带!号的版本能照顾到模式匹配出来的不同情况。 但有的时候,你就是希望文件在那儿,!使得它能报出有意义的错误。 因此,不要写: ```elixir {:ok, body} = File.read(file) ``` 相反地,应该这么写: ```elixir case File.read(file) do {:ok, body} -> # handle ok {:error, r} -> # handle error end ``` 或者 ```elixir File.read!(file) ``` ## 12.3-路径模块 文件模块中绝大多数函数都以路径作为参数。通常这些路径都是二进制,可以被路径模块提供的函数操作: ```elixir iex> Path.join("foo", "bar") "foo/bar" iex> Path.expand("~/hello") "/Users/jose/hello" ``` 有了以上介绍的几个模块和函数,我们已经能对文件系统进行基本的IO操作。 下面将讨论I/O模块中令人好奇的高级话题。这部分不是写Elixir程序必须掌握的,可以跳过不看。 但是如果你大概地浏览一下,可以了解IO是如何在VM上实现以及其它一些有趣的内容。 ## 12.4-进程和组长 你可能已经发现,```File.open/2```函数返回了一个包含PID的元祖: ```elixir iex> {:ok, file} = File.open "hello", [:write] {:ok, #PID<0.47.0>} ``` I/O模块实际上是同进程协同工作的。当你调用```IO.write(pid, binary)```时, I/O模块将发送一条消息给执行操作的进程。 让我们用自己的代码表述下这个过程: ```elixir iex> pid = spawn fn -> ...> receive do: (msg -> IO.inspect msg) ...> end #PID<0.57.0> iex> IO.write(pid, "hello") {:io_request, #PID<0.41.0>, #PID<0.57.0>, {:put_chars, :unicode, "hello"}} ** (ErlangError) erlang error: :terminated ``` 调用```IO.write/2```之后,可以看见打印出了发给IO模块的请求。 然而因为我们没有提供某些东西,这个请求失败了。 [StringIO模块](http://elixir-lang.org/docs/stable/elixir/StringIO.html) 提供了一个基于字符串的IO实现: ```elixir iex> {:ok, pid} = StringIO.open("hello") {:ok, #PID<0.43.0>} iex> IO.read(pid, 2) "he" ``` Erlang虚拟机用进程给I/O设备建模,允许同一个网络中的不同节点通过交换文件进程, 实现节点间的文件读写。在所有IO设备之中,有一个特殊的进程,称作组长(group leader)。 当你写东西到标准输出,实际上是发送了一条消息给组长,它把内容写给*STDIO文件表述者*: ```elixir iex> IO.puts :stdio, "hello" hello :ok iex> IO.puts Process.group_leader, "hello" hello :ok ``` 组长可为每个进程做相应配置,用于处理不同的情况。 例如,当在远程终端执行代码时,它能保证远程机器的消息可以被重定向到发起操作的终端上。 ## 12.5-*iodata*和*chardata* 在以上所有例子中,我们都用的是二进制/字符串方式读写文件。 在“二进制、字符串和字符列表”那章里,我们注意到字符串就是普通的bytes, 而字符列表是code points(字符码)的列表。 I/O模块和文件模块中的函数接受列表作为参数。 还可以接受混合类型的列表,里面内容是整形、二进制都行: ```elixir iex> IO.puts 'hello world' hello world :ok iex> IO.puts ['hello', ?\s, "world"] hello world :ok ``` 尽管如此,有些地方还是要注意。一个列表可能表示一串byte,或者一串字符。 用哪一种需要看I/O设备是怎么编码的。 如果不指明编码,文件就以raw模式打开,这时候只能用文件模块里bin*开头(二进制版) 的函数对其进行操作。 这些函数接受*iodata*作为参数,即,它们期待一个整数值的列表,用来表示byte或二进制。 尽管只是细微的差别,如果你打算传递列表给那些函数,你需要考虑那些细节。 底层的bytes可以表示二进制,这种表示就是raw的。