Исходный код Бинарные данные, строки и списки символов
В разделе "Основные типы" мы немного узнали о строках, и использовали функцию is_binary/1 для проверок:
iex> string = "hello" "hello" iex> is_binary(string) true
В этой главе мы разберёмся, что такое бинарные данные и как они связаны со строками. Мы также узнаем о списках символов, ~c"like this", которые часто используются для взаимодействия с Erlang.
Хотя строки являются одним из самых распространённых типов данных в языках программирования, они имеют скрытую сложность и часто неправильно понимаются. Чтобы понять строки в Elixir, сначала давайте обсудим Unicode и кодировки символов, в частности кодировку UTF-8.
Unicode и Кодовые точки
Для обеспечения эффективного общения между компьютерами на разных языках требуется стандарт, чтобы единицы и нули на одном компьютере имели одинаковое значение при передаче на другой. Стандарт Unicode является официальным реестром практически всех известных символов: это включает символы из классических и исторических текстов, смайлики, а также символы форматирования и управления.
Unicode организует все символы в таблицы кодов, и каждому символу присваивается уникальный числовой индекс. Этот числовой индекс называется кодовой точкой.
В Elixir вы можете использовать ? перед символом, чтобы увидеть его кодовую точку:
iex> ?a 97 iex> ?ł 322
Обратите внимание, что большинство таблиц кодов Unicode будут ссылаться на кодовую точку через шестнадцатеричное (hex) представление, например, 97 соответствует 0061 в шестнадцатеричном формате, и мы можем представить любой символ Unicode в строке Elixir, используя обозначение \uXXXX и шестнадцатеричное представление его кодовой точки:
iex> "\u0061" == "a" true iex> 0x0061 = 97 = ?a 97
Шестнадцатеричное представление также поможет вам найти информацию о кодовой точке, например, https://codepoints.net/U+0061 содержит подробную информацию о строчной букве a, также известной как кодовая точка 97.
UTF-8 и Кодировки
Теперь, когда мы понимаем, что такое стандарт Unicode и что такое кодовые точки, мы можем наконец поговорить о кодировках. Кодовая точка отвечает на вопрос «что» мы храним, а кодировка отвечает на вопрос «как» мы это храним: кодировка — это реализация. Другими словами, нам нужен механизм преобразования числовых кодов в байты, чтобы они могли храниться в памяти, записываться на диск и т.д.
Elixir использует UTF-8 для кодирования своих строк, что означает, что кодовые точки кодируются как последовательность байтов по 8 бит. UTF-8 — это кодировка символов с переменной длиной, которая использует от одного до четырёх байтов для хранения каждой кодовой точки. Она способна кодировать все допустимые кодовые точки Unicode. Посмотрим на пример:
iex> string = "héllo" "héllo" iex> String.length(string) 5 iex> byte_size(string) 6
Хотя в строке выше 5 символов, она использует 6 байтов, так как для представления символа é используются два байта.
Примечание: если вы работаете в Windows, есть вероятность, что ваша консоль по умолчанию не использует UTF-8. Вы можете изменить кодировку текущего сеанса, выполнив
chcp 65001перед вводомiex(iex.bat).
Помимо определения символов, UTF-8 также предоставляет понятие графем. Графемы могут состоять из нескольких символов, которые часто воспринимаются как один. Например, смайлик женщины-пожарного женщина-пожарник представлен сочетанием трёх символов: смайлика женщины (👩), скрытого символа нулевой ширины и смайлика пожарной машины (🚒):
iex> String.codepoints("👩🚒")
["👩", "", "🚒"]
iex> String.graphemes("👩🚒")
["👩🚒"]
Однако Elixir достаточно умен, чтобы понимать, что они воспринимаются как один символ, поэтому длина по-прежнему равна одному:
iex> String.length("👩🚒")
1
Примечание: если вы не видите смайлик выше в своей консоли, убедитесь, что ваша консоль поддерживает смайлики и что вы используете шрифт, который может их отобразить.
Хотя эти правила могут показаться сложными, кодированные в UTF-8 документы встречаются повсюду. Эта страница также закодирована в UTF-8. Информация о кодировке передаётся браузеру, который затем знает, как отобразить все байты, символы и графемы соответственно.
Если вы хотите увидеть точные байты, в которые хранится строка в файле, распространённый способ — это конкатенация нулевого байта <<0>> к ней:
iex> "hełło" <> <<0>> <<104, 101, 197, 130, 197, 130, 111, 0>>
В качестве альтернативы вы можете посмотреть бинарное представление строки, используя IO.inspect/2:
iex> IO.inspect("hełło", binaries: :as_binaries)
<<104, 101, 197, 130, 197, 130, 111>>
Мы немного забегаем вперёд. Давайте поговорим о битовых строках, чтобы понять, что означает конструктор <<>>.
Битовые строки
Хотя мы рассмотрели кодовые точки и кодировку UTF-8, нам всё ещё нужно углубиться в то, как именно мы храним закодированные байты, и здесь мы представляем битовую строку. Битовая строка — это фундаментальный тип данных в Elixir, обозначаемый синтаксисом <<>>. Битовая строка — это непрерывная последовательность бит в памяти.
По умолчанию для хранения каждого числа в битовой строке используется 8 бит (т.е. 1 байт), но вы можете вручную указать количество бит с помощью модификатора ::n для обозначения размера в n битах, или вы можете использовать более подробное объявление ::size(n):
iex> <<42>> == <<42::8>> true iex> <<3::4>> <<3::size(4)>>
Например, десятичное число 3 при представлении с 4 битами в двоичной системе будет 0011, что эквивалентно значениям 0, 0, 1, 1, каждое из которых хранится с использованием 1 бита:
iex> <<0::1, 0::1, 1::1, 1::1>> == <<3::4>> true
Любое значение, превышающее то, что может быть сохранено заданным количеством бит, усекается:
iex> <<1>> == <<257>> true
Здесь 257 в двоичной системе будет представлено как 100000001, но так как мы зарезервировали только 8 бит для его представления (по умолчанию), наиболее значимый бит игнорируется, и значение усекается до 00000001, или просто 1 в десятичной системе.
Полную справку по конструктору битовой строки можно найти в документации к <<>>.
Бинарные данные
Бинарные данные — это битовая строка, где количество бит делится на 8. Это означает, что каждый бинарный тип данных является битовой строкой, но не каждая битовая строка — это бинарные данные. Мы можем использовать функции is_bitstring/1 и is_binary/1 для демонстрации этого.
iex> is_bitstring(<<3::4>>) true iex> is_binary(<<3::4>>) false iex> is_bitstring(<<0, 255, 42>>) true iex> is_binary(<<0, 255, 42>>) true iex> is_binary(<<42::16>>) true
Мы можем использовать шаблонное сопоставление с бинарными / битовыми строками:
iex> <<0, 1, x>> = <<0, 1, 2>> <<0, 1, 2>> iex> x 2 iex> <<0, 1, x>> = <<0, 1, 2, 3>> ** (MatchError) no match of right hand side value: <<0, 1, 2, 3>>
Обратите внимание, что если вы не используете явно модификаторы ::, каждый элемент в бинарном шаблоне должен совпадать с одним байтом (точно 8 битами). Если мы хотим сопоставить бинарные данные неизвестного размера, мы можем использовать модификатор binary в конце шаблона:
iex> <<0, 1, x::binary>> = <<0, 1, 2, 3>> <<0, 1, 2, 3>> iex> x <<2, 3>>
Есть ещё несколько модификаторов, которые могут быть полезны при работе с шаблонами бинарных данных. Модификатор binary-size(n) сопоставит n байтов в бинарных данных:
iex> <<head::binary-size(2), rest::binary>> = <<0, 1, 2, 3>> <<0, 1, 2, 3>> iex> head <<0, 1>> iex> rest <<2, 3>>
Строка — это бинарные данные, закодированные в UTF-8, где кодовая точка каждого символа кодируется от 1 до 4 байтов. Таким образом, каждая строка является бинарными данными, но из-за правил кодирования UTF-8 не все бинарные данные являются допустимыми строками.
iex> is_binary("hello")
true
iex> is_binary(<<239, 191, 19>>)
true
iex> String.valid?(<<239, 191, 19>>)
false
Оператор конкатенации строк <> на самом деле является оператором конкатенации бинарных данных:
iex> "a" <> "ha" "aha" iex> <<0, 1>> <> <<2, 3>> <<0, 1, 2, 3>>
Поскольку строки являются бинарными данными, мы также можем использовать шаблонное сопоставление со строками:
iex> <<head, rest::binary>> = "banana" "banana" iex> head == ?b true iex> rest "anana"
Однако помните, что бинарное шаблонное сопоставление работает с байтами, поэтому сопоставление со строкой, например, «über» с символами с несколькими байтами, не будет сопоставлять символ, оно будет сопоставлять первый байт этого символа:
iex> "ü" <> <<0>> <<195, 188, 0>> iex> <<x, rest::binary>> = "über" "über" iex> x == ?ü false iex> rest <<188, 98, 101, 114>>
Выше, x сопоставился только с первым байтом многобайтового ü символа.
Поэтому при шаблоне сопоставления со строками важно использовать модификатор utf8:
iex> <<x::utf8, rest::binary>> = "über" "über" iex> x == ?ü true iex> rest "ber"
Списки символов
Наш обзор битовых строк, бинарных данных и строк почти завершён, но у нас есть ещё один тип данных, который необходимо объяснить: список символов.
Список символов — это список целых чисел, где все целые числа являются допустимыми кодовыми точками. На практике вы с ними не часто столкнётесь, только в определённых сценариях, таких как взаимодействие со старыми библиотеками Erlang, которые не принимают бинарные данные в качестве аргументов.
iex> ~c"hello" ~c"hello" iex> [?h, ?e, ?l, ?l, ?o] ~c"hello"
Сигнал ~c (мы рассмотрим сигналы позже в главе "Сигналы") указывает на то, что мы имеем дело со списком символов, а не с обычной строкой.
Вместо байтов список символов содержит целые кодовые точки. Однако список отображается только как сигнал, если все кодовые точки находятся в диапазоне ASCII:
iex> ~c"hełło" [104, 101, 322, 322, 111] iex> is_list(~c"hełło") true
Это сделано для облегчения взаимодействия с Erlang, хотя это может привести к некоторому неожиданному поведению. Например, если вы храните список целых чисел, которые случаются в диапазоне от 0 до 127, по умолчанию IEx интерпретирует это как список символов и отобразит соответствующие символы ASCII.
iex> heartbeats_per_minute = [99, 97, 116] ~c"cat"
Вы всегда можете принудительно вывести список символов в его представлении в виде списка, вызвав функцию inspect/2:
iex> inspect(heartbeats_per_minute, charlists: :as_list) "[99, 97, 116]"
Кроме того, вы можете преобразовать список символов в строку и обратно, используя функции to_string/1 и to_charlist/1:
iex> to_charlist("hełło")
[104, 101, 322, 322, 111]
iex> to_string(~c"hełło")
"hełło"
iex> to_string(:hello)
"hello"
iex> to_string(1)
"1"
Перечисленные выше функции полиморфны, то есть они принимают множество форм: они не только преобразуют списки символов в строки (и наоборот), но также могут преобразовывать целые числа, атомы и так далее.
Конкатенация строк (бинарных данных) использует оператор <>, а конкатенация списков символов, являясь списками, использует оператор конкатенации списков ++:
iex> ~c"this " <> ~c"fails"
** (ArgumentError) expected binary argument in <> operator but got: ~c"this "
(elixir) lib/kernel.ex:1821: Kernel.wrap_concatenation/3
(elixir) lib/kernel.ex:1808: Kernel.extract_concatenations/2
(elixir) expanding macro: Kernel.<>/2
iex:1: (file)
iex> ~c"this " ++ ~c"works"
~c"this works"
iex> "he" ++ "llo"
** (ArgumentError) argument error
:erlang.++("he", "llo")
iex> "he" <> "llo"
"hello"
После того как мы разобрались с бинарными данными, строками и списками символов, пришло время поговорить об основных структурах данных для хранения пар «ключ-значение».
© 2012-2024 The Elixir Team
Licensed under the Apache License, Version 2.0.
https://hexdocs.pm/elixir/1.18.1/binaries-strings-and-charlists.html