Исходный код Списки и кортежи
В этой главе мы изучим два наиболее часто используемых типа коллекций данных в Elixir: списки и кортежи.
(Связанные) списки
Elixir использует квадратные скобки для задания списка значений. Значения могут быть любого типа:
iex> [1, 2, true, 3] [1, 2, true, 3] iex> length([1, 2, 3]) 3
Два списка можно конкатенировать или вычесть, используя операторы ++ и -- соответственно:
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 и tl. Давайте присвоим список переменной и получим его голову и хвост:
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-чисел, он выводит его как список символов (буквально список символов). Список символов довольно часто используется при взаимодействии с существующим кодом Erlang. Всякий раз, когда вы видите значение в IEx и не совсем уверены, что это такое, вы можете использовать i для получения информации о нём:
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:
iex> tuple = {:ok, "hello"}
{:ok, "hello"}
iex> put_elem(tuple, 1, "world")
{:ok, "world"}
iex> tuple
{:ok, "hello"}
Обратите внимание, что put_elem вернул новый кортеж. Исходный кортеж, хранимый в переменной 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 разбивает строку на список строк на каждом пробеле. Поскольку количество возвращаемых элементов зависит от входных данных, мы используем список.
С другой стороны, String.split_at разбивает строку на две части в заданной позиции. Поскольку она всегда возвращает две записи, независимо от размера входных данных, она возвращает кортежи:
iex> String.split_at("hello world", 3)
{"hel", "lo world"}
iex> String.split_at("hello world", -4)
{"hello w", "orld"}
Также очень часто кортежи и атомы используются для создания «кортежей-меток», что является удобным значением возврата, когда операция может быть успешной или неудачной. Например, File.read читает содержимое файла по заданному пути, который может или не может существовать. Она возвращает кортежи-метки:
iex> File.read("path/to/existing/file")
{:ok, "... contents ..."}
iex> File.read("path/to/unknown/file")
{:error, :enoent}
Если путь, переданный File.read , существует, она возвращает кортеж с атомом :ok в качестве первого элемента и содержимым файла — во втором. В противном случае она возвращает кортеж с :error и описанием ошибки. Как мы вскоре узнаем, Elixir позволяет нам выполнять сопоставление с образцом по кортежам с метками и без труда обрабатывать случаи успеха и неудачи.
Поскольку Elixir последовательно следует этим правилам, выбор между списками и кортежами становится более понятным по мере изучения и использования языка. Elixir часто подсказывает вам, что делать правильно. Например, есть функция elem для доступа к элементу кортежа:
iex> tuple = {:ok, "hello"}
{:ok, "hello"}
iex> elem(tuple, 1)
"hello"
Однако, так как часто неизвестно количество элементов в списке, нет встроенного эквивалента для доступа к произвольным элементам в списках, кроме его головы.
Размер или длина?
При подсчёте элементов в структуре данных Elixir также придерживается простого правила: функция называется size , если операция выполняется за постоянное время (значение предварительно вычислено), или length , если операция линейная (вычисление длины замедляется по мере увеличения входных данных). В качестве мнемоники, и «длина», и «линейный» начинаются на «л».
Например, мы уже использовали 4 функции подсчёта: byte_size (для количества байтов в строке), tuple_size (для размера кортежа), length (для длины списка) и String.length (для количества графических символов в строке). Мы используем byte_size для получения количества байтов в строке, что является недорогой операцией. Получение количества символов Юникода, с другой стороны, использует String.length, и может быть дорогостоящим, так как зависит от прохода по всей строке.
Теперь, когда мы знакомы с основными типами данных языка, давайте изучим важные конструкции для написания кода, прежде чем мы обсудим более сложные структуры данных.
© 2012-2024 The Elixir Team
Licensed under the Apache License, Version 2.0.
https://hexdocs.pm/elixir/1.18.1/lists-and-tuples.html