Исходный код Списки и кортежи
В этой главе мы изучим два из наиболее часто используемых типов коллекций данных в Elixir: списки и кортежи.
(Связанные) Списки
Elixir использует квадратные скобки для определения списка значений. Значения могут быть любого типа:
iex> [1, 2, true, 3] [1, 2, true, 3] iex> length([1, 2, 3]) 3
Два списка можно конкатенировать или вычесть, используя операторы ++/2 и --/2 соответственно:
iex> [1, 2, 3] ++ [4, 5, 6] [1, 2, 3, 4, 5, 6] iex> [1, true, 2, false, 3, true] -- [true, false] [1, 2, 3, true]
Операторы со списками никогда не изменяют существующий список. Конкатенация или удаление элементов из списка возвращает новый список. Мы говорим, что структуры данных Elixir являются неизменяемыми. Одно из преимуществ неизменяемости — более понятный код. Вы можете свободно передавать данные, гарантируя, что никто их не изменит в памяти — только преобразует.
В течение всего руководства мы будем много говорить о голове и хвосте списка. Голова — это первый элемент списка, а хвост — оставшаяся часть списка. Их можно получить с помощью функций hd/1 и tl/1. Давайте присвоим список переменной и получим его голову и хвост:
iex> list = [1, 2, 3] iex> hd(list) 1 iex> tl(list) [2, 3]
Получение головы или хвоста пустого списка вызывает ошибку:
iex> hd([]) ** (ArgumentError) argument error
Иногда вы создадите список, который вернёт процитированное значение, предваряемое ~c. Например:
iex> [11, 12, 13] ~c"\v\f\r" iex> [104, 101, 108, 108, 111] ~c"hello"
Когда Elixir видит список печатных ASCII-чисел, Elixir выведет его как список символов (буквально список символов). Список символов довольно распространён при взаимодействии с существующим кодом Erlang. Всякий раз, когда вы видите значение в IEx и не совсем уверены, что это такое, вы можете использовать i/1, чтобы получить информацию о нём:
iex> i ~c"hello" Term i ~c"hello" Data type List Description ... Raw representation [104, 101, 108, 108, 111] Reference modules List Implemented protocols ...
Мы поговорим подробнее о списках символов в главе "Двоичные данные, строки и списки символов".
Строки в одинарных кавычках
В Elixir вы также можете использовать
'hello'для создания списков символов, но этот синтаксис устарел в Elixir v1.15 и будет генерировать предупреждения в будущих версиях. Предпочтите использовать~c"hello"вместо этого.
Кортежи
Elixir использует фигурные скобки для определения кортежей. Как и списки, кортежи могут хранить любые значения:
iex> {:ok, "hello"}
{:ok, "hello"}
iex> tuple_size({:ok, "hello"})
2
Кортежи хранят элементы непрерывно в памяти. Это означает, что доступ к элементу кортежа по индексу или получение размера кортежа — это быстрая операция. Индексы начинаются с нуля:
iex> tuple = {:ok, "hello"}
{:ok, "hello"}
iex> elem(tuple, 1)
"hello"
iex> tuple_size(tuple)
2
Также возможно поместить элемент в определённый индекс кортежа с помощью put_elem/3:
iex> tuple = {:ok, "hello"}
{:ok, "hello"}
iex> put_elem(tuple, 1, "world")
{:ok, "world"}
iex> tuple
{:ok, "hello"}
Обратите внимание, что put_elem/3 вернул новый кортеж. Исходный кортеж, хранившийся в переменной tuple, не был изменён. Как и списки, кортежи также неизменяемы. Каждая операция над кортежем возвращает новый кортеж, никогда не изменяя исходный.
Списки или кортежи?
В чём разница между списками и кортежами?
Списки хранятся в памяти как связанные списки, что означает, что каждый элемент в списке содержит своё значение и указывает на следующий элемент до тех пор, пока не будет достигнут конец списка. Это означает, что доступ к длине списка — это линейная операция: нам нужно пройти весь список, чтобы определить его размер.
Аналогично, производительность конкатенации списков зависит от длины левого списка:
iex> list = [1, 2, 3] [1, 2, 3] # This is fast as we only need to traverse `[0]` to prepend to `list` iex> [0] ++ list [0, 1, 2, 3] # This is slow as we need to traverse `list` to append 4 iex> list ++ [4] [1, 2, 3, 4]
Кортежи, с другой стороны, хранятся непрерывно в памяти. Это означает, что получение размера кортежа или доступ к элементу по индексу — быстрые операции. С другой стороны, обновление или добавление элементов в кортежи — дорогостоящая операция, так как требует создания нового кортежа в памяти:
iex> tuple = {:a, :b, :c, :d}
{:a, :b, :c, :d}
iex> put_elem(tuple, 2, :e)
{:a, :b, :e, :d}
Однако сами элементы не копируются. При обновлении кортежа все записи разделяются между старым и новым кортежем, за исключением записи, которая была заменена. Это правило применимо к большинству структур данных в Elixir. Это уменьшает количество выделения памяти, которое должен выполнять язык, и это возможно только благодаря неизменяемым свойствам языка.
Эти характеристики производительности определяют использование этих структур данных. Короче говоря, списки используются, когда количество возвращаемых элементов может изменяться. Кортежи имеют фиксированный размер. Давайте посмотрим на два примера из модуля String:
iex> String.split("hello world")
["hello", "world"]
iex> String.split("hello beautiful world")
["hello", "beautiful", "world"]
Функция String.split/1 разбивает строку на список строк на каждом пробеле. Поскольку количество возвращаемых элементов зависит от входных данных, мы используем список.
С другой стороны, String.split_at/2 разделяет строку на две части в заданной позиции. Поскольку она всегда возвращает две записи, независимо от размера входных данных, она возвращает кортежи:
iex> String.split_at("hello world", 3)
{"hel", "lo world"}
iex> String.split_at("hello world", -4)
{"hello w", "orld"}
Также очень часто используются кортежи и атомы для создания «меченых кортежей», что является удобным значением возврата, когда операция может быть успешной или неудачной. Например, File.read/1 считывает содержимое файла по заданному пути, который может или не может существовать. Он возвращает помеченные кортежи:
iex> File.read("path/to/existing/file")
{:ok, "... contents ..."}
iex> File.read("path/to/unknown/file")
{:error, :enoent}
Если путь, заданный для File.read/1, существует, он возвращает кортеж с атомом :ok в качестве первого элемента и содержимого файла как второго. В противном случае он возвращает кортеж с :error и описанием ошибки. Как мы вскоре узнаем, Elixir позволяет нам выполнять сопоставление с образцом меченых кортежей и без труда обрабатывать случаи успеха и неудачи.
Учитывая, что Elixir последовательно следует этим правилам, выбор между списками и кортежами становится более понятным по мере изучения и использования языка. Elixir часто подводит вас к правильному решению. Например, есть функция elem/2 для доступа к элементу кортежа:
iex> tuple = {:ok, "hello"}
{:ok, "hello"}
iex> elem(tuple, 1)
"hello"
Однако, учитывая, что вы часто не знаете количество элементов в списке, нет встроенного эквивалента для доступа к произвольным записям в списке, за исключением его головы.
Размер или длина?
При подсчёте элементов в структуре данных Elixir также следует простому правилу: функция называется size , если операция выполняется за постоянное время (значение предварительно вычислено), или length , если операция линейная (вычисление длины замедляется по мере увеличения входных данных). В качестве мнемонической подсказки, и "длина", и "линейный" начинаются с "л".
Например, до сих пор мы использовали 4 функции подсчёта: byte_size/1 (для количества байтов в строке), tuple_size/1 (для размера кортежа), length/1 (для длины списка) и String.length/1 (для количества графем в строке). Мы используем byte_size для получения количества байтов в строке, что является быстрой операцией. Получение количества символов Юникода, с другой стороны, использует String.length/1 и может быть дорогостоящим, так как опирается на обход всей строки.
Теперь, когда мы знакомы с базовыми типами данных языка, давайте изучим важные конструкции для написания кода, прежде чем обсудим более сложные структуры данных.
© 2012-2024 The Elixir Team
Licensed under the Apache License, Version 2.0.
https://hexdocs.pm/elixir/1.17.2/lists-and-tuples.html