Spec-Zone.ru › Elixir 1.17

Источник Ввод-вывод и файловая система

В этом разделе рассматриваются механизмы ввода-вывода, задачи, связанные с файловой системой, и соответствующие модули, такие как IO, File и Path. Система ввода-вывода предоставляет отличную возможность узнать некоторые философские аспекты и особенности Elixir и виртуальной машины Erlang.

Модуль IO

Модуль IO является основным механизмом в Elixir для чтения и записи в стандартный ввод/вывод (:stdio), стандартную ошибку (:stderr), файлы и другие устройства ввода-вывода. Использование модуля довольно простое:

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.binread/2 и IO.binwrite/2 из модуля IO:

Предупреждение о потенциальной потере данных

Следующий код открывает файл для записи. Если файл с указанным путем уже существует, его содержимое будет удалено.

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"}

Файл можно открыть с опцией :append, вместо :write, чтобы сохранить его содержимое. Также можно передать опцию :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 прозрачно учитывает разные операционные системы. Наконец, помните, что Elixir автоматически преобразует косые черты (/) в обратные косые черты (\) в Windows при выполнении операций с файлами.

С этим мы рассмотрели основные модули, которые предоставляет Elixir для работы с вводом-выводом и взаимодействием с файловой системой. В следующем разделе мы немного заглянем под капот и узнаем, как реализована система ввода-вывода в виртуальной машине.

Процессы

Вы могли заметить, что File.open/2 возвращает кортеж, подобный {:ok, pid}:

iex> {:ok, file} = File.open("hello")
{: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 ожидал какой-либо результат, которого мы не предоставили.

Моделируя устройства ввода-вывода с помощью процессов, виртуальная машина Erlang позволяет даже читать и записывать файлы на разных узлах. Прекрасно!

iodata и chardata

Во всех примерах выше мы использовали двоичные данные при записи в файлы. Однако большинство функций ввода-вывода в Elixir также принимают «iodata» или «chardata».

Одной из основных причин использования «iodata» и «chardata» является производительность. Например, представьте, что вам нужно приветствовать кого-то в вашем приложении:

name = "Mary"
IO.puts("Hello " <> name <> "!")

Поскольку строки в Elixir, как и большинство структур данных, являются неизменяемыми, в примере выше строка «Mary» будет скопирована в новую строку «Hello Mary!». Хотя это маловероятно, что это будет иметь значение для короткой строки, как в данном примере, копирование может быть довольно дорогостоящим для длинных строк! По этой причине функции ввода-вывода в Elixir позволяют передавать вместо этого список строк:

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 целые числа представляют коды Юникода. Для символов ASCII числовое представление совпадает с кодом, поэтому оно подходит для обоих классификаций. Однако, по умолчанию устройство ввода-вывода работает с chardata, что означает, что мы можем сделать:

iex> IO.puts([?O, ?l, ?á, ?\s, "Mary", ?!])

В целом, целые числа в списке могут представлять либо набор байтов, либо набор символов, и выбор зависит от кодировки устройства ввода-вывода. Если файл открыт без кодировки, файл ожидается в сыром виде, и функции в модуле IO, начинающиеся с bin*, должны быть использованы. Эти функции ожидают iodata в качестве аргумента, где целые числа в списке будут представлять байты.

С другой стороны, устройство ввода-вывода по умолчанию (:stdio) и файлы, открытые с кодировкой :utf8, работают с оставшимися функциями в модуле IO. Эти функции ожидают chardata в качестве аргумента, где целые числа представляют коды символов.

Хотя это тонкое различие, вам нужно беспокоиться об этом только в том случае, если вы намерены передавать списки, содержащие целые числа, в эти функции. Если вы передаёте двоичные данные или список двоичных данных, то нет двусмысленности.

Наконец, есть ещё один конструктор — charlist, о котором мы говорили в предыдущих разделах. Charlists — это особый случай chardata, где все значения являются целыми числами, представляющими коды Юникода. Они могут быть созданы с помощью синтаксического элемента ~c:

iex> ~c"hello"
~c"hello"

Charlists в основном встречаются при взаимодействии с Erlang, так как некоторые API Erlang используют charlist в качестве представления строк. По этой причине любой список, содержащий удобочитаемые символы ASCII, будет напечатан как charlist:

iex> [?a, ?b, ?c]
~c"abc"

Мы упаковали много информации в этот небольшой раздел, поэтому давайте разберём его.

  • iodata и chardata — списки бинарных данных и целых чисел. Эти бинарные данные и целые числа могут быть произвольно вложены внутри списков. Их цель — обеспечить гибкость и производительность при работе с устройствами ввода-вывода и файлами;

  • выбор между iodata и chardata зависит от кодировки устройства ввода-вывода. Если файл открыт без кодировки, файл ожидает iodata, и необходимо использовать функции в модуле IO, начинающиеся с bin*. Стандартное устройство ввода-вывода (:stdio) и файлы, открытые с кодировкой :utf8, ожидают chardata и работают с оставшимися функциями в модуле IO;

  • charlists — это особый случай chardata, где используется исключительно список целых чисел — кодов Юникода. Их можно создать с помощью сигила ~c. Списка целых чисел автоматически выводятся с использованием сигила ~c, если все целые числа в списке представляют напечатаемые символы ASCII.

На этом заканчивается наше знакомство с устройствами ввода-вывода и функциональностью, связанной с вводом-выводом. Мы узнали о трех модулях Elixir — IO, File и Path — а также о том, как виртуальная машина использует процессы для базовых механизмов ввода-вывода и о том, как использовать chardata и iodata для операций ввода-вывода.

← Предыдущая страница Процессы
Следующая страница → alias, require, import и use

Скачать версию ePub

Создано с помощью ExDoc (v0.34.1) для языка программирования Elixir

© 2012-2024 The Elixir Team
Licensed under the Apache License, Version 2.0.
https://hexdocs.pm/elixir/1.17.2/io-and-the-file-system.html

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API