Строки и работа с UTF-8

Понимаем, что строка в Zig — это просто срез байтов, и как с ними жить.

Строка в Zig — это срез байтов []const u8; язык не навязывает кодировку, но строковые литералы хранятся в UTF-8, поэтому длина в байтах может не совпадать с числом символов.

В Zig нет отдельного строкового типа. Строка — это []const u8, то есть срез неизменяемых байтов. Это честно отражает реальность: в памяти строка и есть последовательность байтов. Такой подход прост и предсказуем, но требует понимать разницу между байтами и символами.

Литералы — это байты в UTF-8

const std = @import("std");

pub fn main() void {
    const s = "Привет"; // тип: *const [12:0]u8 — 12 БАЙТ, не 6 символов
    std.debug.print("байт в строке: {d}\n", .{s.len});
}

Вывод:

байт в строке: 12

Слово «Привет» — это 6 кириллических букв, но в UTF-8 каждая занимает по 2 байта, итого 12. s.len возвращает длину в байтах, а не в символах. Это ключевой момент: для ASCII байт = символ, но для кириллицы, эмодзи и прочего — нет.

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

const std = @import("std");

pub fn main() void {
    const a = "zig";
    const eq = std.mem.eql(u8, a, "zig");          // сравнение байтов
    const has = std.mem.indexOf(u8, "hello zig", "zig"); // позиция или null
    std.debug.print("eq={} pos={?d}\n", .{ eq, has });
}

Вывод:

eq=true pos=6

Поскольку строка — срез байтов, сравнивают их через std.mem.eql(u8, ...) (оператор == для срезов не работает по содержимому). indexOf ищет подстроку и возвращает опциональную позицию. Формат {?d} печатает опциональное число, разворачивая его.

Подсчёт реальных символов

Чтобы посчитать символы (а не байты) в UTF-8, нужен декодер. Стандартная библиотека предоставляет std.unicode.Utf8View для итерации по кодовым точкам. Это важно: наивный проход по байтам разрежет многобайтовый символ.

// итерация по кодовым точкам, а не по байтам
var it = (try std.unicode.Utf8View.init("Привет")).iterator();
var count: usize = 0;
while (it.nextCodepoint()) |_| count += 1;
// count == 6 (символов), хотя байт было 12

Как работает под капотом

UTF-8 кодирует символы переменным числом байтов: ASCII — 1 байт, кириллица — 2, многие иероглифы — 3, эмодзи — 4. Поэтому индексация s[i] даёт байт, а не символ, и «разрезать» строку по произвольному байту опасно — можно попасть в середину символа. Декодер UTF-8 читает байты группами по правилам кодировки и выдаёт корректные кодовые точки.

Частые ошибки

Первая — считать s.len числом символов: это число байтов. Вторая — сравнивать строки через ==: для срезов это сравнение указателей, а не содержимого; нужен std.mem.eql. Третья — резать UTF-8 строку по произвольному индексу и портить многобайтовый символ.

Итог

  • Строка в Zig — это []const u8, срез байтов; отдельного строкового типа нет.
  • Литералы хранятся в UTF-8; s.len — длина в байтах, не в символах.
  • Сравнивают строки через std.mem.eql, ищут через std.mem.indexOf.
  • Для подсчёта символов нужен UTF-8-декодер (std.unicode.Utf8View).
Проверьте себя
1. Что вернёт s.len для строкового литерала "Привет" в Zig?
A6 — число символов
B12 — число байтов (каждая кириллическая буква — 2 байта в UTF-8)
C0
DЗависит от платформы
2. Как правильно сравнить две строки по содержимому в Zig?
AЧерез оператор ==
BЧерез std.mem.eql(u8, a, b)
CЧерез a.equals(b)
DЧерез strcmp