Страница 52 из 53
В конечном счете родилась идея кодировки Unicode. Считайте, что это «всемирный набор символов». Увы, на практике все не так просто.
Возможно, вы слышали, что Unicode был (или остается) ограничен 65536 символами (именно столько различных комбинаций можно представить 16 битами). Распространенное заблуждение!.. При проектировании Unicode такие ограничения не закладывались. С самого начала было ясно, что во многих случаях это будет многобайтовая схема. Количество представимых с помощью Unicode символов практически безгранично, и это хорошо, так как 65000 никогда не хватит для всех языков мира.
Говоря об интернационализации, нужно прежде всего понимать, что интерпретация строки не является внутренне присущей самой строке. Это заблуждение проистекает из уже неактуального представления, будто существует лишь один способ хранения строки.
Подчеркну, это исключительно важное положение. Внутренне строка — всего лишь последовательность байтов. Представьте себе, что в памяти машины хранится один байт в кодировке ASCII. Если это буква, которую мы называем «прописная латинская А», то реально хранится число 65.
Почему мы считаем, что 65 — это А? Потому что так мы договорились использовать (интерпретировать) это значение. Если мы складываем его с другим числом, то оно используется (интерпретируется) как число. А если отправляем его на терминал по последовательной линии связи — значит, интерпретируем как ASCII-символ.
Если можно по-разному интерпретировать одиночный байт, то почему же нельзя так сделать для последовательности байтов? На самом деле, чтобы получилась осмысленная строка, предполагаемая схема интерпретации (или кодировка должна быть известна заранее. Кодировка — это просто соответствие между двоичными числами и символами. И снова не все так просто.
Поскольку Ruby появился в Японии, он прекрасно справляется с двумя различными японскими кодировками (и ASCII). Не буду тратить время на рассказ о поддержке японского языка; если вы японец, то в вашем распоряжении сколько угодно книг по Ruby на этом языке. А для всех остальных наиболее распространённой кодировкой является Unicode. О ней мы и будем говорить в этой главе.
Но перед тем как перейти к деталям, познакомимся с некоторыми терминами. Называть вещи полезными именами — одна из основ мудрости!
• Байт — это просто восемь битов (хотя когда-то даже это было неверно). По традиции многие считают, что байт соответствует одному символу. Ясно, что в контексте I18N это не так.
• Кодовая позиция — один элемент воображаемой таблицы, с помощью которой представляется набор символов. Хотя это и не совсем верно, можете считать, что кодовые позиции взаимно однозначно отображаются на символы. Точнее будет сказать, что иногда для уникального указания символа требуется несколько кодовых позиций.
• Глиф (печатный знак) — визуальное представление кодовой позиции. Хотя интуитивно это и не совсем очевидно, символ и его визуальное представление - разные вещи. (Я могу открыть текстовый редактор и набрать прописную А десятком разных шрифтов, но все это будет один и тот же символ А.)
• Понятие графемы близко к глифу, но о графемах мы говорим в контексте языка, а не программного обеспечения. Графема может быть комбинацией (простой или не очень) двух и более глифов. Так пользователь воспринимает символ в контексте своего родного языка. Разница настолько тонкая, что большинство программистов могут о ней никогда не задумываться.
Что же тогда такое символ? Даже в мире Unicode нет четкого понимания этого предмета, поскольку языки ведут себя по-разному, а программисты мыслят иначе, чем прочие люди. Будем говорить, что символ — это абстракция написания знака, который визуально может быть представлен одним или несколькими способами.
Перейдем к конкретике. Сначала я хочу познакомить вас с нотацией. Традиционно кодовые позиции Unicode записываются как U+, а затем четыре или более шестнадцатеричных цифр в верхнем регистре. То, что мы называем латинской буквой А, можно представить в виде U+0041.
Теперь возьмем букву é (строчная е с акутом). Ее можно представить в Unicode двумя способами. Во-первых, это одна кодовая позиция U+00E9 (СТРОЧНАЯ ЛАТИНСКАЯ Е С АКУТОМ). С другой стороны, это сочетание двух кодовых позиций: строчная е + диакритический знак акут — U+0065 и U+0301. Иными словами, СТРОЧНАЯ ЛАТИНСКАЯ Е, за которой следует АКУТ.
Обе формы одинаково правильны. Более короткая называется монолитной (precomposed) формой. Однако имейте в виду, что не для каждого языка имеются монолитные варианты, поэтому не всегда можно свести подобный символ к одной кодовой позиции.
Я назвал Unicode кодировкой, но это не вполне верно. Unicode отображает символы на кодовые позиции; существуют разные способы отобразить кодовые позиции на двоичное представление. По существу, Unicode — это семейство кодировок.
Возьмем, к примеру, строку "Matz". Она состоит из четырех кодовых позиции Unicode:
# U+004d U+0061 U+0074 U+007a
Естественнее всего сохранить их в виде простой последовательности байтов.
00 4d 00 61 00 74 00 7а
Такая кодировка называется UCS-2 (два байта) или UTF-16 (16 битов). Отметим, что эта кодировка имеет две разновидности: тупоконечную (big-endian) и остроконечную (little-endian) — в зависимости от того, старший или младший байт хранится первым.
Заметим, однако, что каждый второй байт в этой последовательности нулевой. Это не просто совпадение, английский язык редко выходит за пределы кодовой позиции U+00FF. Так разбрасываться памятью расточительно.
И это наблюдение подводит нас к идее кодировки UTF-8. В ней «традиционные» символы представлены одним байтом, а остальные — несколькими. Вот как записывается та же строка в кодировке UTF-8:
4d 61 74 7а
Мы всего лишь избавились от нулей. Однако более важен тот факт, что мы получили обычную кодировку ASCII. Так и задумано: «простой ASCII» можно считать собственным подмножеством UTF-8.
Отсюда, в частности, следует, что при интерпретации текста в кодировке UTF-8 как ASCII-текста он выглядит «как обычно» (особенно если это преимущественно англоязычный текст). Иногда вы видите, что браузер или другое приложение отображает английский текст правильно, но местами появляются «крокозябры». Это, скорее всего, означает, что программа сделала неверные предположения об используемой кодировке.
Итак, можно сказать, что UTF-8 экономит память. Конечно, я снова становлюсь на англоцентрическую точку зрения (по крайней мере, ASCII-центрическую). Если текст в основном состоит из ASCII-символов, то да, память экономится, но для других языков, например греческого или русского, размер строк даже увеличится.
Еще одно очевидное достоинство UTF-8 — «обратная совместимость» с кодировкой ASCII, которая, по-видимому, все еще остается самой распространенной однобайтовой кодировкой в мире. Наконец, у UTF-8 есть некоторые особенности, делающие ее удобной для программистов.
Во-первых, байты, входящие в состав многобайтовых символов, тщательно подобраны. Нулевой байт (ASCII 0) никогда не встречается в качестве n-ого байта в последовательности (где n > 1); то же самое справедливо для таких распространенных символов, как косая черта (обычно используется для разделения компонентов пути к файлу). На самом деле никакой байт из диапазона 0x00-0x7F не может быть частью никакого другого символа.
Второй байт многобайтового символа однозначно определяет, сколько байтов за ним следует. Этот второй байт всегда выбирается из диапазона от 0хС0 до 0хF0, а следующие за ним — из диапазона от 0x80 до 0xBF. Таким образом, схема кодирования свободна от состояния и позволяет восстанавливать пропущенные или искаженные байты.