Символьный тип данных: char, Unicode и операции с символами

Символьный тип данных хранит отдельную единицу текста: букву, цифру, знак пунктуации, пробел или управляющий символ. В учебных примерах его часто называют char, но в реальной программе слово «символ» может означать разные вещи — байт, кодовую единицу, кодовую точку Unicode или целый видимый знак.

Простые примеры укладываются во все эти определения:

'A'
'7'
'?'
' '

С emoji, флагами и буквами с диакритическими знаками различия становятся важными. Например, видимый знак é может быть одной кодовой точкой U+00E9 или последовательностью e и комбинируемого ударения U+0301.

Код символа, Unicode и кодировка

Компьютер хранит числа, поэтому тексту требуется соглашение о соответствии чисел символам.

В UTF-8 одна кодовая точка занимает от 1 до 4 байт. В UTF-16 она занимает одну или две 16-битные кодовые единицы. Поэтому длина строки в байтах, кодовых единицах, кодовых точках и видимых пользователю знаках может отличаться.

Что именно хранит char в разных языках

ЯзыкПредставление отдельного символаВажная особенность
C и C++charхранит один байт; сам по себе не гарантирует целый Unicode-символ
Javacharодна 16-битная кодовая единица UTF-16; emoji обычно требует два char
Gorune, псевдоним int32хранит одну кодовую точку Unicode; строка обычно содержит байты UTF-8
JavaScript и TypeScriptотдельного char нетстроковый индекс возвращает кодовую единицу UTF-16
Python 3отдельного char нетстрока длиной 1 хранит одну кодовую точку, но не обязательно один видимый знак

Одинарные кавычки не везде означают одно и то же. В Java 'A' — значение char, в Go 'A'rune, а в JavaScript и Python это обычная строка.

Кодовая точка, кодовая единица и графема

Из-за этого выражение «строка длиной один символ» нужно уточнять: какой именно уровень требуется задаче.

Примеры в разных языках

JavaScript и TypeScript

length и индексирование работают с кодовыми единицами UTF-16. Для получения полной кодовой точки используют codePointAt, а for...of или оператор расширения перебирает кодовые точки:

const letter = 'Я';
const emoji = '🙂';

console.log(letter.codePointAt(0)); // 1071
console.log(emoji.length); // 2 кодовые единицы UTF-16
console.log([...emoji].length); // 1 кодовая точка
console.log(emoji.codePointAt(0)?.toString(16)); // "1f642"

Для разбиения по видимым графемам в современных средах применяют Intl.Segmenter, а не split('').

Go

Одинарные кавычки создают rune. Функция len для строки возвращает число байт, а utf8.RuneCountInString — число кодовых точек:

package main

import (
  "fmt"
  "unicode/utf8"
)

func main() {
  letter := 'Я'
  text := "Я"

  fmt.Printf("%c U+%04X
", letter, letter)
  fmt.Println(len(text))                    // 2 байта в UTF-8
  fmt.Println(utf8.RuneCountInString(text)) // 1 кодовая точка
}

Цикл for _, symbol := range text декодирует строку и перебирает rune, а не отдельные байты.

Java

char подходит для символов базовой многоязычной плоскости, но может содержать только половину суррогатной пары. Для полного диапазона Unicode используют методы, работающие с int-кодовыми точками:

public class Example {
  public static void main(String[] args) {
    String emoji = "🙂";
    int codePoint = emoji.codePointAt(0);

    System.out.println(emoji.length()); // 2 элемента char
    System.out.println(emoji.codePointCount(0, emoji.length())); // 1
    System.out.printf("U+%X%n", codePoint); // U+1F642
  }
}

Python

В Python 3 функции ord и chr преобразуют кодовую точку в число и обратно:

letter = 'Я'
code_point = ord(letter)

print(code_point)          # 1071
print(chr(code_point))     # Я
print(len('🙂'))           # 1 кодовая точка
print(letter.isalpha())    # True

Даже в Python len считает кодовые точки, а не графемы: len('e\u0301') вернет 2, хотя пользователь видит одну букву с ударением.

Основные операции с символами

Получение символа и его кода

Программа может преобразовать символ в кодовую точку и обратно. В JavaScript для полного Unicode-диапазона нужны codePointAt и String.fromCodePoint, а не только старые charCodeAt и fromCharCode.

const codePoint = '🙂'.codePointAt(0);
const symbol = String.fromCodePoint(0x1f642);

console.log(codePoint); // 128578
console.log(symbol); // 🙂

Проверка категории

У символа можно проверить принадлежность к буквам, цифрам, пробелам и другим категориям Unicode. Готовые функции надежнее ручного диапазона от '0' до '9', если ввод не ограничен ASCII.

const symbol = 'Я';
const isUnicodeLetter = /^\p{L}$/u.test(symbol);

console.log(isUnicodeLetter); // true

Аналоги: unicode.IsLetter в Go, Character.isLetter в Java и str.isalpha в Python.

Сравнение и сортировка

Операторы < и > обычно сравнивают числовые коды или кодовые единицы. Такой порядок не совпадает с алфавитным порядком конкретного языка. Для сортировки имен пользователя применяют локализованный компаратор, например Intl.Collator в JavaScript или Collator в Java.

const collator = new Intl.Collator('ru');
const letters = ['я', 'а', 'ё', 'е'];

console.log(letters.toSorted(collator.compare)); // ['а', 'е', 'ё', 'я']

Преобразование регистра

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

Нормализация

Визуально одинаковый текст может иметь разные последовательности кодовых точек. Перед поиском, сравнением идентификаторов или удалением дублей часто применяют нормализацию Unicode, например NFC:

const composed = 'é';
const decomposed = 'e\u0301';

console.log(composed === decomposed); // false
console.log(composed.normalize('NFC') === decomposed.normalize('NFC')); // true

Нормализация не заменяет локализованное сравнение и правила безопасности для логинов, доменов или похожих символов разных алфавитов.

Где используют символьные данные

Перед реализацией важно определить единицу обработки. Парсер формата может работать с ASCII-байтами, анализатор исходного кода — с кодовыми точками, а редактор сообщения — с графемными кластерами.

Вывод

Символьный тип связывает текст с числовым представлением, но «один символ» может означать байт, кодовую единицу, кодовую точку или графему. Корректная программа явно выбирает нужный уровень, учитывает Unicode и использует подходящие API языка вместо предположения, что любой знак помещается в один char.

Источники

  • Горбатов В.А., Горбатов А.В., Горбатова М.В. Дискретная математика: Учебник для студентов втузов. - М.: АСТ, 2014. - 448 с.
  • Горбатов В.А., Горбатов А.В., Горбатова М.В. Теория автоматов: учебник для втузов. - М.: АСТ, 2008. - 559 с.
  • Кузнецов О. П. Дискретная математика для инженера. - Санкт-Петербург [и др.]: Лань, 2009.
  • Содержание курса лекций «Языки программирования». Кафедра алгоритмических языков ВМК МГУ, 2018.
  • Босова Л.Л. Информатика. Базовый курс: учебник для 10-11 классов. - М.: БИНОМ. Лаборатория знаний, 2021.
  • Гладкий Ю.Н. Информатика и информационные технологии: учеб. пособие. - М.: КноРус, 2020.