Spec-Zone.ru › Elixir 1.18

Исходный код Бинарные данные, строки и списки символов

В разделе "Основные типы" мы немного узнали о строках, и использовали функцию 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"

После того как мы разобрались с бинарными данными, строками и списками символов, пришло время поговорить об основных структурах данных для хранения пар «ключ-значение».

← Предыдущая страница Анонимные функции
Следующая страница → Списки ключевых слов и карты

Скачать версию ePub

Создано с помощью ExDoc (v0.36.1) для программного языка Elixir

© 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

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API