Уже давно прошли те времена, когда текстовые строки в языках программирования были исключительно байтовыми без поддержки символов национальных алфавитов, а в некоторых случаях еще и ограничены размером не более 255 символов. В настоящее время наоборот, сложно найти такой язык программирования, который НЕ "поддерживает" юникод в текстовых строках.
Если вы обратили внимание, то слово "поддерживает" взято в кавычки и как говорил Винипух, это жжж не спроста, ведь с появлением Unicode понятие "символ" в текстовых строках стало не совсем однозначным.
Есть старая статья о проблемах поддержки Unicode в разных языках программирования: The importance of language-level abstract Unicode strings Matt Giuca
Основной смысл которой сводится к тому, чтобы призвать разработчиков языков программирования абстрагироваться от схем кодирования Unicode (доступом к отдельным байтам), и оставить для программистов только возможность работы с последовательностью символов, чтобы предотвратить большинство ошибок Unicode, так как с приходом эры Unicode изменилось само понятие символа и текстовой строки!
Консорциум Unicode предоставил нам замечательный стандарт для представления и передачи символов из всех письменностей мира, но большинство современных языков без необходимости раскрывают детали того, как кодируются символы. Это означает, что все программисты должны стать экспертами по Unicode, чтобы создавать высококачественное интернационализированное программное обеспечение.
...
Языки следующего поколения должны предоставлять только строковые операции, ориентированные на символы (кроме случаев, когда программист явно запрашивает кодировку текста). Тогда остальные из нас смогут вернуться к программированию, вместо того, чтобы беспокоиться о проблемах кодирования (строк в Unicode).
Терминология
code point - это примерно то же, что мы привыкли называть символом. Но не совсем. Например, буква «ё» может быть как одним code point'ом, так и двумя - буквой «е» и символом "две точки над предыдущей буквой".
code unit - это единицы кодировки (utf-8, utf-16 или utf-32)
Соответственно с приходом Unicode появились и следующие проблемы:
У текстовых строк могут быть разные размеры кодовой единицы (UTF-8: кодовая единица = 8 бит или 1 байт, UTF-16: кодовая единица = 16 бит или 2 байта. UTF-32: кодовая единица = 32 бита или 4 байта)
Имеем разное количество байт на один code point
Некоторые символы можно закодировать разным количеством code point, например,
е + ̈ == ̈ё
. Это увеличивает размер данных, но добавляет только один символ.Проблемы с индексацией строк (по байтно или по символьно). Доступ к элементу символьной строки Unicode стал не O(1) как у массива, а O(n) так как приходится сканировать строку для подсчета количества Unicode символов.
Требуется проверка корректности данных строки при сериализации/десериализации (контроль ошибок преобразования кодировок / валидности кодовых точек)
И это только самые основные проблемы при использовании Unicode! А есть еще группы символов, символы, которые не символы, поиск и сортировка или например, модификаторы
Модификаторы
Объединитель нулевой ширины (ZWJ) является непечатным символом в компьютерном наборе некоторых сложных шрифтов, таких как арабский или любой индийский шрифт. При помещении между двумя символами, которые в противном случае не были бы связаны, ZWJ заставляет их печататься в объединённой форме.
Разъединитель нулевой ширины (ZWNJ) — это непечатный символ в компьютерных наборах письменностей с лигатурами. При размещении между двумя символами, которые в противном случае были бы соединены в лигатуру, ZWNJ заставляет их печататься в их окончательной и первоначальной формах, соответственно. Действует как пробел, но используется в том случае, когда желательно удерживать слова рядом друг с другом или соединить слово с его морфемой.
Однако подавляющее большинство языков программирования может оперировать символьными строками как байтовыми массивами. А так как способов кодирования Unicode символов, а соответственно и типов литералов для таких строк существует великое множество, то сложилась довольно широко распространённая практика использовать у текстовых строк-литералов различные модификаторы для разных форматов кодирования.
Вот примеры определения разных типов строк в С++
// Character literals
auto c0 = 'A'; // char
auto c1 = u8'A'; // char
auto c2 = L'A'; // wchar_t
auto c3 = u'A'; // char16_t
auto c4 = U'A'; // char32_t
// Multicharacter literals
auto m0 = 'abcd'; // int, value 0x61626364
// String literals
auto s0 = "hello"; // const char*
auto s1 = u8"hello"; // const char* before C++20, encoded as UTF-8,
// const char8_t* in C++20
auto s2 = L"hello"; // const wchar_t*
auto s3 = u"hello"; // const char16_t*, encoded as UTF-16
auto s4 = U"hello"; // const char32_t*, encoded as UTF-32
// Raw string literals containing unescaped \ and "
auto R0 = R"("Hello \ world")"; // const char*
auto R1 = u8R"("Hello \ world")"; // const char* before C++20, encoded as UTF-8,
// const char8_t* in C++20
auto R2 = LR"("Hello \ world")"; // const wchar_t*
auto R3 = uR"("Hello \ world")"; // const char16_t*, encoded as UTF-16
auto R4 = UR"("Hello \ world")"; // const char32_t*, encoded as UTF-32
// Combining string literals with standard s-suffix
auto S0 = "hello"s; // std::string
auto S1 = u8"hello"s; // std::string before C++20, std::u8string in C++20
auto S2 = L"hello"s; // std::wstring
auto S3 = u"hello"s; // std::u16string
auto S4 = U"hello"s; // std::u32string
// Combining raw string literals with standard s-suffix
auto S5 = R"("Hello \ world")"s; // std::string from a raw const char*
auto S6 = u8R"("Hello \ world")"s; // std::string from a raw const char* before C++20, encoded as UTF-8,
// std::u8string in C++20
auto S7 = LR"("Hello \ world")"s; // std::wstring from a raw const wchar_t*
auto S8 = uR"("Hello \ world")"s; // std::u16string from a raw const char16_t*, encoded as UTF-16
auto S9 = UR"("Hello \ world")"s; // std::u32string from a raw const char32_t*, encoded as UTF-32
// ASCII smiling face
const char* s1 = ":-)";
// UTF-16 (on Windows) encoded WINKING FACE (U+1F609)
const wchar_t* s2 = L"???? = \U0001F609 is ;-)";
// UTF-8 encoded SMILING FACE WITH HALO (U+1F607)
const char* s3a = u8"???? = \U0001F607 is O:-)"; // Before C++20
const char8_t* s3b = u8"???? = \U0001F607 is O:-)"; // C++20
// UTF-16 encoded SMILING FACE WITH OPEN MOUTH (U+1F603)
const char16_t* s4 = u"???? = \U0001F603 is :-D";
// UTF-32 encoded SMILING FACE WITH SUNGLASSES (U+1F60E)
const char32_t* s5 = U"???? = \U0001F60E is B-)";
Все наверно помнят байку про связь между космическими кораблями и шириной лошадиного крупа?
Первая попавшаяся с опровержением Про космос и лошадей:
Текст байки про космос и лошадей
По бокам космического корабля "Кеннеди" размещаются два двигателя по 5 футов шириной. Конструкторы корабля хотели бы сделать эти двигатели еще шире, но не смогли. Почему?
Дело в том, что двигатели эти доставлялись по железной дороге, которая проходит по узкому туннелю. Расстояние между рельсами стандартное: 4 фута 8.5 дюйма, поэтому конструкторы могли сделать двигатели только шириной 5 футов.
Возникает вопрос: почему расстояние между рельсами 4 фута 8.5 дюйма? Откуда взялась эта цифра? Оказывается, что железную дорогу в Штатах делали такую же, как и в Англии, а в Англии делали железнодорожные вагоны по тому же принципу, что и трамвайные, а первые трамваи производились в Англии по образу и подобию конки. А длина оси конки составляла как раз 4 фута 8.5 дюйма!
Но почему? Потому что конки делали с тем расчетом, чтобы их оси попадали в колеи на английских дорогах, чтобы колеса меньше изнашивались, а расстояние между колеями в Англии как раз 4 фута 8.5 дюйма! Отчего так? Да просто дороги в Великобритании стали делать римляне, подводя их под размер своих боевых колесниц, и длина оси стандартной римской колесницы равнялась... правильно, 4 футам 8.5 дюймам!
Ну вот теперь мы докопались, откуда взялся этот размер, но все же почему римлянам вздумалось делать свои колесницы с осями именно такой длины? А вот почему: в такую колесницу запрягали обычно двух лошадей. А 4 фута 8.5 дюйма - это был как раз размер двух лошадиных задниц! Делать ось колесницы длиннее было неудобно, так как это нарушало бы равновесие колесницы.
Следовательно, вот и ответ на самый первый вопрос: даже теперь, когда человек вышел в космос, его наивысшие технические достижения напрямую зависят от РАЗМЕРА ЛОШАДИНОЙ ЗАДНИЦЫ.
Так вот, мне кажется, что для текстовых строк в языках программирования история идет тоже от задницы лошади изначального предположения, что текстовая строка и строка байтов, это одно и тоже. И хотя для кодировки UTF-8 это будет почти верным, но в общем случае для Unicode строк это уже не так!
Но поскольку синтаксис записи текстовых строк в языках программирования пошел от этого изначального предположения, а любые символьные строки остаются едиными сущностями, то на текущий момент мы имеем то, что имеем.
Основная мысль
Так как понятие "строка символов" уже было сформировано к моменту прихода эры Unicode, то разработчикам языков программирования ничего не оставалось делать, только как пытаться подстраиваться под новую реальность. У некоторых языков программирования это получилось лучше, у каких-то хуже, но в большинстве случае проблемы с конвертацией, проверкой и прочими прелестями обработки текста легли на плечи программистов.
Хотя мне кажется, что самым простым решением было бы добавить в язык программирования новый тип данных, Unicode строки с доступом исключительно по символам, чтобы физически разделить два представления текста между собой: байтовый массив и последовательность символов Unicode.
Это позволило бы всегда в явном виде контролировать преобразование одного типа строки в другой (что убрало проблемы с контролем ошибок преобразования кодировок / валидности кодовых точек), а также развело бы вопросы индексации по разным типам строк. Байтовые строки - индексация по байтам, символьные строки - индексация по символам.
Комментарии (46)
gev
00.00.0000 00:00Байтовые строки — индексация по байтам, символьные строки — индексация по символам.
Text
иByteString
?
mrCOTOHA
00.00.0000 00:00Поддерживаю. Я на днях долго геморроился с Unicode в Python 2.7, чтобы у меня в zip-архив нормально писались кириллические имена файлов. Так и не решил пока :(
iuabtw
00.00.0000 00:00+5Предлагаю изящное решение - перейти на питон 3.*
Второй уже три года как похоронили, есть куча гайдов по переезду и всякому сохранению совместимости
mrCOTOHA
00.00.0000 00:00Я пока не совсем настоящий сварщик) В проекте 3 месяца. Перетащить сотню тысяч строк кода, писанного не мной, со второго на третий... Боюсь тимлид не одобрит такую инициативу, да и ссыкотно, если честно)
iuabtw
00.00.0000 00:00Тогда не вариант переводить всё. Но можно написать кусок кода на третьем питоне и вызывать через subprocess
ay0ks
00.00.0000 00:00+3У питона есть встроенная утилита
2to3
, которая позволяет переводить код с 2.х на 3.х, вызывается через `python -m 2to3 файл`
NeoCode
00.00.0000 00:00+4Вообще нужен новый стандарт Unicode:) Там тоже хватает странностей, было бы хорошо от них избавиться.
Ну а что касается строк, то да, конечно лучше отдельный тип для строк, со специфическими функциями всякой "нормализации", изменения регистра, и т.п. В Qt например есть тип QString для строк и тип QByteArray для байтовых массивов, с близкими, но все-же отличающимися наборами методов (хотя я бы назвал тип не QByteArray а QBlob, было бы лаконичнее).
funca
00.00.0000 00:00+1Язык программирования это интерфейс между разработчиком и средой исполнения. Выразительные средства языка должны зависеть от решаемых задач.
Если обработка текстов является типичной задачей для данного ЯП, то поддержку юникода есть смысл выносить на уровнь синтаксиса самого языка, чтобы сократить писанину. Если нет - то лучше оставить на откуп библиотекам. Поэтому ответ для python и c++ будет разным.
vadimr
00.00.0000 00:00+3Ответ для питона и с++ будет разным исключительно по той причине, что с++ тянет совместимость с далёким прошлым, когда такая проблема не стояла, а теперь переделать его основы (к которым относится тип char и система его производных) невозможно.
maximw
00.00.0000 00:00Я так понимаю такие попытки уже были. Именно поэтому PHP с пятой мажорной версии скакнул сразу на седьмую.
F6CF
00.00.0000 00:00+4Что есть символ? Кластер графем, как это видит человек? Codepoint?
Для чего вообще нужен случайный доступ к символам? Как будет выглядеть представление таких строк в памяти?
gogalaim Автор
00.00.0000 00:00+1Если речь про Unicode строки, то достаточно будет сделать доступ к code point. То тогда само собой решаются вопросы и с представлением строки в памяти и с индексацией, необходимостью конвертирования и т.д.
yeputons
00.00.0000 00:00+3Но зачем доступ к code point? В общем случае это такая же абстракция. У него нет какого-либо значения, вот хорошая статья на тему.
Например, вот два code point: ????????. Если наивно "развернуть" это по code point'ам, получим другой флаг. Потому что не надо почти никогда разворачивать строчки в вакууме, если это игра — то наверняка игра под конкретный язык.
А вот один code point: ﷽ . Или вот несколько, но выделяются обычно как один: ᄀᄀᄀ각ᆨᆨ.
gogalaim Автор
00.00.0000 00:00+1Потому что code point это минимально возможная единица данных для Unicode строк
ZyXI
00.00.0000 00:00И это объясняет индексацию по code point как именно? Вы вполне можете поступить как в Rust: индексация по code unit (в данном случае байтам), но попытка создать подстроку только с частью code point приводит к ошибке, требует предварительного преобразования строки в массив байт или требует использования
unsafe
и считается ошибкой программиста в случае успеха.
И вполне понятно, зачем они это сделали: так одновременно получается индексация за O(1) и при этом вы не платите в четыре раза больше байт за строку/не усложняете себе жизнь поддержкой трёх возможных размеров code unit и не занимаетесь постоянным перегоном в/из UTF-8. Реализации разных операций со строками это обычно либо совсем не усложняет, либо добавляет простые проверки на попадание на границы символов.
lamerok
00.00.0000 00:00Вот люди привыкают к auto на C++ и привыкают, что 'a' это литерал char, а потом идут в Си и недоумевают, что происходит, потому ято там 'a' это литерал int.
NN1
00.00.0000 00:00+2auto здесь совсем не при чём.
Такое различие между языками.
Кстати , в C23 теперь тоже появится auto с выводом типа.
lamerok
00.00.0000 00:00+1Auto скрывает от разрабатывает реальный тип, что сю часто приводит к ошибкам, если разраб не помнит всех правил вывода.
ИМХО, лучше явно тип указывать, либо сразу кастовать, типо
auto I = std::uint32_t(2);
Понятно, что из за разницы в языке, если в C23 auto ввели, количество багов увеличится при переходе с одного языка на другой.
MrNutz
00.00.0000 00:00-1Можно конкретный примерчик с "проблемой"? Где и какие сложности возникают? Или весь опус просто о лени и очередной вариации как бы хорошо иметь очередную кнопку "сделай офигенно"? За 15+ лет разработки на разных языках никакого геморроя с юникодом не испытывал.
P. S. : Тип auto - это какая то жесть. Вот потом и получается разработчик, который не понимает что делает и как все устроено под капотом.
yeputons
00.00.0000 00:00+1Мне кажется, проблемы скорее не из-за юникода, а из-за всяких неверных предположений о языках при разработке (статья). Даже арабский (один из шести официальных языков ООН) на сайтах отображают неверно: https://isthisarabic.com/
А с эмоджи в текстах тоже никаких проблем не возникало? С русским-то проблем лишь чуть-чуть больше, чем с английским — просто меняем концепцию "один символ — один байт" на "один символ — один code point" и теперь работаем и с русским, и с английским. А вот эмоджи это разламывают.
gogalaim Автор
00.00.0000 00:00просто меняем концепцию "один символ — один байт" на "один символ — один code point"
Это идеальный вариант, но он сработает только если будет два разных типа текстовых строк.
qw1
00.00.0000 00:00Поверх последовательности code-points нужен ещё один уровень абстракции: отображаемые символы. Иначе, придётся у себя в программе решать эту задачу.
Например, если символ состоит из 6 code-points (выше были примеры), а мы пишем текстовый редактор, и нужно при нажатии клавиши "вправо" перейти на следующий символ. Или если у нас бегущая строка, в буфер которой надо подкидывать вовсе не code-points, а печатные символы.
gogalaim Автор
00.00.0000 00:00+2Поверх последовательности code-points нужен ещё один уровень абстракции: отображаемые символы. Иначе, придётся у себя в программе решать эту задачу.
Так это и должно делаться в конечной программе. Текстовая строка Unicode это только хранилище данных, а их визуализация и интерпретация в виде печатных символов, это более высокий уровень абстракции.
qw1
00.00.0000 00:00+1То же можно сказать и о code-points. Массив байт это только хранилище данных, а интерпретация остаётся на усмотрение прикладного программиста.
Но зачем в каждой программе дублировать логику, описанную стандартом, если в библиотеку или даже в язык можно ввести абстракцию более высокого уровня и всем сэкономить время.
rsashka
00.00.0000 00:00То же можно сказать и о code-points. Массив байт это только хранилище данных, а интерпретация остаётся на усмотрение прикладного программиста.
Так об Unicode сказать нельзя, так как минимальная единица информации это именно codepoint, в противном случае можно скатиться в рассуждениях и до отдельных бит (ведь именно бит минимальная единица информации).
qw1
00.00.0000 00:00Вниз по абстракциям путь уже открыт — любой язык позволяет взять символ как (unsigned) int и инспектировать отдельные биты.
gogalaim Автор
00.00.0000 00:00+2К сожалению code point тоже несколько видов и один единственный тип данных для хранения unicode строк сделать не получится.
gogalaim Автор
00.00.0000 00:00+1Любая программа, это абстракция над абстракцией. Было бы логичным использовать именно самый низкоуровневый элемент для хранения данных, и в случае Unicode это действительно code-point.
Правда их тоже несколько видов и если делать реализацию именно на уровне синтаксиса языка, то непонятно как это учесть. Возможно все как раз и скатится к тому виду, как это реализовано в С++ с разными вариантами code point.
Gena00X
00.00.0000 00:00А в чём проблема?
С++ позволяет создать собственный тип данных, думаю любой другой язык программирования с поддержкой ООП тоже, если кому-то нужна Unicode строка индексируемая по code-point или по отображаемым символам - он может сам реализовать её.
Только не получится ли, что такая строка вынуждена будет проверять все составные символы Unicode каждый раз когда выполняется индексирование, чтобы убедиться что очередной символ - не один из них и не требует специальной обработки?
vadimr
00.00.0000 00:00+1Проблема в том, что этих собственных типов данных уже насоздавали дохрена. И вот начинаются преобразования туда-сюда между какими-нибудь char*, std::String и QString.
qw1
00.00.0000 00:00+1Встраивание Unicode в язык тоже решает проблему лишь на некоторое время. Например, когда создавался C# примерно в 2000-м, казалось, что UCS-2 будет достаточно, чтобы индексировать строку по codepoints. А потом пришли новые версии Unicode, и это допущение сломалось.
Введя сейчас в язык unicode-строки по самым последним стандартам, через 20 лет они тоже устареют, и нужно будет переделывать, а язык отправится в legacy. В этом смысле библиотеки лучше, их проще заменить (std::string → std::wstring → std::u32string → ...)
vadimr
00.00.0000 00:00+1Ну в данном случае это исключительно косяк Майкрософта, тянущийся от изначально неудачного выбора в Win32. В Фортране, например, в 2003 году уже в стандарт ISO прописали UCS-4, а на уровне расширений это было ещё с 1995 года. Правда, большинство разработчиков компиляторов забило на этот стандарт :)
Но на самом деле никто же не просит прибивать гвоздями к языку (или библиотеке) конкретный способ кодирования и количество байтов на символ.
qw1
00.00.0000 00:00Но на самом деле никто же не просит прибивать гвоздями к языку (или библиотеке) конкретный способ кодирования и количество байтов на символ
То есть, вы считаете, что Microsoft без проблем сможет в своей System.String перейти на 32-битные символы? Мне кажется, слишком много unsafe-кода предполагают, что символы там 16-битные, и поменять это в языке уже невозможно, не сломав очень много старого кода.
vadimr
00.00.0000 00:00Для этого с самого начала надо было прописать, что размер зависит от реализации. Теперь уж поздно.
rsashka
Не хватает опроса после статьи
gogalaim Автор
Опрос добавил
Tuxman
Не хватает пункта - этот опрос сосёт.