Уже давно прошли те времена, когда текстовые строки в языках программирования были исключительно байтовыми без поддержки символов национальных алфавитов, а в некоторых случаях еще и ограничены размером не более 255 символов. В настоящее время наоборот, сложно найти такой язык программирования, который НЕ "поддерживает" юникод в текстовых строках.
Если вы обратили внимание, то слово "поддерживает" взято в кавычки и как говорил Винипух, это жжж не спроста, ведь с появлением Unicode понятие "символ" в текстовых строках стало не совсем однозначным.
Есть старая статья о проблемах поддержки Unicode в разных языках программирования: The importance of language-level abstract Unicode strings Matt Giuca
Основной смысл которой сводится к тому, чтобы призвать разработчиков языков программирования абстрагироваться от схем кодирования Unicode (доступом к отдельным байтам), и оставить для программистов только возможность работы с последовательностью символов, чтобы предотвратить большинство ошибок Unicode, так как с приходом эры Unicode изменилось само понятие символа и текстовой строки!
code unit - это единицы кодировки (utf-8, utf-16 или utf-32)
Соответственно с приходом 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-)";
Все наверно помнят байку про связь между космическими кораблями и шириной лошадиного крупа?
Первая попавшаяся с опровержением Про космос и лошадей:
Текст байки про космос и лошадей
задницы лошади изначального предположения, что текстовая строка и строка байтов, это одно и тоже. И хотя для кодировки UTF-8 это будет почти верным, но в общем случае для Unicode строк это уже не так!
Но поскольку синтаксис записи текстовых строк в языках программирования пошел от этого изначального предположения, а любые символьные строки остаются едиными сущностями, то на текущий момент мы имеем то, что имеем.
Хотя мне кажется, что самым простым решением было бы добавить в язык программирования новый тип данных, Unicode строки с доступом исключительно по символам, чтобы физически разделить два представления текста между собой: байтовый массив и последовательность символов Unicode.
Это позволило бы всегда в явном виде контролировать преобразование одного типа строки в другой (что убрало проблемы с контролем ошибок преобразования кодировок / валидности кодовых точек), а также развело бы вопросы индексации по разным типам строк. Байтовые строки - индексация по байтам, символьные строки - индексация по символам.
Если вы обратили внимание, то слово "поддерживает" взято в кавычки и как говорил Винипух, это жжж не спроста, ведь с появлением 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 так как приходится сканировать строку для подсчета количества Unicode символов.
- Требуется проверка корректности данных строки при сериализации/десериализации (контроль ошибок преобразования кодировок / валидности кодовых точек)
Однако подавляющее большинство языков программирования может оперировать символьными строками как байтовыми массивами. А так как способов кодирования Unicode символов, а соответственно и типов литералов для таких строк существует великое множество, то сложилась довольно широко распространённая практика использовать у текстовых строк-литералов различные модификаторы для разных форматов кодирования.Модификаторы
Объединитель нулевой ширины (ZWJ) является непечатным символом в компьютерном наборе некоторых сложных шрифтов, таких как арабский или любой индийский шрифт. При помещении между двумя символами, которые в противном случае не были бы связаны, ZWJ заставляет их печататься в объединённой форме.
Разъединитель нулевой ширины (ZWNJ) — это непечатный символ в компьютерных наборах письменностей с лигатурами. При размещении между двумя символами, которые в противном случае были бы соединены в лигатуру, ZWNJ заставляет их печататься в их окончательной и первоначальной формах, соответственно. Действует как пробел, но используется в том случае, когда желательно удерживать слова рядом друг с другом или соединить слово с его морфемой.
Вот примеры определения разных типов строк в С++
// 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 дюйма - это был как раз размер двух лошадиных задниц! Делать ось колесницы длиннее было неудобно, так как это нарушало бы равновесие колесницы.
Следовательно, вот и ответ на самый первый вопрос: даже теперь, когда человек вышел в космос, его наивысшие технические достижения напрямую зависят от РАЗМЕРА ЛОШАДИНОЙ ЗАДНИЦЫ.
Но поскольку синтаксис записи текстовых строк в языках программирования пошел от этого изначального предположения, а любые символьные строки остаются едиными сущностями, то на текущий момент мы имеем то, что имеем.
Основная мысль
Так как понятие "строка символов" уже было сформировано к моменту прихода эры Unicode, то разработчикам языков программирования ничего не оставалось делать, только как пытаться подстраиваться под новую реальность. У некоторых языков программирования это получилось лучше, у каких-то хуже, но в большинстве случае проблемы с конвертацией, проверкой и прочими прелестями обработки текста легли на плечи программистов.Хотя мне кажется, что самым простым решением было бы добавить в язык программирования новый тип данных, Unicode строки с доступом исключительно по символам, чтобы физически разделить два представления текста между собой: байтовый массив и последовательность символов Unicode.
Это позволило бы всегда в явном виде контролировать преобразование одного типа строки в другой (что убрало проблемы с контролем ошибок преобразования кодировок / валидности кодовых точек), а также развело бы вопросы индексации по разным типам строк. Байтовые строки - индексация по байтам, символьные строки - индексация по символам.
Текстовые строки в языках программирования
Уже давно прошли те времена, когда текстовые строки в языках программирования были исключительно байтовыми без поддержки символов национальных алфавитов, а в некоторых случаях еще и ограничены...
habr.com