Source Списки и кортежи
В этой главе мы изучим два наиболее часто используемых типа коллекций данных в 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/2 разбивает строку на список строк на каждом пробеле. Поскольку количество возвращаемых элементов зависит от входных данных, мы используем список.
С другой стороны, 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.16.3/lists-and-tuples.html