Двоичные данные, строки и списки символов

В «Базовых типах» мы познакомились со строками и использовали функцию is_binary/1 для проверок:

iex> string = "hello"
"hello"

iex> is_binary(string)
true

В этой главе мы узнаем, что такое двоичные данные, как они связаны со строками, и что в Эликсире представляют собой значения в одинарных кавычках 'like this'.

UTF-8 и Юникод

Строка в UTF-8 представляет собой бинарную последовательность. Для понимания, что мы подразумеваем, следует понять разницу между байтами и кодовыми обозначениями.

Стандарт Юникод связывает кодовые обозначения со многими известными символами. Например, латинская буква a имеет кодовое обозначение 97, тогда как буква ł имеет код 322. Чтобы записать строку "hełło" на диск, нужно конвертировать эти коды в байты. Если мы примем за правило, что один байт соответствует одному кодовому обозначению, мы не сможем записать строку "hełło", потому что она использует код 322 для ł, а один байт может представлять число от 0 до 255. Но, раз мы можем прочитать "hełło" на экране, есть какой-то способ записать такую строку. Это место, где начинает работать кодировка.

Для представления кодовых обозначений в виде байт, нужно как-то их закодировать. Эликсир выбрал кодировку UTF-8 своей главной и стандартной кодировкой. Когда мы говорим, что UTF-8 закодирован в двоичном виде, мы имеем в виду, что строка – это набор байт, представляющих некоторые кодовые обозначения, в соответствии с правилами кодировки UTF-8.

Т. к. у нас есть символы вроде ł, соответствующий коду 322, нам нужно больше одного байта для их представления. Поэтому мы видим разницу при вычислении byte_size/1 в сравнении с результатом String.length/1:

iex> string = "hełło"
"hełło"

iex> byte_size(string)
7

iex> String.length(string)
5

Так, byte_size/1 считает количество байт, необходимых для представления строки, а String.length/1 считает символы.

Обратите внимание. Tсли вы работаете под Виндоус, есть шанс, что ваш терминал не использует UTF-8 по умолчанию. Вы можете изменить кодировку текущей сессии, выполнив chcp 65001 перед запуском IEx (iex.bat).

UTF-8 нужен один байт для представления символов h, e, и o, но два байта для представления ł. В Эликсире вы можете узнать код символа с помощью ?:

iex> ?a
97

iex> 
322

Вы можете также использовать функции из модуля String для разделения строки на индивидуальные символы, каждый из которых будет строкой длины 1:

iex> String.codepoints("hełło")
["h", "e", "ł", "ł", "o"]

Вы увидите, что Эликсир имеет отличную поддержку работы со строками. Он также поддерживает многие операции Юникода. Фактически, Эликсир успешно проходит все тесты, показанные в статье «The string type is broken».

Однако, строки – это только часть истории. Раз строки являются бинарными, и мы использовали функцию is_binary/1, Эликсир должен иметь более мощный тип, лежащий в основе строк. И он есть! Поговорим о бинарных данных.

Бинарные данные (и битовые строки)

В Эликсире вы можете определить бинарную последовательность, используя <<>>:

iex> <<0, 1, 2, 3>>
<<0, 1, 2, 3>>
iex> byte_size(<<0, 1, 2, 3>>)
4

Бинарные данные – это последовательность байт. Эти байты могут быть записаны в любом порядке, даже если эта последовательность не будет корректной строкой:

iex> String.valid?(<<239, 191, 191>>)
false

Оператор конкатенации строк на самом деле оператор бинарной конкатенации:

iex> <<0, 1>> <> <<2, 3>>
<<0, 1, 2, 3>>

Часто используемый трюк в Эликсире – конкатенация нулевого байта <<0>> к строке, чтобы увидеть её бинарное представление:

iex> "hełło" <> <<0>>
<<104, 101, 197, 130, 197, 130, 111, 0>>

Каждое число в бинарной последовательности должно помещаться в один байт, а значит, быть не больше 255. Существую модификаторы для хранения числе больше, чем 255 или конвертации кодовых обозначений в их UTF-8 представления:

iex> <<255>>
<<255>>

iex> <<256>> # обрезается
<<0>>

iex> <<256 :: size(16)>> # используется 16 бит (2 байта) для хранения числа
<<1, 0>>

iex> <<256 :: utf8>> # число в виде кодового обозначения
"Ā"
iex> <<256 :: utf8, 0>>
<<196, 128, 0>>

Если в байте 8 бит, что произойдёт, если мы укажем размер в 1 бит?

iex> <<1 :: size(1)>>
<<1::size(1)>>

iex> <<2 :: size(1)>> # обрезается
<<0::size(1)>>

iex> is_binary(<<1 :: size(1)>>)
false

iex> is_bitstring(<<1 :: size(1)>>)
true

iex> bit_size(<< 1 :: size(1)>>)
1

Значение больше не бинарное, но является битовой строкой – набором битов! Таким образом бинарная последовательность – это битовая строка, количество бит в которой делится на 8.

iex>  is_binary(<<1 :: size(16)>>)
true

iex>  is_binary(<<1 :: size(15)>>)
false

Мы можем также сравнивать по шаблону бинарные последовательности и битовые строки:

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>>

Тот же результат может быть достигнут с помощью оператора конкатенации <>:

iex> "he" <> rest = "hello"
"hello"

iex> rest
"llo"

Полную справку о конструкторе <<>> бинарных последовательностей / битовых строк можно найти в документации модулей Эликсира. На этом мы закончим обзор битовых строк, бинарных последовательностей и строк. Строка – это бинарная последовательность в кодировке UTF-8, а бинарная последовательность – битовая строка, число бит в которой делится на 8. Хотя это показывает гибкость, которую даёт Эликсир для работы с битами и байтами, 99% времени вы будете работать с бинарными последовательностями, используя функции is_binary/1 и byte_size/1.

Списки символов

Списки символов – не что иное, как списки из кодовых обозначений. Списки символов можно создать с помощью литералов в одиночных кавычках:

iex> 'hełło'
[104, 101, 322, 322, 111]

iex> is_list 'hełło'
true

iex> 'hello'
'hello'

iex> List.first('hello')
104

Вы можете увидеть, что список символов хранит не последовательность байт, а кодовые обозначения символов, указанных в одинарных кавычках (помните, что IEx по умолчанию будет выводить только коды, если хотя бы один из них не входит в диапазон ASCII). Таким образом двойные кавычки используются для строк (внутри это бинарная последовательность), одиночные – для списков символов (внутри список).

На практике, списки символов чаще всего используются для взаимодействия с Эрлангом, в частности со старыми библиотеками, которые не принимают бинарные последовательности в качестве аргументов. Вы можете конвертировать список символов в строку и обратно, используя функции: to_string/1 и to_charlist/1:

iex> to_charlist "hełło"
[104, 101, 322, 322, 111]

iex> to_string 'hełło'
"hełło"

iex> to_string :hello
"hello"

iex> to_string 1
"1"

Обратите внимание, что эти функции полиморфичны. Они могут конвертировать в строки не только списки символов, но и числа, атомы и т. д.

На это всё с бинарными последовательностями, строками и списками символов. Самое время поговорить о структурах данных, основанных на парах ключ-значение.

© 2020 / Россия Любые мысли и вопросы пишите на elixir@wunsh.ru.