檢視原始碼 IO 和檔案系統
本章節介紹輸入/輸出機制、檔案系統相關任務,以及相關模組,例如 IO
、File
和 Path
。IO 系統提供了絕佳機會,可以深入探討 Elixir 和 Erlang VM 的一些哲學和好奇之處。
IO
模組
IO
模組是 Elixir 中用於讀取和寫入標準輸入/輸出 (:stdio
)、標準錯誤 (:stderr
)、檔案和其他 IO 裝置的主要機制。這個模組的使用相當簡單明瞭
iex> IO.puts("hello world")
hello world
:ok
iex> IO.gets("yes or no? ")
yes or no? yes
"yes\n"
預設情況下,IO
模組中的函式會從標準輸入讀取並寫入標準輸出。我們可以透過傳遞參數來變更,例如傳遞 :stderr
(用於寫入標準錯誤裝置)
iex> IO.puts(:stderr, "hello world")
hello world
:ok
File
模組
File
模組包含允許我們開啟檔案作為 IO 裝置的函式。預設情況下,檔案會以二進位模式開啟,這需要開發人員使用 IO
模組中的特定 IO.binread/2
和 IO.binwrite/2
函式
iex> {:ok, file} = File.open("path/to/file/hello", [:write])
{:ok, #PID<0.47.0>}
iex> IO.binwrite(file, "world")
:ok
iex> File.close(file)
:ok
iex> File.read("path/to/file/hello")
{:ok, "world"}
檔案也可以使用 :utf8
編碼開啟,這會指示 File
模組將從檔案讀取的位元組解譯為 UTF-8 編碼的位元組。
除了用於開啟、讀取和寫入檔案的功能外,File
模組還有許多用於處理檔案系統的功能。這些功能以其 UNIX 等效項命名。例如,File.rm/1
可用於移除檔案,File.mkdir/1
可用於建立目錄,File.mkdir_p/1
可用於建立目錄及其所有父鏈。甚至還有 File.cp_r/2
和 File.rm_rf/1
分別用於遞迴複製和移除檔案和目錄(也就是說,同時複製和移除目錄的內容)。
您還會注意到 File
模組中的功能有兩個變體:一個「常規」變體和另一個帶有尾隨驚嘆號 (!
) 的變體。例如,當我們在上面的範例中讀取 "hello"
檔案時,我們使用 File.read/1
。或者,我們可以使用 File.read!/1
iex> File.read("path/to/file/hello")
{:ok, "world"}
iex> File.read!("path/to/file/hello")
"world"
iex> File.read("path/to/file/unknown")
{:error, :enoent}
iex> File.read!("path/to/file/unknown")
** (File.Error) could not read file "path/to/file/unknown": no such file or directory
請注意,帶有 !
的版本會傳回檔案內容,而不是元組,而且如果發生任何問題,函式會引發錯誤。
當您想要使用模式比對處理不同的結果時,建議使用不帶 !
的版本
case File.read("path/to/file/hello") do
{:ok, body} -> # do something with the `body`
{:error, reason} -> # handle the error caused by `reason`
end
但是,如果您預期檔案存在,驚嘆號變體會更實用,因為它會引發有意義的錯誤訊息。避免撰寫
{:ok, body} = File.read("path/to/file/unknown")
因為如果發生錯誤,File.read/1
會傳回 {:error, reason}
,而且模式比對會失敗。您仍然會得到預期的結果(引發錯誤),但訊息會與不匹配的模式有關(因此對於錯誤實際涉及的內容來說是難以理解的)。
因此,如果您不想處理錯誤結果,請優先使用以驚嘆號結尾的函式,例如 File.read!/1
。
Path
模組
File
模組中的大多數函式都將路徑視為引數。最常見的情況是,這些路徑將是常規二進位檔。Path
模組提供處理此類路徑的工具
iex> Path.join("foo", "bar")
"foo/bar"
iex> Path.expand("~/hello")
"/Users/jose/hello"
使用 Path
模組中的函式,而不是直接操作字串,是比較好的選擇,因為 Path
模組會透明地處理不同的作業系統。最後,請記住,在 Windows 上執行檔案操作時,Elixir 會自動將斜線 (/
) 轉換為反斜線 (\
)。
有了這些,我們已經涵蓋了 Elixir 提供用於處理 IO 和與檔案系統互動的主要模組。在下一節中,我們將深入了解一下,並學習 IO 系統如何在 VM 中實作。
程序
你可能已經注意到 File.open/2
會傳回一個像 {:ok, pid}
iex> {:ok, file} = File.open("hello", [:write])
{:ok, #PID<0.47.0>}
這是因為 IO
模組實際上會使用程序(請參閱 前一章節)。假設一個檔案是一個程序,當你寫入一個已經關閉的檔案時,你實際上是向一個已經終止的程序傳送訊息
iex> File.close(file)
:ok
iex> IO.write(file, "is anybody out there")
** (ErlangError) Erlang error: :terminated:
* 1st argument: the device has terminated
(stdlib 5.0) io.erl:94: :io.put_chars(#PID<0.114.0>, "is anybody out there")
iex:4: (file)
讓我們更詳細地了解當你要求 IO.write(pid, binary)
時會發生什麼事。 IO
模組會向由 pid
識別的程序傳送訊息,並附上所需的作業。一個小型特設程序可以幫助我們看到它
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>, #Reference<0.0.8.91>,
{:put_chars, :unicode, "hello"}}
** (ErlangError) erlang error: :terminated
在 IO.write/2
之後,我們可以看到 IO
模組傳送的請求已列印出來(一個四元素元組)。在那之後不久,我們會看到它失敗,因為 IO
模組預期會有一些結果,但我們沒有提供。
透過使用程序對 IO 裝置進行建模,Erlang VM 甚至允許我們跨節點讀寫檔案。太棒了!
iodata
和 chardata
在以上所有範例中,我們在寫入檔案時都使用了二進位資料。但是,Elixir 中的大部分 IO 函式也接受「iodata」或「chardata」。
使用「iodata」和「chardata」的主要原因之一是為了效能。例如,假設你需要在你的應用程式中向某人打招呼
name = "Mary"
IO.puts("Hello " <> name <> "!")
由於 Elixir 中的字串是不可變的,就像大多數資料結構一樣,以上的範例會將字串「Mary」複製到新的「Hello Mary!」字串中。雖然這不太可能對像以上那樣的短字串造成影響,但複製對於大型字串來說可能會非常昂貴!因此,Elixir 中的 IO 函式允許你傳遞字串清單
name = "Mary"
IO.puts(["Hello ", name, "!"])
在以上的範例中,沒有複製。相反地,我們建立一個包含原始名稱的清單。我們將這樣的清單稱為「iodata」或「chardata」,我們很快就會了解它們之間的精確差異。
這些清單非常有用,因為它實際上可以簡化在各種情況下處理字串。例如,假設你有一個值清單,例如 ["apple", "banana", "lemon"]
,你想要寫入磁碟並以逗號分隔。你如何達成這一點?
一個選項是使用 Enum.join/2
並將值轉換為字串
iex> Enum.join(["apple", "banana", "lemon"], ",")
"apple,banana,lemon"
上述會透過將每個值複製到新字串中,傳回一個新字串。然而,透過本節中的知識,我們知道可以將字串清單傳遞給 IO/File 函式。因此,我們可以執行
iex> Enum.intersperse(["apple", "banana", "lemon"], ",")
["apple", ",", "banana", ",", "lemon"]
"iodata" 和 "chardata" 不僅包含字串,它們也可能包含任意巢狀的字串清單
iex> IO.puts(["apple", [",", "banana", [",", "lemon"]]])
"iodata" 和 "chardata" 也可能包含整數。例如,我們可以使用 ?,
作為分隔符號來列印逗號分隔的值清單,這是表示逗號的整數 (44
)
iex> IO.puts(["apple", ?,, "banana", ?,, "lemon"])
"iodata" 和 "chardata" 之間的差異正是該整數所表示的內容。對於 iodata,整數表示位元組。對於 chardata,整數表示 Unicode 碼點。對於 ASCII 字元,位元組表示法與碼點表示法相同,因此符合這兩種分類。然而,預設 IO 裝置會使用 chardata,這表示我們可以執行
iex> IO.puts([?O, ?l, ?á, ?\s, "Mary", ?!])
總之,清單中的整數可能表示一堆位元組或一堆字元,而使用哪一個取決於 IO 裝置的編碼。如果檔案是在沒有編碼的情況下開啟,則預期檔案處於原始模式,且必須使用 IO
模組中以 bin*
開頭的函式。這些函式預期 iodata
作為引數,其中清單中的整數將表示位元組。
另一方面,預設 IO 裝置 (:stdio
) 和使用 :utf8
編碼開啟的檔案會使用 IO
模組中其餘的函式。這些函式預期 chardata
作為引數,其中整數表示碼點。
雖然這是一個細微的差異,但只有當你打算將包含整數的清單傳遞給這些函式時,才需要擔心這些細節。如果你傳遞二進位檔案,或二進位檔案清單,則沒有歧義。
最後,有一個稱為 charlist 的最後建構,我們在前面章節中討論過。Charlist 是 chardata 的特殊情況,其中所有值都是代表 Unicode 編碼點的整數。它們可以用 ~c
標記建立
iex> ~c"hello"
~c"hello"
Charlist 大多出現在與 Erlang 介面時,因為一些 Erlang API 使用 charlist 作為其字串表示。因此,任何包含可列印 ASCII 編碼點的清單將會列印為 charlist
iex> [?a, ?b, ?c]
~c"abc"
我們將許多內容濃縮到這個小節,讓我們來分解它
iodata 和 chardata 是二進制和整數的清單。這些二進制和整數可以在清單中任意巢狀。它們的目標是在使用 IO 裝置和檔案時提供彈性和效能;
iodata 和 chardata 之間的選擇取決於 IO 裝置的編碼。如果檔案是在沒有編碼的情況下開啟的,則檔案會預期 iodata,而且必須使用
IO
模組中以bin*
開頭的函數。預設的 IO 裝置 (:stdio
) 和使用:utf8
編碼開啟的檔案會預期 chardata,並使用IO
模組中其餘的函數;charlist 是 chardata 的特殊情況,其中它專門使用 Unicode 編碼點的整數清單。它們可以用
~c
標記建立。如果清單中的所有整數都代表可列印的 ASCII 編碼點,則會使用~c
標記自動列印整數清單。
這結束了我們對 IO 裝置和 IO 相關功能的介紹。我們已經瞭解了三個 Elixir 模組 - IO
、File
和 Path
- 以及 VM 如何使用程序來執行底層 IO 機制,以及如何在 IO 操作中使用 chardata
和 iodata
。