7. Строки, символы и байты
Различают строку и char, UTF-8 длину в байтах, NUL, поиск и двоичные данные.
Wave Foundation
буквы на экране и байты в памяти
Вы видите буквы на экране, но в памяти хранятся байты. Особенно в тех случаях, когда один символ имеет несколько байтов UTF-8, например, в корейском языке, легко допустить ошибку, если «длина» и «количество символов» используются как взаимозаменяемые слова.
В этой главе различаются str и char, заканчивающиеся NUL, escape, местоположение поиска и двоичные данные. Каждый из примеров представляет собой полную программу.
Строковые литералы и вывод
fun main() {
var greeting: str = "안녕하세요";
println("{}", greeting);
println("line one\nline two");
println("quote: \"Wave\"");
}
Результат выполнения:
안녕하세요
line one
line two
quote: "Wave"
Обычные символы в двойных кавычках обозначаются как UTF-8. escape обозначает байты, которые сложно записать непосредственно из источника. \n — это байт разрыва строки, который не печатает два символа, обратную косую черту и n. Чтобы напечатать саму обратную косую черту, используйте \\.
Длина – это количество байтов.
import("std::string::len")::{
len
};
fun main() {
println("ASCII={}", len("Wave"));
println("Korean={}", len("한"));
println("mixed={}", len("Wave한"));
}
Результат выполнения:
ASCII=4
Korean=3
mixed=7
len считает байты до завершающего NUL, а не видимые символы. Символ 한 занимает три байта в UTF-8. Количество символов, количество кодовых точек Unicode и количество байтов обычно не являются взаимозаменяемыми. Ширина дисплея также зависит от таких факторов, как шрифты и сочетание символов.
Следовательно, функции, которые разрезают строку в произвольной позиции байта и отображают ее на экране, должны отдельно учитывать границу Unicode. Четко определите условия ввода, будь то программа, обрабатывающая только текст ASCII или общий текст Unicode.
NUL Конец и длина
str использует 0 байт для обозначения конца. len не включает в длину последний байт. Помещение NUL внутри строкового литерала является ошибкой. Вот пример намеренной ошибки:
fun main() {
var text: str = "left\x00right";
}
\xNN в источнике указывает один байт, содержащий ровно две шестнадцатеричные цифры. \x41 представляет байт A, который равен 65. Поскольку запись обычного символа как UTF-8 и вставка произвольного байта различны, не все str, которые можно записать как \xNN, действительны UTF-8.
char не содержит весь символ Unicode
char — 8-битное символьное значение без знака. Вы можете использовать литералы, представляющие значения в однобайтовом диапазоне, например 'A'. '한' — это ошибка, поскольку она не попадает в этот диапазон. "한" представляет собой отдельный str с несколькими байтами UTF-8.
Не пытайтесь всегда помещать одну букву в одну char. Сначала вы должны решить, являются ли единицы измерения, необходимые для обработки текста, байтами или кодовыми точками Unicode.
сравнение строк
Чтобы сравнить содержимое строки, используйте функцию std. Ниже представлена программа, которая проверяет идентичное содержимое и различия в регистре.
import("std::string::cmp")::{
eq, starts_with, ends_with
};
fun main() {
if (eq("Wave", "Wave")) {
println("same bytes");
}
if (!eq("Wave", "wave")) {
println("case differs");
}
if (starts_with("report.txt", "report") && ends_with("report.txt", ".txt")) {
println("name matches");
}
}
Результат выполнения:
same bytes
case differs
name matches
Это сравнение сравнивает строки байтов. Он не выполняет автоматическое преобразование регистра для конкретного языка или нормализацию Unicode. Даже при сравнении имен файлов правила равенства имен файлов в OS не совпадают с простыми сравнениями строк.
Единицы и неудачи результатов поиска
import("std::string::find")::{
find, count
};
fun main() {
var first: i32 = find("banana", "na");
var missing: i32 = find("banana", "xy");
var matches: i32 = count("aaaa", "aa");
println("first={} missing={}", first, missing);
println("matches={}", matches);
}
Результат выполнения:
first=2 missing=-1
matches=2
find возвращает первую позицию или -1. Индекс 0 также является успешным, поэтому он проверяется с помощью result >= 0. count — это не позиция, а количество непересекающихся совпадений. Выше aa — это номер 2, поскольку он соответствует 0–1 и 2–3.
Бин needle также является частью контракта. find возвращает 0, contains возвращает true и count возвращает 0. Не думайте, что только потому, что имя функции находится в том же модуле, даже метод возврата один и тот же.
Удаление пробелов отличается от создания новой строки
trim_range возвращает диапазон без пробелов без изменения или копирования исходного текста. Поскольку мы получаем указатель вывода, мы сначала подготавливаем целое число для хранения результата.
import("std::string::trim")::{
trim_range
};
fun main() {
var start: i32 = 0;
var end: i32 = 0;
trim_range(" Wave ", &start, &end);
println("start={} end={} bytes={}", start, end, end - start);
}
Результат выполнения:
start=2 end=6 bytes=4
Диапазон: [start, end). Он включает в себя начало, но не конец, поэтому его длина равна end-start. Добавление start к начальному адресу оригинала не приводит автоматически к созданию NUL в местоположении end. Вам придется носить с собой диапазон отдельно или подготовить новое место для струн.
Двоичные данные имеют отдельную длину.
На данные, содержащие нули, не распространяются правила конца строки. Он использует байтовые массивы и длины.
fun main() {
var bytes: array<u8, 3> = [65, 0, 66];
for (var index: i32 = 0; index < 3; index += 1) {
println("{}", bytes[index]);
}
}
Результат выполнения:
65
0
66
Второй 0 — это фактические данные. Если вы интерпретируете это как str, оно рассматривается как заканчивающееся первым 0, и вы не можете увидеть следующие 66. И наоборот, если вы измените массив без NUL на str с cast, существует риск чтения за пределами массива. cast не является операцией добавления байта завершения.
Упражнение: Проверка имен файлов
Проверьте, заканчивается ли имя файла на .wave, и если строка содержит test, выведите ее в тестовый файл. Это упражнение проверяет только наличие шаблонов байтов в имени и не учитывает фактическое существование файла.
Полное решение
import("std::string::cmp")::{
ends_with
};
import("std::string::find")::{
contains
};
fun classify(name: str) {
if (!ends_with(name, ".wave")) {
println("other file");
return;
}
if (contains(name, "test")) {
println("Wave test file");
} else {
println("Wave source file");
}
}
fun main() {
classify("main.wave");
classify("parser_test.wave");
classify("notes.txt");
}
Результат выполнения:
Wave source file
Wave test file
other file
Существуют отдельные правила относительно того, как обрабатывать заглавную букву .WAVE и считать ли ее проверкой, даже если test включен во весь путь. Даже если это небольшая функция, ее работу можно точно описать, только если определить, на какой вход она нацелена.