Исходный код Бинарные данные, строки и списки символов
В разделе "Основные типы" мы немного узнали о строках и использовали функцию is_binary/1 для проверок:
iex> string = "hello" "hello" iex> is_binary(string) true
В этой главе мы более подробно разберём, что такое бинарные данные, как они связаны со строками и что означают значения в одинарных кавычках, 'like this', в Elixir. Хотя строки являются одним из самых распространённых типов данных в языках программирования, они могут быть скрыто сложными и часто неправильно понимаются. Чтобы понять строки в 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, обозначаемый синтаксисом <<>>/1. Битовая строка — это непрерывная последовательность битов в памяти.
По умолчанию для хранения каждого числа в битовой строке используется 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"
Вы всегда можете заставить IEx отображать списки символов в виде списков, вызвав функцию 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.16.3/binaries-strings-and-charlists.html