[Перевод] Декларативное управление памятью
(достаточно вольный перевод огромной статьи, которая на практике наводит мосты между возможностями Си и Rust в плане решения бизнес-задач и разрешение багов, связанных с ручным управлением памятью. Также должно быть полезно и людям с опытом сборки мусора — отличий в плане практики намного меньше, чем может показаться — прим.пер.)
Кажется, с момента, когда я заинтересовался Rust, прошла вечность, тем не менее я отчетливо помню знакомство с анализатором заимствований (borrow checker, далее — БЧ — прим.пер.), сопровождаемое головной болью и отчаянием. Разумеется я не один такой страдающий — статей в интернете на тему общения с БЧ предостаточно. Однако я хотел бы выделиться и осветить в данной статье БЧ с точки зрения практической пользы, а не только лишь генератора головной боли.
Периодически мной встречаются мнения, что в Rust — ручное управление памятью (вероятно, раз не автоматическое с GC, тогда какое же еще? — прим.пер.), однако я совершенно не разделяю данную точку зрения. Способ, примененный в Rust, я называю термином «декларативное управление памятью». Почему так — сейчас покажу.
Правила оформления
Вместо того, чтобы теоретизировать, давайте напишем что-то полезное. Встречайте Овербук — издательство художественной литературы!
Как у любого издательства, в Овербуке есть правила оформления. Точнее, правило всего одно, простое как двери — никаких запятых. Овербук искренне считает, что запятые суть последствие авторской лени и — цитата — «должны быть истреблены как явление». К примеру, фраза «Она прочла и рассмеялась» — хорошая, годная. «Она прочла, после чего рассмеялась» — требует коррекции.
Проще некуда, казалось бы, однако в Овербуке регулярно ловят авторов на патологическом несоблюдении данного правила! Как будто такого правила вообще не существует, возмутительно! Приходится все перепроверять. Вручную. Более того, если по черновику издательством запрашивается правка, автор может прислать версию, исправленную в одном, но испорченную в другом месте, и поэтому все приходится перепроверять с самого начала. Бизнес такого халатного отношения к рабочему времени не терпит, и сама собой возникла необходимость процесс отлова «авторской лени» автоматизировать. Например, компьютерной программой. Можно же, да?
Робин спешит на выручку
Робин — один из сотрудников издательства, который вызвался помочь с написанием программы, так как знал программирование — вот это удача! Правда, в универе все, включая Робина, учили Си и Java, а Java в издательстве устанавливать беспричинно запретили — вот так поворот! Ну пусть будет Си, на Си много чего написано, язык уверен и проверен. Все пойдет как надо, инфа 100%.
#include
int main() {
printf("Зашибца.");
return 0;
}
Успех почти в кармане. Но Робин опытный и ожидает в будущем трудности, и запиливает Makefile:
.PHONY: all
all:
gcc -Wall -O0 -g main.c -o main
./main
Ничего военного:
- Варнинги включены
- Не оптимизировать
- Дебажное инфо
- Запустить после компиляции
Пока что все зашибца:
$ make
gcc -Wall -O0 -g main.c -o main
./main
Зашибца.
Робин осмелел. «Нам нужные еще некоторые функции и типы данных!»
// пропустим: #include directives
struct Mistake {
// человеко-читаемое описание ошибки:
char *message;
};
struct CheckResult {
// имя проверяемого файла
char *path;
// NULL если проблем в файле нет
// иначе мы здесь опишем проблему
struct Mistake *mistake;
};
struct CheckResult check(char *path, char *buf) {
struct CheckResult result;
result.path = path;
result.mistake = NULL;
// TODO(Robin): прочесть файл
// TODO(Robin): проверить ошибки
return result;
}
// пропустим: main()
«Нам нужен буфер» — продолжал Робин — «Мы в основном работаем с повестями, нам 256КБ должно хватить».
#define BUF_SIZE 256 * 1024
int main() {
char buf[BUF_SIZE];
struct CheckResult result = check("sample.txt", buf);
if (result.mistake != NULL) {
printf("У нас проблемы!");
return 1;
}
return 0;
}
Робин регулярно читает мой блог, и потому, несмотря на отсутствие постоянной практики программирования, смог безошибочно с первого раза написать функцию чтения файлов:
struct CheckResult check(char *path, char *buf) {
struct CheckResult result;
result.path = path;
result.mistake = NULL;
FILE *f = fopen(path, "r");
size_t len = fread(buf, 1, BUF_SIZE - 1, f);
fclose(f);
// не забыть про нулл-терминаторы в строках, это же Си, блин.
buf[len] = '\0';
// TODO(Robin): поискать ошибки
return result;
}
В Овербук принимаются только тексты на литературном английском языке, потому формат принимаемых файлов исключительно *.txt, и переживать по поводу всяких глупых кодировок не стоит. Один раз кто-то пытался протащить в тексте эмодзи, но больше ни этого автора, ни эмодзи в тексте никто никогда не видел…
Энивей, в коде остался всего один TODO — это ли не признак скорого успеха? Робин был горд как никогда. Однако впереди еще была задача выделения памяти:
// надо для malloc
#include
// пропустим: structs, etc.
struct CheckResult check(char *path, char *buf) {
struct CheckResult result;
result.path = path;
result.mistake = NULL;
FILE *f = fopen("sample.txt", "r");
size_t len = fread(buf, 1, BUF_SIZE - 1, f);
fclose(f);
buf[len] = '\0';
// Робин в теме C99, и не портит вид функции непонятными
// определениями переменных, определяя то, что нужно для цикла,
// только в цикле.
for (size_t i = 0; i < len; i++) {
if (buf[i] == ',') {
struct Mistake *m = malloc(sizeof(Mistake));
m->message = "запятые нельзя!";
result.mistake = m;
break;
}
}
return result;
}
Для проверки написанного Робин сделал выжимку из нетленной классики Льюиса Кэррола «Бармаглот» (в русском переводе Дины Орловской — прим.пер.) в файле sample.txt
:
'О, бойся Бармаглота, сын!
Натравил его на программу:
$ make
gcc -Wall -O0 -g main.c -o main
./main
У нас проблемы!
В файле sample2.txt
запятых уже не было — для беспроблемного примера:
Он стал под дерево и ждёт.
И действительно:
$ make
gcc -Wall -O0 -g main.c -o main
./main
Проблем не было! Окрыленный успехом Робин отдал программу в релиз и быстро и решительно сбежал на недельку в Европу отдыхать от проделанной работы.
А через неделю…
А через неделю размякшего после отпуска Робина ожидало в корпоративной почте письмо примерно следующего содержания:
Программа работает безукоризненно, определяя, есть ли в тексте ошибка. Однако нам поступило множество запросов от редакторов ее улучшить, возможностью показать, где же именно в тексте произошла ошибка.
Да, мы совершенно уверены, что редакторы в основной своей массе редкостные лентяи и при возможности спихнут с себя любой труд. Однако не мог бы ты все же уделить этой проблеме время?
Робин открыл уже немного подзабытый проект. «Агамсь. Я могу добавить в Mistake указатель на, собственно, ошибку.». Сказано — сделано:
struct Mistake {
char *message;
// указывает на ошибку
char *location;
}
// ...
struct CheckResult check(char *path, char *buf) {
// пропустим: определение 'result'
// пропустим: чтение файла
for (size_t i = 0; i < len; i++) {
if (buf[i] == ',') {
struct Mistake *m = malloc(sizeof(struct Mistake));
// НОВИНКА: локация проблемы
m->location = &buf[i];
m->message = "запятые нельзя!";
result.mistake = m;
break;
}
}
return result;
}
Надо еще эту инфу напечатать:
void report(struct CheckResult result) {
printf("\n");
printf("~ %s ~\n", result.path);
if (result.mistake == NULL) {
printf("Ошибок нет!!\n");
} else {
// внимание: "%s" печатает вообще все.
// а нам надо контекст и первых 12 символов текста, или меньше,
printf(
"проблема: %s: '%.12s'\n",
result.mistake->message,
result.mistake->location
);
}
}
int main() {
char buf[BUF_SIZE];
{
struct CheckResult result = check("sample.txt", buf);
report(result);
}
{
struct CheckResult result = check("sample2.txt", buf);
report(result);
}
}
Нормас, решил Робин. Проверим:
$ make
gcc -Wall -O0 -g main.c -o main
./main
~ sample.txt ~
проблема: запятые нельзя!: ', бойся Барм'
~ sample2.txt ~
Ошибок нет!!
Хорошо. А что, если еще более упростить редакторам задачу и проверять все тексты сразу, а в конце выдавать все ошибки?
#define MAX_RESULTS 128
// пропустим: все, что точно работает
int main() {
char buf[BUF_SIZE];
struct CheckResult bad_results[MAX_RESULTS];
int num_results = 0;
char *paths[] = { "sample2.txt", "sample.txt", NULL };
for (int i = 0; paths[i] != NULL; i++) {
char *path = paths[i];
struct CheckResult result = check(path, buf);
bad_results[num_results++] = result;
}
for (int i = 0; i < num_results; i++) {
report(bad_results[i]);
}
}
Робин выдохнул. Не то чтобы программисты не любили программирование — просто никогда нельзя знать наверняка, правильно ли работает программа. Но именно здесь вроде все прекрасно.
$ make
gcc -Wall -O0 -g main.c -o main
./main
~ sample2.txt ~
Ошибок нет!!
~ sample.txt ~
проблема: запятые нельзя!: ', бойся Барм'
Приключения продолжаются
На следующий день Робин находит письмо:
Приветы, Робин,
Очень классная получилась программа, с помощью нее мы сэкономили уйму времени, особенно с новой фичей, когда можно скормить ей все тексты сразу! Только с отображением места ошибки какая-то шляпа — оно показывает неверный кусок. Глянешь?
«Даблин!» — воскликнул Робин — «У меня же все работало!»
Некоторое время ушло на воспроизведение бага. В конце концов, переставив входные файлы местами, Робин получил ошибку:
int main() {
// пропустим: неважный кусок
// тут было "sample2.txt", "sample.txt"
char *paths[] = { "sample.txt", "sample2.txt", NULL };
for (int i = 0; paths[i] != NULL; i++) {
// пропустим проверку
}
for (int i = 0; i < num_results; i++) {
report(results[i]);
}
}
```bash
$ make
gcc -Wall -O0 -g main.c -o main
./main
~ sample.txt ~
проблема: запятые нельзя!: 'н стал под д'
~ sample2.txt ~
Ошибок нет!!
Еще какое-то время ушло на вдумчивый осмотр кода. Наконец, отойдя к кофемашине, Робин внезапно понял:
— Итить вы бестолочь, сударь. У меня же общий буфер на все файлы! Я перезаписал его содержимое беспроблемным текстом, а указатель-то с проблемного файла — остался!
Находкой срочно надо было с кем-то поделиться, но ёлки, тут же издательство, а не аутсорс, Робина никто с его проблемами не понимал до конца, хотя его находку все подтверждали:
— Да, точно. Все ошибки были с текстом из самого последнего файла. Зуб даю.
Теперь это бы починить. Выделение отдельного буфера каждому файлу вообще не выход — никакой оперативки не хватит. Отдавать ошибку сразу после отлова — значит отменить последнюю полюбившуюся фичу и кормить прогу по одному файлу по старинке.
«Может тупо копировать только кусок с ошибкой куда-то?» — подумал Робин.
// еще инклуд, для memcpy
#include
struct CheckResult check(char *path, char *buf) {
// пропустим: то, что работает
for (size_t i = 0; i < len; i++) {
if (buf[i] == ',') {
struct Mistake *m = malloc(sizeof(struct Mistake));
// копируем максимум 12 символов в "m->location"
size_t location_len = len - i;
if (location_len > 12) {
location_len = 12;
}
m->location = malloc(location_len + 1);
memcpy(m->location, &buf[i], location_len);
m->location[location_len] = '\0';
m->message = "запятые нельзя!";
result.mistake = m;
break;
}
}
return result;
}
Для того, чтоб проблема никогда больше не повторилась, был добавлен новый тестовый файл, sample3.txt
:
Но взял он меч, и взял он щит
Опля, работает!
$ make
gcc -Wall -O0 -g main.c -o main
./main
~ sample.txt ~
проблема: запятые нельзя!: ', бойся Барм'
~ sample2.txt ~
Ошибок нет!!
~ sample3.txt ~
mistake: commas are forbidden: ', и взял он '
Кореш директора
Разумеется, долго праздник продолжаться не мог — новое письмо от СЕО на следующий день:
Привет, Робин. В чатике Тим,
Я тут показал нашу программку приятелю. Ему дико понравилось, но он заметил, что у нас течет память. Понимаю, что мы в первую очередь издательство, и утечки памяти нам вроде безразличны, но ты мог бы все же глянуть, в чем дело?
«Наша песня хороша», вздохнул Робин и полез открывать проект. Ага, ну да, мы память выделяем с malloc()
, а возвращать ее системе с free()
не возвращаем. Ну так она должна автоматически освобождаться при завершении программы, не так ли? Робин вроде похожее читал в блоге, но инфа не 100%, к тому же босс вряд ли бы напрягся только из-за того, что кто-то что-то там сказал или написал. В любом случае, надо исправить.
int main() {
char buf[BUF_SIZE];
struct CheckResult results[MAX_RESULTS];
int num_results = 0;
// пропустим: то, что работает
// вернем память системе!
for (int i = 0; i < num_results; i++) {
free(results[i].mistake);
}
}
Теперь точно все. Остальное — buf
и results
определены на стеке и не нуждаются в ручном освобождении. Робин уже было расслабился, как вдруг за спиной появился незнакомец.
— Привет, ты Робин?
— Да, а ты кто?
— Я Джеф, друг Тима. У тебя очень крутая прога.
— Ой, привет, Джеф! Спасибо. Да, и спасибо за утечку. Я ее как раз только что поправил.
— Класс. А можно посмотреть?
Эээ. Ну ладно. Пренебрегая всеми правилами конфиденциальности компании, Робин пустил незнакомого человека за ноутбук. Кореш директора, в конце концов. Через минуту Джеф ответил:
— Почти получилось. Надо добавить еще один free()
, и все, в программу можно будет никогда не лезть.
С одной стороны, Робина раздражало, что кто-то прерывает его работу без предупреждения, с другой стороны, он веселился, представив себе новый знак качества «Джеф одобряэ», на случай когда/если его программка снова сломается, и ее снова придется чинить.
int main() {
// ну вы поняли...
// Больше освобождения памяти богу освобождения памяти!
for (int i = 0; i < num_results; i++) {
free(results[i].mistake->location);
free(results[i].mistake);
}
}
— Ты это, не переживай. Вызов free()
не рекурсивный, освобождает только то, что ты в него передал, прозевать такое просто, как два байта переслать.
Вообще-то я не программист в компании, начнем с этого, подумал Робин. Все пишется на энтузиазме. Я не трогал Си уже черте-сколько, ну короче, Джеф, все ок, замяли. Джеф тем временем продолжал:
— Главное, не перепутай эти вызовы free()
. Иначе будет сегфолт.
Робин был вот совсем не уверен, что тут будет именно сегфолт, и вообще, есть же системы, в которых мало того что сегфолтов в принципе не бывает, так и системы, которые не говорят про сегфолт пользователю. Даже Джефу. Пусть живет с этим. А то шибко собой доволен.
— Да-да, безусловно, конечно, спасибо, досвидания.
Спустя год
Робин уже было заскучал. Начальство довольно, программа работает, редакторы эффективны, утечек нет. Овербук усиленно готовился к крупному релизу, и Робин как раз обдумывал рекламную кампанию. Словом, все настолько спокойно, что не могло так долго продолжаться.
Действительно, Робина срочно вызывают на совещание.
— Робин, нам кранты. Ты знаешь, я не люблю такое.
Робин не совсем понимал, что именно не любит Тим. Точно не любит выражаться. Может что-то еще. Может, не любит Си?
— А в чем, собственно, дело?
— Ни черта не работает. Проверялка вчера работала, а сегодня нет.
За последний год поделие Робина стало играть ключевую роль в издательском бизнесе Овербука. Сейчас только после прогона через Проверялку (ей дали название!) издание допускалось к публикации. Это значит, что пока Проверялся не работала, Овербук терял деньги. Десятки долларов в минуту.
Робин немедленно сел за починку. Код выглядел до последнего символа тем же, каким он оставил его год назад. Функционал не изменился, соответственно. Ломаться было нечему.
— Секундочку, — мелькнуло у Робина в голове, — А сколько текстов одновременно загружается в проверялку?
— На этой неделе завал, не меньше 130 текстов в очереди.
А, ну да.
#define MAX_RESULTS 128
int main() {
char buf[BUF_SIZE];
struct CheckResult results[MAX_RESULTS];
// ...
}
Казалось бы вот, проблема найдена, но Робин знал, что настоящая проблема только чудом не вылезла до сих пор. За пределы results
Проверялка вылезти не должна (это чревато переполнением буфера, но операционка за этим бдит). Но лишь чудом переменные были определены именно в этом порядке, не позволяя results
выходить за пределы стека. Будь buf
определен позже, ссылки наresults
перетекли бы в него, указывая на произвольные участки памяти, показывая пользователю произвольный текст вместо ошибок, и все репутация Овербука пошла бы по грибы. Так оставлять было нельзя. Робин в качестве быстрой заплатки поднял значение MAX_RESULTS
до 256. На ближайшее время бизнес был спасен.
— Робин. Ты спас нам всем задницы. Вот в такие моменты мне становится неловко, что мы платим тебе минималку. Не настолько неловко, чтобы почесаться и как-то это изменить, но достаточно, чтобы сожалеть. Ну ты понял. Фрустрация.
Робину тем временем было не до этого. Амбиции компании легко могут преодолеть барьер в 256 текстов за раз, не сегодня, так через неделю. Проблема не решена. Программу нужно было полностью переписать.
«Да нахрен.» — произнес про себя Робин, — «Переписываю на Rust.»
Книгой спустя
(видимо, под книгой имеется в виду растбук — прим.пер.)
Робин ввел в консоль cargo new checker
и открыл src/main.rs
. Удобно. Теперь надо добавить чтение из файла:
use std::fs::read_to_string;
fn main() {
let s = read_to_string("sample.txt");
}
«Лол, похоже на Ruby!» — за спиной явился Джеф, какого-то лешего тусящий в офисе издательства. Робин промолчал. Хрен тебе, Джеф, а не Ruby. Ты не в теме вообще.
«Так, что у нас тут с поиском запятых?»
fn main() {
let s = read_to_string("sample.txt");
s.find(",");
}
Не взлетело, понял Робин по сообщению компилятора:
error[E0599]: no method named `find` found for type `std::result::Result` in the current scope
--> src\main.rs:5:7
|
5 | s.find(",");
| ^^^^
Хм. Ошибку чтения файла проигнорировать не удалось — в типе Result
метода find()
нет, он есть только в String
. Робин обязан обработать ошибку чтения.
fn main() -> Result<(), std::io::Error> {
let s = read_to_string("sample.txt")?;
s.find(",");
Ok(())
}
Теперь мы показываем, что main()
тоже может упасть, и ошибку из read_to_string()
мы перенаправляем в main()
, а та роняет всю программу. Это уже выглядело обнадеживающе, даже несмотря на то, что программа по сути ничем полезным не занималась. Можно пойти дальше и посмотреть, что возвращает find()
:
fn main() -> Result<(), std::io::Error> {
let s = read_to_string("sample.txt")?;
let result = s.find(",");
println!("Result: {:?}", result);
Ok(())
}
$ cargo run --quiet
Result: Some(22)
Ага. Если оно что-то найдет, вернет Option::Some(index)
, а если нет, то Option::None
. Сюда можно применить сопоставление с образцом (паттерн матчинг, если понятным языком — прим.пер.):
fn main() -> Result<(), std::io::Error> {
let path = "sample.txt";
let s = read_to_string(path)?;
println!("~ {} ~", path);
match s.find(",") {
Some(index) => {
println!("проблема: запятые нельзя: символ №{}", index);
},
None => println!("Ошибок нет!!"),
}
Ok(())
}
$ cargo run --quiet
~ sample.txt ~
проблема: запятые нельзя: символ №22
Робин читал Песнь Льда и Пламени, потому понимал, что показывать порядковый номер символа в тексте размером с этот слегка бесполезно, даже если ты автор текста. Нужно вернуть отображение позиции с контекстом.
fn main() -> Result<(), std::io::Error> {
let path = "sample.txt";
let s = read_to_string(path)?;
println!("~ {} ~", path);
match s.find(",") {
Some(index) => {
// так, эта штука ест вообще всю строку,
// не только первые 12 символов,
// но проблем быть не должно
let slice = &s[index..];
println!("проблема: запятые нельзя: {:?}", slice);
}
None => println!("Ошибок нет!!"),
}
Ok(())
}
$ cargo run --quiet
~ sample.txt ~
проблема: запятые нельзя: ', бойся Бармаглота, сын!'
Робин готов был обрыдаться от счастья. Нет malloc()
! Нет memcpy()
! Модный и молодежный синтаксис для нарезания слайсов (ну в Go он тоже есть, например — прим.пер.)! Символьные слайсы можно форматировать с {:?}
, и макрос знает, что внутри строка, и сам окружает кавычками. Озадачило, что free()
тоже не было. В книге было написано, что память освобожается самостоятельно сразу же, как только переменная выходит за пределы области видимости — то есть s
освобождается сразу после match
. Слайс (&s[index..]
) тоже не выделяет под себя память, но является просто частью изначального массива символов, вычитанного из файла. Вообще надо перечитать этот кусок про память, подумал Робин, а то уже все подзабылось.
Сокращаем разрыв
Смешение всего в main()
и общая неструктурированность нового кода Робина раздражалы, потому он взялся за рефактор, начав с выделения функции check()
:
fn main() -> Result<(), std::io::Error> {
check("sample.txt")?;
Ok(())
}
fn check(path: &str) -> Result<(), std::io::Error> {
let s = read_to_string(path)?;
println!("~ {} ~", path);
match s.find(",") {
Some(index) => {
let slice = &s[index..];
println!("проблема: запятые нельзя: {:?}", slice);
}
None => println!("Ошибок нет!!"),
}
Ok(())
}
Что показательно, рефактор не привел к изменению результатов тестирования. Так и надо.
Также было замечено, что новая Проверялка на каждый новый файл выделяет отдельный буфер, то есть аллокаций памяти больше. Подгоним управление памятью под версию Си:
fn main() -> Result<(), std::io::Error> {
let mut s = String::with_capacity(256 * 1024);
check("sample.txt", &mut s)?;
Ok(())
}
fn check(path: &str, s: &mut String) -> Result<(), std::io::Error> {
use std::fs::File;
use std::io::Read;
s.clear();
File::open(path)?.read_to_string(s)?;
println!("~ {} ~", path);
match s.find(",") {
Some(index) => {
let slice = &s[index..];
println!("проблема: запятые нельзя: {:?}", slice);
}
None => println!("Ошибок нет!!"),
}
Ok(())
}
В восторг Робина привели следующие вещи:
- несмотря на изначальный размер буфера
s
в 256 КБ, при необходимости он может самостоятельно расширяться. - существенно более читаемый код чтения файла — сразу видно, где и какие ошибки могут выскочить: при открытии файла, и при непосредственном его чтении.
- Можно, в отличие от
#include
, лепитьuse
ровно туда, где его содержимое использовалось: типажRead
и его функцияread_to_string
используется только внутриcheck()
.
Миграция, однако, еще не завершена. В версии Си check()
возвращал CheckResult
, в котором мог быть Mistake
. В версии Rust Робину ничего не мешало убрать CheckResult
вовсе, заменив его на Option
. Впрочем, Mistake
все равно нужен. Робин закатал рукава:
struct Mistake {
location: &str,
}
Но нет. У компилятора были другие планы.
$ cargo run --quiet
error[E0106]: missing lifetime specifier
--> src\main.rs:10:15
|
10 | location: &str,
| ^ expected lifetime parameter
«Хрень», подумал Робин, «об этом меня и предупреждали!», но общая элегантность уже написанного кода убедила его не бросить все на полпути, а вернуться к книге. Книга в этом вопросе оказалась на редкость неуклюжей в плане объяснения, и все, что Робин оттуда выудил — эта хрень зовется временем жизни (здесь и далее — ВЖ — прим.пер.). Пришлось покурить форумы, и вскоре Робин вернулся с решением:
struct Mistake<'a> {
location: &'a str,
}
Факт того, что этот стремный код собрался, настолько впечатлила Робина, что он бросился дореализовывать остальные части из версии Си, хотя по хорошему стоило бы остановиться и почитать больше теории по этим самым ВЖ. Если ВЖ настолько важны, я на них еще наткнусь, решил он мимоходом.
В общем, сейчас check()
возвращает Option
вместо ()
(спецтип, который возвращается, если из функции ничего не возвращается — прим.пер.):
fn check(path: &str, s: &mut String) -> Result
Понимание ВЖ Робину пока не особо давалось, зато окружающие концепции он схватывал на лету. Вот например — если функция заканчивается выражением, значение выражения возвращается из функции. match
— выражение, потому Ok(match ...)
взлетает. Это было слишком круто, чтобы не попробовать его применить к main()
:
fn main() -> Result<(), std::io::Error> {
let mut s = String::with_capacity(256 * 1024);
let result = check("sample.txt", &mut s)?;
if let Some(mistake) = result {
println!("проблема: запятые нельзя: {:?}", mistake.location);
}
Ok(())
}
«По большому счету, отдельный result
мне тут тоже не нужен», защищал себя перед воображаемым ревьюером Робин, «но с ним все куда более читаемо». Еще как-то сразу зашел вот этот if let
, вроде и мелочь, можно было оставить и match
, но насколько аккуратней выглядит!
От винта!
$ cargo run --quiet
error[E0106]: missing lifetime specifier
--> src\main.rs:17:55
|
17 | fn check(path: &str, s: &mut String) -> Result
Невыученные уроки всегда наносят ответный удар, и от этого удара Робин рисковал не оправиться. К счастью, вовремя была замечена рука поддержки от компилятора:
Хелп: Эта функция возвращает тип с заимствованным значением, но в сигнатуре не указывается, было оно заимствовано изpath
или изs
.
Заимствованное? Этосамое, в Mistake
есть location: &'a str
— заимствование. Оно пришло из s
— одного из параметров. Теоретически их надо объединить одним и тем же ВЖ:
// все эти полные пути и мена типов писать утомительно:
use std::io::Error as E;
fn check(path: &str, s: &'a mut String) -> Result
Так, вроде правильно. Почему ВЖ названо именно 'a
? А фиг знает. Может других имен не бывает вовсе? А почему перед именем ВЖ одинарная кавычка, одна? Так надо, что ли? Надо будет опять мануал покурить, потом.
error[E0261]: use of undeclared lifetime name `'a`
--> src\main.rs:19:26
|
19 | fn check(path: &str, s: &'a mut String) -> Result
Компилер недоволен. Какое еще декларирование ВЖ? Робин пробежался по коду:
struct Mistake<'a> {
location: &'a str,
}
Здесь, решил он, компилятор знает про 'a
в location: &'a str
потому что 'a
определено в Mistake<'a>
, что в свою очередь страшно напоминает декларацию дженериков из Java. Хохохо. А можно ли подход дженериков из Java в методах применить к ВЖ из Rust?
fn check<'a>(path: &str, s: &'a mut String) -> Result
Вот это дела, взлетело!
$ cargo run --quiet
~ sample.txt ~
проблема: запятые нельзя: ", бойся Бармаглота, сын!"
Больше структур!
Передохнув минутку, Робин осмотрел код полностью:
fn main() -> Result<(), std::io::Error> {
let mut s = String::with_capacity(256 * 1024);
let path = "sample.txt";
let result = check(path, &mut s)?;
println!("~ {} ~", path);
if let Some(mistake) = result {
println!("проблема: запятые нельзя: {:?}", mistake.location);
}
Ok(())
}
struct Mistake<'a> {
location: &'a str,
}
use std::io::Error as E;
fn check<'a>(path: &str, s: &'a mut String) -> Result
Бесконечные 'a
в сигнатурах его напрягали, и вообще не было до конца понятно, на кой пес они там нужны. Впрочем, работает и ладно. Запилим report()
:
fn main() -> Result<(), std::io::Error> {
let mut s = String::with_capacity(256 * 1024);
let path = "sample.txt";
let result = check(path, &mut s)?;
report(path, result);
Ok(())
}
fn report(path: &str, result: Option) {
println!("~ {} ~", path);
if let Some(mistake) = result {
println!("проблема: запятые нельзя: {:?}", mistake.location);
} else {
println!("Ошибок нет!!");
}
}
Наверное, путь к файлу надо сделать частью Mistake
, тогда я смогу напилить к нему типаж Display
и красиво выводить инфо пользователю.
struct Mistake<'a> {
// пыщь!
path: &'static str,
location: &'a str,
}
Вовремя Робин вычитал, что для значений, которые живут всю программу, можно и нужно использовать ВЖ 'static
. Путь к файлу захардкожен в код, соответственно — жив всю программу от начала до конца. Заполним путь в check()
:
fn check<'a>(path: &str, s: &'a mut String) -> Result
Попутно решим уже знакомую проблемку:
$ cargo run --quiet
error[E0621]: explicit lifetime required in the type of `path`
--> src\main.rs:37:28
|
27 | fn check<'a>(path: &str, s: &'a mut String) -> Result
// new: &'static
fn check<'a>(path: &'static str, s: &'a mut String) -> Result
Вклеим обещаный Display
:
use std::fmt;
impl<'a> fmt::Display for Mistake<'a> {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(
f,
"{}: запятые нельзя: {:?}",
self.path, self.location
)
}
}
И подгоним под это все report()
:
fn report(result: Option) {
if let Some(mistake) = result {
println!("{}", mistake);
}
}
Хорошо, когда все работает :)
$ cargo run --quiet
sample.txt: запятые нельзя: ", бойся Бармаглота, сын!"
50 строк кода, включая пустые строки для красоты, пот этом содержание каждой предельно понятно. Если это не счастье, тогда что же?
Сразу несколько текстов
Ай, забыли совсем. Самое главное!
fn main() -> Result<(), std::io::Error> {
let mut s = String::with_capacity(256 * 1024);
let paths = ["sample.txt", "sample2.txt", "sample3.txt"];
for path in &paths {
let result = check(path, &mut s)?;
report(result);
}
Ok(())
}
$ cargo run --quiet
sample.txt: запятые нельзя: ", бойся Бармаглота, сын!"
sample3.txt: запятые нельзя: ", и взял он щит"
Даже не пришлось напрягаться. Но потом Робин вспомнил, что в версии Си ошибки сначала собирались со всех файлов, а потом выдавались в самом конце. Плевое дело.
fn main() -> Result<(), std::io::Error> {
let mut s = String::with_capacity(256 * 1024);
let paths = ["sample.txt", "sample2.txt", "sample3.txt"];
// все собери
let mut results = vec![];
for path in &paths {
let result = check(path, &mut s)?;
results.push(result);
}
// потом отчитайся
for result in results {
report(result);
}
Ok(())
}
Как тут вдруг откуда ни возмись…
$ cargo run --quiet
error[E0499]: cannot borrow `s` as mutable more than once at a time
--> src\main.rs:9:34
|
9 | let result = check(path, &mut s)?;
| ^^^^^^ mutable borrow starts here in previous iteration of loop
Паааадажжи. Чего-чего? Нельзя заимствовать s
для изменения более одного раза?
Это было что-то совсем уж новое. Заимствование, думал Робин, это ссылка на s
, то есть &s
. Заимствование для изменения — это, выходит, &mut s
. А зачем его заимствовать для изменения несколько раз? Ааа, у нас s
это буфер для чтения из файла:
fn check<'a>(path: &'static str, s: &'a mut String) -> Result
Да, и буфер у них один и тот же. Но это получается, что…
Компилятор Rust.
Не собирает программу.
С багом, который уже был в программе на Си!
Но Си его съел и выдавал в рантайме бред, а Rust считает ошибкой компиляции!
Екарный бабай.
Хорошо. Вероятно, вопрос можно решить способом, аналогичным Си. Только без memcpy()
, пожалуйста-пожалуйста.
В первую очередь надо было пересмотреть содержимое Mistake
. location
сейчас был ссылкой на общую строку, из которой росла по остальному коду зависимость от ВЖ строки. В то же время в Си Mistake
владела собственной строкой, в которой хранилась часть оригинала. Как нам завладеть строкой в Rust?
Спустя минуту у Робина было и решение — отдельный тип String
, который, в противовес ссылочному &str
был владельцем содержимого строки. Изначально Робин ни фига не понимал, зачем нужно два отдельных типа для строки, и просто принимал это как должное. Сейчас было куда понятней.
struct Mistake<'a> {
// это все еще ссылка, хоть и вечноживущая
path: &'static str,
// а вот это всецело принадлежит Mistake
location: String,
}
Компилятор, настроенный на активацию при каждом сохранении, встрепенулся:
error[E0392]: parameter `'a` is never used
--> src\main.rs:27:16
|
27 | struct Mistake<'a> {
| ^^ unused parameter
|
= help: consider removing `'a` or using a marker such as `std::marker::PhantomData`
«Да-да, я в курсе», сказал вслух Робин, удаляя <'a>
из структуры. Также оказались не нужны 'a
в Display
и check()
:
struct Mistake {
// это все еще ссылка, хоть и вечноживущая
path: &'static str,
// а вот это всецело принадлежит Mistake
location: String,
}
// ...
impl fmt::Display for Mistake {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(
f,
"{}: запятые нельзя: {:?}",
self.path, self.location
)
}
}
// ...
fn check(path: &'static str, s: &mut String) -> Result
Компилятор, однако, был неумолим:
error[E0308]: mismatched types
--> src\main.rs:43:34
|
43 | Some(Mistake { path, location })
| ^^^^^^^^
| |
| expected struct `std::string::String`, found &str
| help: try using a conversion method: `location: location.to_string()`
|
= note: expected type `std::string::String`
found type `&str`
Окай. location
и был изначально ссылкой на s
. А нам надо, чтобы это была отдельная строка с собственным содержимым. Робин рисовал пальцем на стене на воображаемой доске воображаемое расположение разных строк в воображаемой оперативной памяти, отчего коллеги по комнате периодически на него оглядывались. Впрочем, с тех пор, как от Робина стали слышаться слова «Rust» и «переписать», коллеги просто молча понимающе переглядывались и уходили назад в свою работу. Робин тем временем дорисовал воображаемые строки и вернулся к реальной проблеме — как получить строку во владение?
«У меня нет нигде malloc()
. Я вообще не видел, чтобы где-то выделял память! Что вообще происходит?» Уже привычным движением Робин переключился на браузер, где его ждала документация. Спустя минуту была найдена страница с интересным содержимым:
«Вроде как я могу использовать оба варианта. Типаж ToString
реализует преобразование именно в строку, а ToOwned
— для всех остальных типов из ссылки в отдельный экземпляр, которым можно завладеть. Робин поспешил применить свежее знание:
fn check(path: &'static str, s: &mut String) -> Result
```bash
$ cargo run --quiet
sample.txt: запятые нельзя: ", бойся Бармаглота, сын!"
sample3.txt: запятые нельзя: ", и взял он щит"
На этом задача считалась законченой. Робин отправил новую Проверялку в прод, а старую, написанную на Си, спрятал поглубже в дебри компа, чтоб никто случайно не нашел.
Прошло пять лет
Пять долгих лет Rust-версия Проверялки трудилась на благо Овербука, не получив ни упрека в свой адрес. Для софта, тем более написанного за вечер на коленке, это значимое достижение. Тем не менее ни мир, ни Овербук не стояли на месте, и, как читатель уже мог догадаться, Робин снова был приглашен на совещание. На