ВУЗ: Не указан
Категория: Не указан
Дисциплина: Не указана
Добавлен: 11.01.2024
Просмотров: 1152
Скачиваний: 5
ВНИМАНИЕ! Если данный файл нарушает Ваши авторские права, то обязательно сообщите нам.
СОДЕРЖАНИЕ
Листинг 8-22: Показывает, что ключи и значения находятся во владении HashMap, как только они были
вставлены
Мы не можем использовать переменные field_name и field_value после того, как их значения были перемещены в HashMap вызовом метода insert
Если мы вставим в HashMap ссылки на значения, то они не будут перемещены в
HashMap. Значения, на которые указывают ссылки, должны быть действительными хотя бы до тех пор, пока хеш-карта действительна. Мы поговорим подробнее об этих вопросах в разделе "Валидация ссылок при помощи времён жизни"
главы 10.
Обновление данных в HashMap
Хотя количество ключей и значений может увеличиваться в HashMap, каждый ключ может иметь только одно значение, связанное с ним в один момент времени (обратное утверждение неверно: команды "Blue" и "Yellow" могут хранить в хеш-карте scores одинаковое количество очков, например 10).
Когда вы хотите изменить данные в хеш-карте, необходимо решить, как обрабатывать случай, когда ключ уже имеет назначенное значение. Можно заменить старое значение новым, полностью игнорируя старое. Можно сохранить старое значение и игнорировать новое, или добавлять новое значение, если только ключ ещё не имел значения. Или можно было бы объединить старое значение и новое значение. Давайте посмотрим, как сделать каждый из вариантов!
Перезапись старых значений
Если мы вставим ключ и значение в HashMap, а затем вставим тот же ключ с новым значением, то старое значение связанное с этим ключом, будет заменено на новое. Даже несмотря на то, что код в листинге 8-23 вызывает insert дважды, хеш-карта будет содержать только одну пару ключ/значение, потому что мы вставляем значения для одного и того же ключа - ключа команды "Blue".
use std::collections::HashMap; let field_name =
String
::from(
"Favorite color"
); let field_value =
String
::from(
"Blue"
); let mut map = HashMap::new(); map.insert(field_name, field_value);
// field_name and field_value are invalid at this point, try using them and
// see what compiler error you get!
Листинг 8-23: Замена значения, хранимого в конкретном ключе
Код напечатает
{"Blue": 25}
. Начальное значение
10
было перезаписано.
Вставка значения только в том случае, когда ключ не имеет значения
Обычно проверяют, существует ли конкретный ключ в хеш-карте со значением, а затем предпринимаются следующие действия: если ключ существует в хеш-карте,
существующее значение должно оставаться таким, какое оно есть. Если ключ не существует, то вставляют его и значение для него.
Хеш-карты имеют для этого специальный API, называемый entry
, который принимает ключ для проверки в качестве входного параметра. Возвращаемое значение метода entry
- это перечисление
Entry
, с двумя вариантами: первый представляет значение,
которое может существовать, а второй говорит о том, что значение отсутствует.
Допустим, мы хотим проверить, имеется ли ключ и связанное с ним значение для команды "Yellow". Если хеш-карта не имеет значения для такого ключа, то мы хотим вставить значение 50. То же самое мы хотим проделать и для команды "Blue".
Используем API entry в коде листинга 8-24.
Листинг 8-24: Использование метода
entry
для вставки значения только в том случае, когда ключ не имеет
значения
Метод or_insert определён в
Entry так, чтобы возвращать изменяемую ссылку на соответствующее значение ключа внутри варианта перечисления
Entry
, когда этот ключ существует, а если его нет, то вставлять параметр в качестве нового значения этого ключа и возвращать изменяемую ссылку на новое значение. Эта техника намного чище,
чем самостоятельное написание логики и, кроме того, она более безопасна и согласуется с правилами заимствования.
use std::collections::HashMap; let mut scores = HashMap::new(); scores.insert(
String
::from(
"Blue"
),
10
); scores.insert(
String
::from(
"Blue"
),
25
); println!
(
"{:?}"
, scores); use std::collections::HashMap; let mut scores = HashMap::new(); scores.insert(
String
::from(
"Blue"
),
10
); scores.entry(
String
::from(
"Yellow"
)).or_insert(
50
); scores.entry(
String
::from(
"Blue"
)).or_insert(
50
); println!
(
"{:?}"
, scores);
При выполнении кода листинга 8-24 будет напечатано
{"Yellow": 50, "Blue": 10}
Первый вызов метода entry вставит ключ для команды "Yellow" со значением 50, потому что для жёлтой команды ещё не имеется значения в HashMap. Второй вызов entry не изменит хеш-карту, потому что для ключа команды "Blue" уже имеется значение 10.
Создание нового значения на основе старого значения
Другим распространённым вариантом использования хеш-карт является поиск значения по ключу, а затем обновление этого значения на основе старого значения. Например, в листинге 8-25 показан код, который подсчитывает, сколько раз определённое слово встречается в некотором тексте. Мы используем HashMap со словами в качестве ключей и увеличиваем соответствующее слову значение, чтобы отслеживать, сколько раз мы встретили это слово. Если мы впервые встретили слово, то сначала вставляем значение
0.
Листинг 8-25: Подсчёт количества вхождений слов с использованием хеш-карты, которая хранит слова и
счётчики
Этот код напечатает
{"world": 2, "hello": 1, "wonderful": 1}
. Если вы увидите, что пары ключ/значение печатаются в другом порядке, то вспомните, что мы писали в секции "Доступ к данным в HashMap"
, что итерация по хеш-карте происходит в произвольном порядке.
Метод split_whitespace возвращает итератор по срезам строки, разделённых пробелам,
для строки text
. Метод or_insert возвращает изменяемую ссылку (
&mut V
) на значение ключа. Мы сохраняем изменяемую ссылку в переменной count
, для этого,
чтобы присвоить переменной значение, необходимо произвести разыменование с помощью звёздочки (*). Изменяемая ссылка удаляется сразу же после выхода из области видимости цикла for
, поэтому все эти изменения безопасны и согласуются с правилами заимствования.
Функция хеширования
use std::collections::HashMap; let text =
"hello world wonderful world"
; let mut map = HashMap::new(); for word in text.split_whitespace() { let count = map.entry(word).or_insert(
0
);
*count +=
1
;
} println!
(
"{:?}"
, map);
По умолчанию
HashMap использует функцию хеширования SipHash, которая может противостоять атакам класса отказ в обслуживании, Denial of Service (DoS) с использованием хэш-таблиц
^siphash
. Это не самый быстрый из возможных алгоритмов хеширования, в данном случае производительность идёт на компромисс с обеспечением лучшей безопасности. Если после профилирования вашего кода окажется, что хеш- функция, используемая по умолчанию, очень медленная, вы можете заменить её
используя другой hasher. Hasher - это тип, реализующий трейт
BuildHasher
. Подробнее о типажах мы поговорим в Главе 10. Вам совсем не обязательно реализовывать свою собственную функцию хеширования; crates.io имеет достаточное количество библиотек,
предоставляющих разные реализации hasher с множеством общих алгоритмов хеширования.
Итоги
Векторы, строки и хеш-карты предоставят большое количество функционала для программ, когда необходимо сохранять, получать доступ и модифицировать данные.
Теперь вы готовы решить следующие учебные задания:
Есть список целых чисел. Создайте функцию, используйте вектор и верните из списка: среднее значение; медиану (значение элемента из середины списка после его сортировки); моду списка (mode of list, то значение которое встречается в списке наибольшее количество раз; HashMap будет полезна в данном случае).
Преобразуйте строку в кодировку "поросячьей латыни" (Pig Latin), где первая согласная каждого слова перемещается в конец и к ней добавляется окончание "ay".
Например "first" в поросячьей латыни станет "irst-fay". Если слово начинается на гласную, то в конец слова добавляется суффикс "hay" ("apple" становится "apple- hay"). Помните о деталях работы с кодировкой UTF-8!
Используя хеш-карту и векторы, создайте текстовый интерфейс позволяющий пользователю добавлять имена сотрудников к названию отдела компании.
Например, "Add Sally to Engineering" или "Add Amir to Sales". Затем позвольте пользователю получить список всех людей из отдела или всех людей в компании отсортированным в алфавитном порядке по отделам.
Документация API стандартной библиотеки описывает методы у векторов, строк и
HashMap. Рекомендуем воспользоваться ей при решении упражнений.
Потихоньку мы переходим к более сложным программам, в которых операции могут потерпеть неудачу. Наступило идеальное время для обсуждения обработки ошибок.
Обработка ошибок
Возникновение ошибок в ходе выполнения программ - это суровая реальность в жизни программного обеспечения, поэтому Rust имеет ряд функций для обработки ситуаций в которых что-то идёт не так. Во многих случаях Rust требует, чтобы вы признали возможность ошибки и предприняли некоторые действия, прежде чем ваш код будет скомпилирован. Это требование делает вашу программу более надёжной, гарантируя,
что вы обнаружите ошибки и обработаете их надлежащим образом, прежде чем развернёте свой код в производственной среде!
В Rust ошибки группируются на две основные категории исправимые (recoverable) и
неисправимые (unrecoverable). В случае исправимой ошибки, такой как файл не найден,
мы, скорее всего, просто хотим сообщить о проблеме пользователю и повторить операцию. Неисправимые ошибки всегда являются симптомами дефектов в коде,
например, попытка доступа к ячейке за пределами границ массива, и поэтому мы хотим немедленно остановить программу.
Большинство языков не различают эти два вида ошибок и обрабатывают оба вида одинаково, используя такие механизмы, как исключения. В Rust нет исключений. Вместо этого он имеет тип
Result
для обрабатываемых (исправимых) ошибок и макрос panic!
, который останавливает выполнение, когда программа встречает необрабатываемую (неисправимую) ошибку. Сначала эта глава расскажет про вызов panic!
, а потом расскажет о возврате значений
Result
. Кроме того, мы рассмотрим, что нужно учитывать при принятии решения о том, следует ли попытаться исправить ошибку или остановить выполнение.
Неустранимые ошибки с макросом panic!
Иногда в коде происходят плохие вещи, и вы ничего не можете с этим поделать. В этих случаях у Rust есть макрос panic! На практике существует два способа вызвать панику:
путём выполнения действия, которое вызывает панику в нашем коде (например,
обращение к массиву за пределами его размера) или путём явного вызова макроса panic!
. В обоих случаях мы вызываем панику в нашей программе. По умолчанию паника выводит сообщение об ошибке, раскручивает и очищает стек вызовов, и завершают работу. С помощью переменной окружения вы также можете заставить Rust отображать стек вызовов при возникновении паники, чтобы было легче отследить источник паники.
Раскручивать стек или прерывать выполнение программы в ответ
на панику?
По умолчанию, когда происходит паника, программа начинает процесс раскрутки
стека, означающий в Rust проход обратно по стеку вызовов и очистку данных для каждой обнаруженной функции. Тем не менее, этот обратный проход по стеку и очистка генерируют много работы. Rust как альтернативу предоставляет вам возможность немедленного прерывания (aborting), которое завершает работу программы без очистки.
Память, которую использовала программа, должна быть очищена операционной системой. Если в вашем проекте нужно насколько это возможно сделать маленьким исполняемый файл, вы можете переключиться с варианта раскрутки стека на вариант прерывания при панике, добавьте panic = 'abort'
в раздел [profile]
вашего
Cargo.toml файла. Например, если вы хотите прервать панику в режиме релиза, добавьте это:
Давайте попробуем вызвать panic!
в простой программе:
Файл: src/main.rs
При запуске программы, вы увидите что-то вроде этого:
[profile.release]
panic =
'abort'
fn main
() { panic!
(
"crash and burn"
);
}
Выполнение макроса panic!
вызывает сообщение об ошибке, содержащееся в двух последних строках. Первая строка показывает сообщение паники и место в исходном коде, где возникла паника: src/main.rs: 2:5 указывает, что это вторая строка, пятый символ внутри нашего файла src/main.rs
В этом случае указанная строка является частью нашего кода, и если мы перейдём к этой строке, мы увидим вызов макроса panic!
. В других случаях вызов panic!
мог бы произойти в стороннем коде, который вызывает наш код, тогда имя файла и номер строки для сообщения об ошибке будет из чужого кода, где макрос panic!
выполнен, а не из строк нашего кода, которые в конечном итоге привели к выполнению panic!
. Мы можем использовать обратную трассировку вызовов функций которые вызвали panic!
чтобы выяснить, какая часть нашего кода вызывает проблему. Мы обсудим обратную трассировку более подробно далее.
Использование обратной трассировки panic!
Давайте посмотрим на другой пример, где, вызов panic!
происходит в сторонней библиотеке из-за ошибки в нашем коде (а не как в примере ранее, из-за вызова макроса нашим кодом напрямую). В листинге 9-1 приведён код, который пытается получить доступ по индексу в векторе за пределами допустимого диапазона значений индекса.
Файл: src/main.rs
Листинг 9-1: Попытка доступа к элементу за пределами вектора, которая вызовет
panic!
Здесь мы пытаемся получить доступ к 100-му элементу вектора (который находится по индексу 99, потому что индексирование начинается с нуля), но вектор имеет только 3
элемента. В этой ситуации, Rust будет вызывать панику. Использование
[]
должно возвращать элемент, но вы передаёте неверный индекс: не существует элемента,
который Rust мог бы вернуть.
В языке C, например, попытка прочесть за пределами конца структуры данных (в нашем случае векторе) приведёт к неопределённому поведению, undefined behavior, UB. Вы всё
$
cargo run
Compiling panic v0.1.0 (file:///projects/panic)
Finished dev [unoptimized + debuginfo] target(s) in 0.25s
Running `target/debug/panic` thread 'main' panicked at 'crash and burn', src/main.rs:2:5 note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace fn main
() { let v = vec!
[
1
,
2
,
3
]; v[
99
];
}
Листинг 9-2: Обратная трассировка, сгенерированная вызовом
panic!
, когда установлена переменная
окружения
RUST_BACKTRACE
Тут много вывода! Вывод, который вы увидите, может отличаться от представленного, в зависимости от вашей операционной системы и версии Rust. Для того, чтобы получить обратную трассировку с этой информацией, должны быть включены символы отладки,
debug symbols. Символы отладки включены по умолчанию при использовании cargo build или cargo run без флага
--release
, как у нас в примере.
В выводе обратной трассировки листинга 9-2, строка #6 указывает на строку в нашем проекте, которая вызывала проблему: строка 4 из файла src/main.rs. Если мы не хотим,
чтобы наша программа запаниковала, мы должны начать исследование с места, на которое указывает первая строка с упоминанием нашего файла. В листинге 9-1, где мы для демонстрации обратной трассировки сознательно написали код, который паникует,
способ исправления паники состоит в том, чтобы не запрашивать элемент за пределами диапазона значений индексов вектора. Когда ваш код запаникует в будущем, вам нужно будет выяснить, какое выполняющееся кодом действие, с какими значениями вызывает панику и что этот код должен делать вместо этого.
$
RUST_BACKTRACE=1 cargo run thread 'main' panicked at 'index out of bounds: the len is 3 but the index is 99', src/main.rs:4:5 stack backtrace:
0: rust_begin_unwind at
/rustc/7eac88abb2e57e752f3302f02be5f3ce3d7adfb4/library/std/src/panicking.rs:483 1: core::panicking::panic_fmt at
/rustc/7eac88abb2e57e752f3302f02be5f3ce3d7adfb4/library/core/src/panicking.rs:85 2: core::panicking::panic_bounds_check at
/rustc/7eac88abb2e57e752f3302f02be5f3ce3d7adfb4/library/core/src/panicking.rs:62 3:>::index at
/rustc/7eac88abb2e57e752f3302f02be5f3ce3d7adfb4/library/core/src/slice/index.rs:255 4: core::slice::index:: for [T]>::index at
/rustc/7eac88abb2e57e752f3302f02be5f3ce3d7adfb4/library/core/src/slice/index.rs:15 5: <:vec::vec> as core::ops::index::Index>::index at
/rustc/7eac88abb2e57e752f3302f02be5f3ce3d7adfb4/library/alloc/src/vec.rs:1982 6: panic::main at ./src/main.rs:4 7: core::ops::function::FnOnce::call_once at
/rustc/7eac88abb2e57e752f3302f02be5f3ce3d7adfb4/library/core/src/ops/function.rs:227
note: Some details are omitted, run with `RUST_BACKTRACE=full` for a verbose backtrace.
Мы вернёмся к обсуждению макроса panic!
, и того когда нам следует и не следует использовать panic!
для обработки ошибок в разделе "
panic!
или НЕ panic!
"
этой главы. Далее мы рассмотрим, как восстановить выполнение программы после исправляемых ошибок, использующих тип
Result
вставлены
Мы не можем использовать переменные field_name и field_value после того, как их значения были перемещены в HashMap вызовом метода insert
Если мы вставим в HashMap ссылки на значения, то они не будут перемещены в
HashMap. Значения, на которые указывают ссылки, должны быть действительными хотя бы до тех пор, пока хеш-карта действительна. Мы поговорим подробнее об этих вопросах в разделе "Валидация ссылок при помощи времён жизни"
главы 10.
Обновление данных в HashMap
Хотя количество ключей и значений может увеличиваться в HashMap, каждый ключ может иметь только одно значение, связанное с ним в один момент времени (обратное утверждение неверно: команды "Blue" и "Yellow" могут хранить в хеш-карте scores одинаковое количество очков, например 10).
Когда вы хотите изменить данные в хеш-карте, необходимо решить, как обрабатывать случай, когда ключ уже имеет назначенное значение. Можно заменить старое значение новым, полностью игнорируя старое. Можно сохранить старое значение и игнорировать новое, или добавлять новое значение, если только ключ ещё не имел значения. Или можно было бы объединить старое значение и новое значение. Давайте посмотрим, как сделать каждый из вариантов!
Перезапись старых значений
Если мы вставим ключ и значение в HashMap, а затем вставим тот же ключ с новым значением, то старое значение связанное с этим ключом, будет заменено на новое. Даже несмотря на то, что код в листинге 8-23 вызывает insert дважды, хеш-карта будет содержать только одну пару ключ/значение, потому что мы вставляем значения для одного и того же ключа - ключа команды "Blue".
use std::collections::HashMap; let field_name =
String
::from(
"Favorite color"
); let field_value =
String
::from(
"Blue"
); let mut map = HashMap::new(); map.insert(field_name, field_value);
// field_name and field_value are invalid at this point, try using them and
// see what compiler error you get!
Листинг 8-23: Замена значения, хранимого в конкретном ключе
Код напечатает
{"Blue": 25}
. Начальное значение
10
было перезаписано.
Вставка значения только в том случае, когда ключ не имеет значения
Обычно проверяют, существует ли конкретный ключ в хеш-карте со значением, а затем предпринимаются следующие действия: если ключ существует в хеш-карте,
существующее значение должно оставаться таким, какое оно есть. Если ключ не существует, то вставляют его и значение для него.
Хеш-карты имеют для этого специальный API, называемый entry
, который принимает ключ для проверки в качестве входного параметра. Возвращаемое значение метода entry
- это перечисление
Entry
, с двумя вариантами: первый представляет значение,
которое может существовать, а второй говорит о том, что значение отсутствует.
Допустим, мы хотим проверить, имеется ли ключ и связанное с ним значение для команды "Yellow". Если хеш-карта не имеет значения для такого ключа, то мы хотим вставить значение 50. То же самое мы хотим проделать и для команды "Blue".
Используем API entry в коде листинга 8-24.
Листинг 8-24: Использование метода
entry
для вставки значения только в том случае, когда ключ не имеет
значения
Метод or_insert определён в
Entry так, чтобы возвращать изменяемую ссылку на соответствующее значение ключа внутри варианта перечисления
Entry
, когда этот ключ существует, а если его нет, то вставлять параметр в качестве нового значения этого ключа и возвращать изменяемую ссылку на новое значение. Эта техника намного чище,
чем самостоятельное написание логики и, кроме того, она более безопасна и согласуется с правилами заимствования.
use std::collections::HashMap; let mut scores = HashMap::new(); scores.insert(
String
::from(
"Blue"
),
10
); scores.insert(
String
::from(
"Blue"
),
25
); println!
(
"{:?}"
, scores); use std::collections::HashMap; let mut scores = HashMap::new(); scores.insert(
String
::from(
"Blue"
),
10
); scores.entry(
String
::from(
"Yellow"
)).or_insert(
50
); scores.entry(
String
::from(
"Blue"
)).or_insert(
50
); println!
(
"{:?}"
, scores);
При выполнении кода листинга 8-24 будет напечатано
{"Yellow": 50, "Blue": 10}
Первый вызов метода entry вставит ключ для команды "Yellow" со значением 50, потому что для жёлтой команды ещё не имеется значения в HashMap. Второй вызов entry не изменит хеш-карту, потому что для ключа команды "Blue" уже имеется значение 10.
Создание нового значения на основе старого значения
Другим распространённым вариантом использования хеш-карт является поиск значения по ключу, а затем обновление этого значения на основе старого значения. Например, в листинге 8-25 показан код, который подсчитывает, сколько раз определённое слово встречается в некотором тексте. Мы используем HashMap со словами в качестве ключей и увеличиваем соответствующее слову значение, чтобы отслеживать, сколько раз мы встретили это слово. Если мы впервые встретили слово, то сначала вставляем значение
0.
Листинг 8-25: Подсчёт количества вхождений слов с использованием хеш-карты, которая хранит слова и
счётчики
Этот код напечатает
{"world": 2, "hello": 1, "wonderful": 1}
. Если вы увидите, что пары ключ/значение печатаются в другом порядке, то вспомните, что мы писали в секции "Доступ к данным в HashMap"
, что итерация по хеш-карте происходит в произвольном порядке.
Метод split_whitespace возвращает итератор по срезам строки, разделённых пробелам,
для строки text
. Метод or_insert возвращает изменяемую ссылку (
&mut V
) на значение ключа. Мы сохраняем изменяемую ссылку в переменной count
, для этого,
чтобы присвоить переменной значение, необходимо произвести разыменование с помощью звёздочки (*). Изменяемая ссылка удаляется сразу же после выхода из области видимости цикла for
, поэтому все эти изменения безопасны и согласуются с правилами заимствования.
Функция хеширования
use std::collections::HashMap; let text =
"hello world wonderful world"
; let mut map = HashMap::new(); for word in text.split_whitespace() { let count = map.entry(word).or_insert(
0
);
*count +=
1
;
} println!
(
"{:?}"
, map);
По умолчанию
HashMap использует функцию хеширования SipHash, которая может противостоять атакам класса отказ в обслуживании, Denial of Service (DoS) с использованием хэш-таблиц
^siphash
. Это не самый быстрый из возможных алгоритмов хеширования, в данном случае производительность идёт на компромисс с обеспечением лучшей безопасности. Если после профилирования вашего кода окажется, что хеш- функция, используемая по умолчанию, очень медленная, вы можете заменить её
используя другой hasher. Hasher - это тип, реализующий трейт
BuildHasher
. Подробнее о типажах мы поговорим в Главе 10. Вам совсем не обязательно реализовывать свою собственную функцию хеширования; crates.io имеет достаточное количество библиотек,
предоставляющих разные реализации hasher с множеством общих алгоритмов хеширования.
Итоги
Векторы, строки и хеш-карты предоставят большое количество функционала для программ, когда необходимо сохранять, получать доступ и модифицировать данные.
Теперь вы готовы решить следующие учебные задания:
Есть список целых чисел. Создайте функцию, используйте вектор и верните из списка: среднее значение; медиану (значение элемента из середины списка после его сортировки); моду списка (mode of list, то значение которое встречается в списке наибольшее количество раз; HashMap будет полезна в данном случае).
Преобразуйте строку в кодировку "поросячьей латыни" (Pig Latin), где первая согласная каждого слова перемещается в конец и к ней добавляется окончание "ay".
Например "first" в поросячьей латыни станет "irst-fay". Если слово начинается на гласную, то в конец слова добавляется суффикс "hay" ("apple" становится "apple- hay"). Помните о деталях работы с кодировкой UTF-8!
Используя хеш-карту и векторы, создайте текстовый интерфейс позволяющий пользователю добавлять имена сотрудников к названию отдела компании.
Например, "Add Sally to Engineering" или "Add Amir to Sales". Затем позвольте пользователю получить список всех людей из отдела или всех людей в компании отсортированным в алфавитном порядке по отделам.
Документация API стандартной библиотеки описывает методы у векторов, строк и
HashMap. Рекомендуем воспользоваться ей при решении упражнений.
Потихоньку мы переходим к более сложным программам, в которых операции могут потерпеть неудачу. Наступило идеальное время для обсуждения обработки ошибок.
Обработка ошибок
Возникновение ошибок в ходе выполнения программ - это суровая реальность в жизни программного обеспечения, поэтому Rust имеет ряд функций для обработки ситуаций в которых что-то идёт не так. Во многих случаях Rust требует, чтобы вы признали возможность ошибки и предприняли некоторые действия, прежде чем ваш код будет скомпилирован. Это требование делает вашу программу более надёжной, гарантируя,
что вы обнаружите ошибки и обработаете их надлежащим образом, прежде чем развернёте свой код в производственной среде!
В Rust ошибки группируются на две основные категории исправимые (recoverable) и
неисправимые (unrecoverable). В случае исправимой ошибки, такой как файл не найден,
мы, скорее всего, просто хотим сообщить о проблеме пользователю и повторить операцию. Неисправимые ошибки всегда являются симптомами дефектов в коде,
например, попытка доступа к ячейке за пределами границ массива, и поэтому мы хотим немедленно остановить программу.
Большинство языков не различают эти два вида ошибок и обрабатывают оба вида одинаково, используя такие механизмы, как исключения. В Rust нет исключений. Вместо этого он имеет тип
Result
для обрабатываемых (исправимых) ошибок и макрос panic!
, который останавливает выполнение, когда программа встречает необрабатываемую (неисправимую) ошибку. Сначала эта глава расскажет про вызов panic!
, а потом расскажет о возврате значений
Result
. Кроме того, мы рассмотрим, что нужно учитывать при принятии решения о том, следует ли попытаться исправить ошибку или остановить выполнение.
Неустранимые ошибки с макросом panic!
Иногда в коде происходят плохие вещи, и вы ничего не можете с этим поделать. В этих случаях у Rust есть макрос panic! На практике существует два способа вызвать панику:
путём выполнения действия, которое вызывает панику в нашем коде (например,
обращение к массиву за пределами его размера) или путём явного вызова макроса panic!
. В обоих случаях мы вызываем панику в нашей программе. По умолчанию паника выводит сообщение об ошибке, раскручивает и очищает стек вызовов, и завершают работу. С помощью переменной окружения вы также можете заставить Rust отображать стек вызовов при возникновении паники, чтобы было легче отследить источник паники.
Раскручивать стек или прерывать выполнение программы в ответ
на панику?
По умолчанию, когда происходит паника, программа начинает процесс раскрутки
стека, означающий в Rust проход обратно по стеку вызовов и очистку данных для каждой обнаруженной функции. Тем не менее, этот обратный проход по стеку и очистка генерируют много работы. Rust как альтернативу предоставляет вам возможность немедленного прерывания (aborting), которое завершает работу программы без очистки.
Память, которую использовала программа, должна быть очищена операционной системой. Если в вашем проекте нужно насколько это возможно сделать маленьким исполняемый файл, вы можете переключиться с варианта раскрутки стека на вариант прерывания при панике, добавьте panic = 'abort'
в раздел [profile]
вашего
Cargo.toml файла. Например, если вы хотите прервать панику в режиме релиза, добавьте это:
Давайте попробуем вызвать panic!
в простой программе:
Файл: src/main.rs
При запуске программы, вы увидите что-то вроде этого:
[profile.release]
panic =
'abort'
fn main
() { panic!
(
"crash and burn"
);
}
Выполнение макроса panic!
вызывает сообщение об ошибке, содержащееся в двух последних строках. Первая строка показывает сообщение паники и место в исходном коде, где возникла паника: src/main.rs: 2:5 указывает, что это вторая строка, пятый символ внутри нашего файла src/main.rs
В этом случае указанная строка является частью нашего кода, и если мы перейдём к этой строке, мы увидим вызов макроса panic!
. В других случаях вызов panic!
мог бы произойти в стороннем коде, который вызывает наш код, тогда имя файла и номер строки для сообщения об ошибке будет из чужого кода, где макрос panic!
выполнен, а не из строк нашего кода, которые в конечном итоге привели к выполнению panic!
. Мы можем использовать обратную трассировку вызовов функций которые вызвали panic!
чтобы выяснить, какая часть нашего кода вызывает проблему. Мы обсудим обратную трассировку более подробно далее.
Использование обратной трассировки panic!
Давайте посмотрим на другой пример, где, вызов panic!
происходит в сторонней библиотеке из-за ошибки в нашем коде (а не как в примере ранее, из-за вызова макроса нашим кодом напрямую). В листинге 9-1 приведён код, который пытается получить доступ по индексу в векторе за пределами допустимого диапазона значений индекса.
Файл: src/main.rs
Листинг 9-1: Попытка доступа к элементу за пределами вектора, которая вызовет
panic!
Здесь мы пытаемся получить доступ к 100-му элементу вектора (который находится по индексу 99, потому что индексирование начинается с нуля), но вектор имеет только 3
элемента. В этой ситуации, Rust будет вызывать панику. Использование
[]
должно возвращать элемент, но вы передаёте неверный индекс: не существует элемента,
который Rust мог бы вернуть.
В языке C, например, попытка прочесть за пределами конца структуры данных (в нашем случае векторе) приведёт к неопределённому поведению, undefined behavior, UB. Вы всё
$
cargo run
Compiling panic v0.1.0 (file:///projects/panic)
Finished dev [unoptimized + debuginfo] target(s) in 0.25s
Running `target/debug/panic` thread 'main' panicked at 'crash and burn', src/main.rs:2:5 note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace fn main
() { let v = vec!
[
1
,
2
,
3
]; v[
99
];
}
равно получите значение, которое находится в том месте памяти компьютера, которое соответствовало бы этому элементу в векторе, несмотря на то, что память по тому адресу совсем не принадлежит вектору (всё просто: C рассчитал бы место хранения элемента с индексом 99 и считал бы то, что там хранится, упс). Это называется чтением за пределом
буфера, buffer overread, и может привести к уязвимостям безопасности. Если злоумышленник может манипулировать индексом таким образом, то у него появляется возможность читать данные, которые он не должен иметь возможности читать.
Чтобы защитить вашу программу от такого рода уязвимостей при попытке прочитать элемент с индексом, которого не существует, Rust остановит выполнение и откажется продолжить работу программы. Давайте попробуем так сделать и посмотрим на поведение Rust:
Следующая строка говорит, что мы можем установить переменную среды
RUST_BACKTRACE
, чтобы получить обратную трассировку того, что именно стало причиной ошибки. Обратная трассировка создаёт список всех функций, которые были вызваны до какой-то определённой точки выполнения программы. Обратная трассировка в Rust работает так же, как и в других языках. По этому предлагаем вам читать данные обратной трассировки как и везде - читать сверху вниз, пока не увидите информацию о файлах написанных вами. Это место, где возникла проблема. Другие строки, которые выше над строками с упоминанием наших файлов, - это код, который вызывается нашим кодом; строки ниже являются кодом, который вызывает наш код. Эти строки могут включать основной код Rust, код стандартной библиотеки или используемые крейты. Давайте попробуем получить обратную трассировку с помощью установки переменной среды
RUST_BACKTRACE
в любое значение, кроме 0. Листинг 9-2
показывает вывод, подобный тому, что вы увидите.
$
cargo run
Compiling panic v0.1.0 (file:///projects/panic)
Finished dev [unoptimized + debuginfo] target(s) in 0.27s
Running `target/debug/panic` thread 'main' panicked at 'index out of bounds: the len is 3 but the index is 99', src/main.rs:4:5 note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
буфера, buffer overread, и может привести к уязвимостям безопасности. Если злоумышленник может манипулировать индексом таким образом, то у него появляется возможность читать данные, которые он не должен иметь возможности читать.
Чтобы защитить вашу программу от такого рода уязвимостей при попытке прочитать элемент с индексом, которого не существует, Rust остановит выполнение и откажется продолжить работу программы. Давайте попробуем так сделать и посмотрим на поведение Rust:
Следующая строка говорит, что мы можем установить переменную среды
RUST_BACKTRACE
, чтобы получить обратную трассировку того, что именно стало причиной ошибки. Обратная трассировка создаёт список всех функций, которые были вызваны до какой-то определённой точки выполнения программы. Обратная трассировка в Rust работает так же, как и в других языках. По этому предлагаем вам читать данные обратной трассировки как и везде - читать сверху вниз, пока не увидите информацию о файлах написанных вами. Это место, где возникла проблема. Другие строки, которые выше над строками с упоминанием наших файлов, - это код, который вызывается нашим кодом; строки ниже являются кодом, который вызывает наш код. Эти строки могут включать основной код Rust, код стандартной библиотеки или используемые крейты. Давайте попробуем получить обратную трассировку с помощью установки переменной среды
RUST_BACKTRACE
в любое значение, кроме 0. Листинг 9-2
показывает вывод, подобный тому, что вы увидите.
$
cargo run
Compiling panic v0.1.0 (file:///projects/panic)
Finished dev [unoptimized + debuginfo] target(s) in 0.27s
Running `target/debug/panic` thread 'main' panicked at 'index out of bounds: the len is 3 but the index is 99', src/main.rs:4:5 note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
Листинг 9-2: Обратная трассировка, сгенерированная вызовом
panic!
, когда установлена переменная
окружения
RUST_BACKTRACE
Тут много вывода! Вывод, который вы увидите, может отличаться от представленного, в зависимости от вашей операционной системы и версии Rust. Для того, чтобы получить обратную трассировку с этой информацией, должны быть включены символы отладки,
debug symbols. Символы отладки включены по умолчанию при использовании cargo build или cargo run без флага
--release
, как у нас в примере.
В выводе обратной трассировки листинга 9-2, строка #6 указывает на строку в нашем проекте, которая вызывала проблему: строка 4 из файла src/main.rs. Если мы не хотим,
чтобы наша программа запаниковала, мы должны начать исследование с места, на которое указывает первая строка с упоминанием нашего файла. В листинге 9-1, где мы для демонстрации обратной трассировки сознательно написали код, который паникует,
способ исправления паники состоит в том, чтобы не запрашивать элемент за пределами диапазона значений индексов вектора. Когда ваш код запаникует в будущем, вам нужно будет выяснить, какое выполняющееся кодом действие, с какими значениями вызывает панику и что этот код должен делать вместо этого.
$
RUST_BACKTRACE=1 cargo run thread 'main' panicked at 'index out of bounds: the len is 3 but the index is 99', src/main.rs:4:5 stack backtrace:
0: rust_begin_unwind at
/rustc/7eac88abb2e57e752f3302f02be5f3ce3d7adfb4/library/std/src/panicking.rs:483 1: core::panicking::panic_fmt at
/rustc/7eac88abb2e57e752f3302f02be5f3ce3d7adfb4/library/core/src/panicking.rs:85 2: core::panicking::panic_bounds_check at
/rustc/7eac88abb2e57e752f3302f02be5f3ce3d7adfb4/library/core/src/panicking.rs:62 3:
/rustc/7eac88abb2e57e752f3302f02be5f3ce3d7adfb4/library/core/src/slice/index.rs:255 4: core::slice::index::
/rustc/7eac88abb2e57e752f3302f02be5f3ce3d7adfb4/library/core/src/slice/index.rs:15 5: <:vec::vec> as core::ops::index::Index>::index at
/rustc/7eac88abb2e57e752f3302f02be5f3ce3d7adfb4/library/alloc/src/vec.rs:1982 6: panic::main at ./src/main.rs:4 7: core::ops::function::FnOnce::call_once at
/rustc/7eac88abb2e57e752f3302f02be5f3ce3d7adfb4/library/core/src/ops/function.rs:227
note: Some details are omitted, run with `RUST_BACKTRACE=full` for a verbose backtrace.
Мы вернёмся к обсуждению макроса panic!
, и того когда нам следует и не следует использовать panic!
для обработки ошибок в разделе "
panic!
или НЕ panic!
"
этой главы. Далее мы рассмотрим, как восстановить выполнение программы после исправляемых ошибок, использующих тип
Result
1 ... 15 16 17 18 19 20 21 22 ... 62