Символьный тип данных: char, Unicode и операции с символами
Символьный тип данных хранит отдельную единицу текста: букву, цифру, знак пунктуации, пробел или управляющий символ. В учебных примерах его часто называют char, но в реальной программе слово «символ» может означать разные вещи — байт, кодовую единицу, кодовую точку Unicode или целый видимый знак.
Простые примеры укладываются во все эти определения:
'A'
'7'
'?'
' 'С emoji, флагами и буквами с диакритическими знаками различия становятся важными. Например, видимый знак é может быть одной кодовой точкой U+00E9 или последовательностью e и комбинируемого ударения U+0301.
Код символа, Unicode и кодировка
Компьютер хранит числа, поэтому тексту требуется соглашение о соответствии чисел символам.
- ASCII описывает 128 основных кодов: латинские буквы, цифры, знаки и управляющие символы. Код
Aравен 65. - Unicode присваивает абстрактным символам кодовые точки, например
U+0041дляA,U+042FдляЯиU+1F642для🙂. - UTF-8 и UTF-16 — способы кодирования кодовых точек в байты или кодовые единицы. Unicode и UTF-8 — не синонимы.
В UTF-8 одна кодовая точка занимает от 1 до 4 байт. В UTF-16 она занимает одну или две 16-битные кодовые единицы. Поэтому длина строки в байтах, кодовых единицах, кодовых точках и видимых пользователю знаках может отличаться.
Что именно хранит char в разных языках
| Язык | Представление отдельного символа | Важная особенность |
|---|---|---|
| C и C++ | char | хранит один байт; сам по себе не гарантирует целый Unicode-символ |
| Java | char | одна 16-битная кодовая единица UTF-16; emoji обычно требует два char |
| Go | rune, псевдоним int32 | хранит одну кодовую точку Unicode; строка обычно содержит байты UTF-8 |
| JavaScript и TypeScript | отдельного char нет | строковый индекс возвращает кодовую единицу UTF-16 |
| Python 3 | отдельного char нет | строка длиной 1 хранит одну кодовую точку, но не обязательно один видимый знак |
Одинарные кавычки не везде означают одно и то же. В Java 'A' — значение char, в Go 'A' — rune, а в JavaScript и Python это обычная строка.
Кодовая точка, кодовая единица и графема
- Кодовая точка — номер в стандарте Unicode, например
U+1F642. - Кодовая единица — элемент конкретной кодировки: байт UTF-8 или 16-битное слово UTF-16.
- Графемный кластер — то, что пользователь воспринимает как один знак. Он может состоять из нескольких кодовых точек: базовой буквы и диакритики, emoji с модификатором тона кожи или семейного emoji, соединенного символами ZWJ.
Из-за этого выражение «строка длиной один символ» нужно уточнять: какой именно уровень требуется задаче.
Примеры в разных языках
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.