Уже давно прошли те времена, когда текстовые строки в языках программирования были исключительно байтовыми без поддержки символов национальных алфавитов, а в некоторых случаях еще и ограничены размером не более 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)


  1. rsashka
    00.00.0000 00:00
    +7

    Не хватает опроса после статьи


    1. gogalaim Автор
      00.00.0000 00:00
      +3

      Опрос добавил


      1. Tuxman
        00.00.0000 00:00

        Не хватает пункта - этот опрос сосёт.


  1. gev
    00.00.0000 00:00

    Байтовые строки — индексация по байтам, символьные строки — индексация по символам.

    Text и ByteString?


  1. mrCOTOHA
    00.00.0000 00:00

    Поддерживаю. Я на днях долго геморроился с Unicode в Python 2.7, чтобы у меня в zip-архив нормально писались кириллические имена файлов. Так и не решил пока :(


    1. iuabtw
      00.00.0000 00:00
      +5

      Предлагаю изящное решение - перейти на питон 3.*

      Второй уже три года как похоронили, есть куча гайдов по переезду и всякому сохранению совместимости


      1. mrCOTOHA
        00.00.0000 00:00

        Я пока не совсем настоящий сварщик) В проекте 3 месяца. Перетащить сотню тысяч строк кода, писанного не мной, со второго на третий... Боюсь тимлид не одобрит такую инициативу, да и ссыкотно, если честно)


        1. iuabtw
          00.00.0000 00:00

          Тогда не вариант переводить всё. Но можно написать кусок кода на третьем питоне и вызывать через subprocess


        1. ay0ks
          00.00.0000 00:00
          +3

          У питона есть встроенная утилита 2to3, которая позволяет переводить код с 2.х на 3.х, вызывается через `python -m 2to3 файл`


  1. NeoCode
    00.00.0000 00:00
    +4

    Вообще нужен новый стандарт Unicode:) Там тоже хватает странностей, было бы хорошо от них избавиться.

    Ну а что касается строк, то да, конечно лучше отдельный тип для строк, со специфическими функциями всякой "нормализации", изменения регистра, и т.п. В Qt например есть тип QString для строк и тип QByteArray для байтовых массивов, с близкими, но все-же отличающимися наборами методов (хотя я бы назвал тип не QByteArray а QBlob, было бы лаконичнее).


    1. vikitoriya
      00.00.0000 00:00

      QString это же обертка над обычными std::string, только с QT прибамбахами


      1. thevlad
        00.00.0000 00:00
        +5

        Это собственная реализация. И если дефолтный std::string хранит char без привязки к кодировки, то QString это всегда UTF-16 внутри.


        1. gogalaim Автор
          00.00.0000 00:00

          Спасибо, не знал.


  1. vadimr
    00.00.0000 00:00
    +1

    В питоне3 так и есть.


  1. funca
    00.00.0000 00:00
    +1

    Язык программирования это интерфейс между разработчиком и средой исполнения. Выразительные средства языка должны зависеть от решаемых задач.

    Если обработка текстов является типичной задачей для данного ЯП, то поддержку юникода есть смысл выносить на уровнь синтаксиса самого языка, чтобы сократить писанину. Если нет - то лучше оставить на откуп библиотекам. Поэтому ответ для python и c++ будет разным.


    1. vadimr
      00.00.0000 00:00
      +3

      Ответ для питона и с++ будет разным исключительно по той причине, что с++ тянет совместимость с далёким прошлым, когда такая проблема не стояла, а теперь переделать его основы (к которым относится тип char и система его производных) невозможно.


  1. maximw
    00.00.0000 00:00

    Я так понимаю такие попытки уже были. Именно поэтому PHP с пятой мажорной версии скакнул сразу на седьмую.


  1. F6CF
    00.00.0000 00:00
    +4

    Что есть символ? Кластер графем, как это видит человек? Codepoint?

    Для чего вообще нужен случайный доступ к символам? Как будет выглядеть представление таких строк в памяти?


    1. gogalaim Автор
      00.00.0000 00:00
      +1

      Если речь про Unicode строки, то достаточно будет сделать доступ к code point. То тогда само собой решаются вопросы и с представлением строки в памяти и с индексацией, необходимостью конвертирования и т.д.


      1. yeputons
        00.00.0000 00:00
        +3

        Но зачем доступ к code point? В общем случае это такая же абстракция. У него нет какого-либо значения, вот хорошая статья на тему.

        Например, вот два code point: ????????. Если наивно "развернуть" это по code point'ам, получим другой флаг. Потому что не надо почти никогда разворачивать строчки в вакууме, если это игра — то наверняка игра под конкретный язык.

        А вот один code point: ﷽ . Или вот несколько, но выделяются обычно как один: ᄀᄀᄀ각ᆨᆨ.


        1. gogalaim Автор
          00.00.0000 00:00
          +1

          Потому что code point это минимально возможная единица данных для Unicode строк


          1. ZyXI
            00.00.0000 00:00

            И это объясняет индексацию по code point как именно? Вы вполне можете поступить как в Rust: индексация по code unit (в данном случае байтам), но попытка создать подстроку только с частью code point приводит к ошибке, требует предварительного преобразования строки в массив байт или требует использования unsafe и считается ошибкой программиста в случае успеха.
            И вполне понятно, зачем они это сделали: так одновременно получается индексация за O(1) и при этом вы не платите в четыре раза больше байт за строку/не усложняете себе жизнь поддержкой трёх возможных размеров code unit и не занимаетесь постоянным перегоном в/из UTF-8. Реализации разных операций со строками это обычно либо совсем не усложняет, либо добавляет простые проверки на попадание на границы символов.


  1. lamerok
    00.00.0000 00:00

    Вот люди привыкают к auto на C++ и привыкают, что 'a' это литерал char, а потом идут в Си и недоумевают, что происходит, потому ято там 'a' это литерал int.


    1. NN1
      00.00.0000 00:00
      +2

      auto здесь совсем не при чём.

      Такое различие между языками.

      Кстати , в C23 теперь тоже появится auto с выводом типа.


      1. lamerok
        00.00.0000 00:00
        +1

        Auto скрывает от разрабатывает реальный тип, что сю часто приводит к ошибкам, если разраб не помнит всех правил вывода.

        ИМХО, лучше явно тип указывать, либо сразу кастовать, типо

        auto I = std::uint32_t(2);

        Понятно, что из за разницы в языке, если в C23 auto ввели, количество багов увеличится при переходе с одного языка на другой.


        1. DistortNeo
          00.00.0000 00:00
          +1

          А ещё это проблема слабой типизации.


  1. MrNutz
    00.00.0000 00:00
    -1

    Можно конкретный примерчик с "проблемой"? Где и какие сложности возникают? Или весь опус просто о лени и очередной вариации как бы хорошо иметь очередную кнопку "сделай офигенно"? За 15+ лет разработки на разных языках никакого геморроя с юникодом не испытывал.

    P. S. : Тип auto - это какая то жесть. Вот потом и получается разработчик, который не понимает что делает и как все устроено под капотом.


    1. yeputons
      00.00.0000 00:00
      +1

      Мне кажется, проблемы скорее не из-за юникода, а из-за всяких неверных предположений о языках при разработке (статья). Даже арабский (один из шести официальных языков ООН) на сайтах отображают неверно: https://isthisarabic.com/

      А с эмоджи в текстах тоже никаких проблем не возникало? С русским-то проблем лишь чуть-чуть больше, чем с английским — просто меняем концепцию "один символ — один байт" на "один символ — один code point" и теперь работаем и с русским, и с английским. А вот эмоджи это разламывают.


      1. gogalaim Автор
        00.00.0000 00:00

        просто меняем концепцию "один символ — один байт" на "один символ — один code point"

        Это идеальный вариант, но он сработает только если будет два разных типа текстовых строк.


        1. qw1
          00.00.0000 00:00

          Поверх последовательности code-points нужен ещё один уровень абстракции: отображаемые символы. Иначе, придётся у себя в программе решать эту задачу.


          Например, если символ состоит из 6 code-points (выше были примеры), а мы пишем текстовый редактор, и нужно при нажатии клавиши "вправо" перейти на следующий символ. Или если у нас бегущая строка, в буфер которой надо подкидывать вовсе не code-points, а печатные символы.


          1. gogalaim Автор
            00.00.0000 00:00
            +2

            Поверх последовательности code-points нужен ещё один уровень абстракции: отображаемые символы. Иначе, придётся у себя в программе решать эту задачу.

            Так это и должно делаться в конечной программе. Текстовая строка Unicode это только хранилище данных, а их визуализация и интерпретация в виде печатных символов, это более высокий уровень абстракции.


            1. qw1
              00.00.0000 00:00
              +1

              То же можно сказать и о code-points. Массив байт это только хранилище данных, а интерпретация остаётся на усмотрение прикладного программиста.


              Но зачем в каждой программе дублировать логику, описанную стандартом, если в библиотеку или даже в язык можно ввести абстракцию более высокого уровня и всем сэкономить время.


              1. rsashka
                00.00.0000 00:00

                То же можно сказать и о code-points. Массив байт это только хранилище данных, а интерпретация остаётся на усмотрение прикладного программиста.

                Так об Unicode сказать нельзя, так как минимальная единица информации это именно codepoint, в противном случае можно скатиться в рассуждениях и до отдельных бит (ведь именно бит минимальная единица информации).


                1. qw1
                  00.00.0000 00:00

                  Вниз по абстракциям путь уже открыт — любой язык позволяет взять символ как (unsigned) int и инспектировать отдельные биты.


                1. gogalaim Автор
                  00.00.0000 00:00
                  +2

                  К сожалению code point тоже несколько видов и один единственный тип данных для хранения unicode строк сделать не получится.


                  1. DistortNeo
                    00.00.0000 00:00
                    +1

                    Кажется, вы путаете code point и code unit.


              1. gogalaim Автор
                00.00.0000 00:00
                +1

                Любая программа, это абстракция над абстракцией. Было бы логичным использовать именно самый низкоуровневый элемент для хранения данных, и в случае Unicode это действительно code-point.
                Правда их тоже несколько видов и если делать реализацию именно на уровне синтаксиса языка, то непонятно как это учесть. Возможно все как раз и скатится к тому виду, как это реализовано в С++ с разными вариантами code point.


  1. Rustified
    00.00.0000 00:00

    Зачем тут тег "Rust" если он даже не упоминается?


    1. gogalaim Автор
      00.00.0000 00:00

      Изначально, когда я собирал материал, то хотел привести Rust как один из примеров языков, в которых отсутствует на байтовые и Unicode строки. Но потом отказался от этого, а тег забыл убрать.


      1. DarkEld3r
        00.00.0000 00:00
        +1

        А зачем отдельные байтовые строки, если можно использовать вектор байт?


  1. Gena00X
    00.00.0000 00:00

    А в чём проблема?

    С++ позволяет создать собственный тип данных, думаю любой другой язык программирования с поддержкой ООП тоже, если кому-то нужна Unicode строка индексируемая по code-point или по отображаемым символам - он может сам реализовать её.

    Только не получится ли, что такая строка вынуждена будет проверять все составные символы Unicode каждый раз когда выполняется индексирование, чтобы убедиться что очередной символ - не один из них и не требует специальной обработки?


    1. vadimr
      00.00.0000 00:00
      +1

      Проблема в том, что этих собственных типов данных уже насоздавали дохрена. И вот начинаются преобразования туда-сюда между какими-нибудь char*, std::String и QString.


      1. qw1
        00.00.0000 00:00
        +1

        Встраивание Unicode в язык тоже решает проблему лишь на некоторое время. Например, когда создавался C# примерно в 2000-м, казалось, что UCS-2 будет достаточно, чтобы индексировать строку по codepoints. А потом пришли новые версии Unicode, и это допущение сломалось.


        Введя сейчас в язык unicode-строки по самым последним стандартам, через 20 лет они тоже устареют, и нужно будет переделывать, а язык отправится в legacy. В этом смысле библиотеки лучше, их проще заменить (std::string → std::wstring → std::u32string → ...)


        1. vadimr
          00.00.0000 00:00
          +1

          Ну в данном случае это исключительно косяк Майкрософта, тянущийся от изначально неудачного выбора в Win32. В Фортране, например, в 2003 году уже в стандарт ISO прописали UCS-4, а на уровне расширений это было ещё с 1995 года. Правда, большинство разработчиков компиляторов забило на этот стандарт :)

          Но на самом деле никто же не просит прибивать гвоздями к языку (или библиотеке) конкретный способ кодирования и количество байтов на символ.


          1. qw1
            00.00.0000 00:00

            Но на самом деле никто же не просит прибивать гвоздями к языку (или библиотеке) конкретный способ кодирования и количество байтов на символ

            То есть, вы считаете, что Microsoft без проблем сможет в своей System.String перейти на 32-битные символы? Мне кажется, слишком много unsafe-кода предполагают, что символы там 16-битные, и поменять это в языке уже невозможно, не сломав очень много старого кода.


            1. vadimr
              00.00.0000 00:00

              Для этого с самого начала надо было прописать, что размер зависит от реализации. Теперь уж поздно.