Перекодировать текст из UTF-16 ↓
Задайте программисту на JavaScript вопрос, сколько символов в строке из одного-единственного смайлика, и правильный ответ его, скорее всего, удивит: язык сообщит, что длина такой строки равна двум. Это не ошибка и не шутка разработчиков, а прямое следствие того, что строки в JavaScript устроены по правилам UTF-16. С этой кодировкой ежедневно имеет дело почти каждый владелец компьютера или смартфона, хотя мало кто видит её своими глазами: она редко встречается в файлах и на сайтах, зато работает в оперативной памяти множества программ.
История UTF-16 началась с красивой, но ошибочной идеи. Создатели Юникода на рубеже 1980-х и 1990-х годов полагали, что для всех живых письменностей планеты хватит 65 536 кодовых позиций, то есть ровно столько, сколько помещается в шестнадцать бит. Отсюда родилась простая схема под названием UCS-2: любой знак занимает два байта, никаких исключений. По сравнению с зоопарком старых таблиц вроде Windows-1251, KOI8-R или Shift_JIS это выглядело настоящим прорывом, ведь у каждого символа появился единственный и постоянный номер.
Довольно быстро выяснилось, что места не хватает. Одних только китайских, японских и корейских иероглифов набралось больше, чем предполагалось, а ведь были ещё исторические письменности, математические и музыкальные знаки, а позднее и эмодзи. В 1996 году Юникод расширили до семнадцати так называемых плоскостей, по 65 536 позиций в каждой. Но к тому времени на двухбайтовую схему уже успели опереться крупные программные платформы, и ломать совместимость никто не хотел. Нужен был способ втиснуть новые символы в старый шестнадцатибитный формат.
Так появился механизм суррогатных пар, который и превратил UCS-2 в UTF-16. В первой, базовой плоскости были зарезервированы два диапазона по 1024 значения, которым не соответствуют никакие реальные знаки. Символ из дополнительных плоскостей записывается двумя шестнадцатибитными единицами: первая берётся из одного диапазона, вторая из другого. Комбинаций получается чуть больше миллиона, и именно этим числом сегодня ограничен весь Юникод. Привычные буквы, включая латиницу, кириллицу и большинство иероглифов, по-прежнему занимают одну единицу, а редкие знаки и смайлики требуют двух. Вот почему смайлик в JavaScript "весит" два символа.
У шестнадцатибитного формата есть деликатная проблема, которой лишены однобайтовые кодировки: два байта можно записать в разном порядке. Одни процессоры ставят старший байт первым, другие последним, поэтому существуют два варианта, UTF-16BE и UTF-16LE. Чтобы читающая программа не гадала, в начало файла обычно помещают специальную метку порядка байтов, известную как BOM. Если метка потеряна, а порядок угадан неверно, текст превращается в набор случайных иероглифов и значков, никак не связанных с исходным содержанием.
Где же применяется UTF-16? Прежде всего в Windows: начиная с линейки NT системные функции принимают строки именно в этом виде, а пункт "Юникод" в старых версиях Блокнота означал как раз UTF-16LE. На том же формате построены строки в Java, C#, JavaScript, в библиотеке Qt и в программных интерфейсах Apple. Родственником кодировки пользуются даже мобильные сети: SMS с русским текстом передаётся в UCS-2, и поэтому в одно сообщение помещается всего 70 знаков вместо 160 латинских.
А вот за пределами оперативной памяти UTF-16 почти не встречается, и на то есть веские причины. Главная из них состоит в несовместимости с ASCII. Английская буква в UTF-16 сопровождается нулевым байтом, а нулевой байт во множестве старых программ и сетевых протоколов означает конец строки. Файл в такой кодировке, открытый как обычный однобайтовый текст, выглядит так, будто между буквами расставлены пробелы или пустые квадратики. UTF-8 этой беды избежала: латиница в ней записывается точно так же, как в ASCII, и потому именно она завоевала интернет, электронную почту и мир Unix.
Если сравнивать расход памяти, картина получается неоднозначной. Русский текст занимает в UTF-16 по два байта на букву, столько же, сколько в UTF-8, и вдвое больше, чем в Windows-1251. Английский текст и программный код в UTF-16 вдвое объёмнее, чем в UTF-8. Зато китайский или японский выигрывает: иероглиф требует двух байтов против трёх. Что касается UTF-32, где любой знак занимает четыре байта, то она проще в обработке, но слишком расточительна и применяется редко. Получается, что UTF-16 занимает промежуточное положение и унаследовала как достоинства, так и неудобства обоих соседей.
Главное из этих неудобств знакомо каждому разработчику. Поскольку длина символа непостоянна, строку нельзя безопасно резать в произвольном месте: если ножницы попадут между половинками суррогатной пары, на экране появится знак замены. Ошибки такого рода годами остаются незамеченными, потому что проявляются только на редких символах. С распространением эмодзи они стали всплывать гораздо чаще, и сегодня аккуратная работа с суррогатными парами считается обязательным требованием к любому текстовому полю.
Для обычного пользователя практический вывод прост. Если файл открылся с пропусками между буквами или вовсе в виде иероглифов, вполне возможно, что он сохранён в UTF-16, например выгружен из какой-нибудь программы для Windows. Достаточно перекодировать его в UTF-8, и текст примет нормальный вид. А сама UTF-16 останется там, где ей и место: в глубине операционных систем и языков программирования, где она незаметно выполняет свою работу уже не первое десятилетие.