ВУЗ: Не указан
Категория: Не указан
Дисциплина: Не указана
Добавлен: 11.01.2024
Просмотров: 1155
Скачиваний: 5
ВНИМАНИЕ! Если данный файл нарушает Ваши авторские права, то обязательно сообщите нам.
СОДЕРЖАНИЕ
что он хочет делать в этом случае. Однако в тех случаях, когда продолжение выполнения программы может быть небезопасным или вредным, лучшим выбором будет вызов panic!
и оповещение пользователя, использующего вашу библиотеку, об ошибке в его коде, чтобы он мог исправить её во время разработки. Аналогично panic! подходит, если вы вызываете внешний, неподконтрольный вам код, и он возвращает недопустимое состояние, которое вы не можете исправить.
Однако, когда ожидается сбой, лучше вернуть
Result
, чем выполнить вызов panic!
. В
качестве примера можно привести синтаксический анализатор, которому передали неправильно сформированные данные, или HTTP-запрос, возвращающий статус указывающий на то, что вы достигли ограничения на частоту запросов. В этих случаях возврат
Result означает, что ошибка является ожидаемой и вызывающий код должен решить, как её обрабатывать.
Когда ваш код выполняет операцию, которая может подвергнуть пользователя риску,
если она вызывается с использованием недопустимых значений, ваш код должен сначала проверить допустимость значений и паниковать, если значения недопустимы.
Так рекомендуется делать в основном из соображений безопасности: попытка оперировать некорректными данными может привести к уязвимостям. Это основная причина, по которой стандартная библиотека будет вызывать panic!, если попытаться получить доступ к памяти вне границ массива: доступ к памяти, не относящейся к текущей структуре данных, является известной проблемой безопасности. Функции часто имеют контракты: их поведение гарантируется, только если входные данные отвечают определённым требованиям. Паника при нарушении контракта имеет смысл, потому что это всегда указывает на дефект со стороны вызывающего кода, и это не ошибка, которую вы хотели бы, чтобы вызывающий код явно обрабатывал. На самом деле, нет разумного способа для восстановления вызывающего кода; программисты, вызывающие ваш код,
должны исправить свой. Контракты для функции, особенно когда нарушение вызывает панику, следует описать в документации по API функции.
Тем не менее, наличие множества проверок ошибок во всех ваших функциях было бы многословным и раздражительным. К счастью, можно использовать систему типов Rust
(следовательно и проверку типов компилятором), чтобы она сделала множество проверок вместо вас. Если ваша функция имеет определённый тип в качестве параметра,
вы можете продолжить работу с логикой кода зная, что компилятор уже обеспечил правильное значение. Например, если используется обычный тип, а не тип
Option
, то ваша программа ожидает наличие чего-то вместо ничего. Ваш код не должен будет обрабатывать оба варианта
Some и
None
: он будет иметь только один вариант для определённого значения. Код, пытающийся ничего не передавать в функцию, не будет даже компилироваться, поэтому ваша функция не должна проверять такой случай во время выполнения. Другой пример - это использование целого типа без знака, такого как u32
, который гарантирует, что параметр никогда не будет отрицательным.
и оповещение пользователя, использующего вашу библиотеку, об ошибке в его коде, чтобы он мог исправить её во время разработки. Аналогично panic! подходит, если вы вызываете внешний, неподконтрольный вам код, и он возвращает недопустимое состояние, которое вы не можете исправить.
Однако, когда ожидается сбой, лучше вернуть
Result
, чем выполнить вызов panic!
. В
качестве примера можно привести синтаксический анализатор, которому передали неправильно сформированные данные, или HTTP-запрос, возвращающий статус указывающий на то, что вы достигли ограничения на частоту запросов. В этих случаях возврат
Result означает, что ошибка является ожидаемой и вызывающий код должен решить, как её обрабатывать.
Когда ваш код выполняет операцию, которая может подвергнуть пользователя риску,
если она вызывается с использованием недопустимых значений, ваш код должен сначала проверить допустимость значений и паниковать, если значения недопустимы.
Так рекомендуется делать в основном из соображений безопасности: попытка оперировать некорректными данными может привести к уязвимостям. Это основная причина, по которой стандартная библиотека будет вызывать panic!, если попытаться получить доступ к памяти вне границ массива: доступ к памяти, не относящейся к текущей структуре данных, является известной проблемой безопасности. Функции часто имеют контракты: их поведение гарантируется, только если входные данные отвечают определённым требованиям. Паника при нарушении контракта имеет смысл, потому что это всегда указывает на дефект со стороны вызывающего кода, и это не ошибка, которую вы хотели бы, чтобы вызывающий код явно обрабатывал. На самом деле, нет разумного способа для восстановления вызывающего кода; программисты, вызывающие ваш код,
должны исправить свой. Контракты для функции, особенно когда нарушение вызывает панику, следует описать в документации по API функции.
Тем не менее, наличие множества проверок ошибок во всех ваших функциях было бы многословным и раздражительным. К счастью, можно использовать систему типов Rust
(следовательно и проверку типов компилятором), чтобы она сделала множество проверок вместо вас. Если ваша функция имеет определённый тип в качестве параметра,
вы можете продолжить работу с логикой кода зная, что компилятор уже обеспечил правильное значение. Например, если используется обычный тип, а не тип
Option
, то ваша программа ожидает наличие чего-то вместо ничего. Ваш код не должен будет обрабатывать оба варианта
Some и
None
: он будет иметь только один вариант для определённого значения. Код, пытающийся ничего не передавать в функцию, не будет даже компилироваться, поэтому ваша функция не должна проверять такой случай во время выполнения. Другой пример - это использование целого типа без знака, такого как u32
, который гарантирует, что параметр никогда не будет отрицательным.
1 ... 17 18 19 20 21 22 23 24 ... 62
Создание пользовательских типов для проверки
Давайте разовьём идею использования системы типов Rust чтобы убедиться, что у нас есть корректное значение, и рассмотрим создание пользовательского типа для валидации. Вспомним игру угадывания числа из Главы 2, в которой наш код просил пользователя угадать число между 1 и 100. Мы никогда не проверяли, что предположение пользователя лежит между этими числами, перед сравнением предположения с загаданным нами числом; мы только проверяли, что оно положительно. В этом случае последствия были не очень страшными: наши сообщения
«Слишком много» или «Слишком мало», выводимые в консоль, все равно были правильными. Но было бы лучше подталкивать пользователя к правильным догадкам и иметь различное поведение для случаев, когда пользователь предлагает число за пределами диапазона, и когда пользователь вводит, например, буквы вместо цифр.
Один из способов добиться этого - пытаться разобрать введённое значение как i32
, а не как u32
, чтобы разрешить потенциально отрицательные числа, а затем добавить проверку для нахождение числа в диапазоне, например, так:
Выражение if проверяет, находится ли наше значение вне диапазона, сообщает пользователю о проблеме и вызывает continue
, чтобы начать следующую итерацию цикла и попросить ввести другое число. После выражения if мы можем продолжить сравнение значения guess с загаданным числом, зная, что guess лежит в диапазоне от
1 до 100.
Однако это не идеальное решение: если бы было чрезвычайно важно, чтобы программа работала только со значениями от 1 до 100, существовало бы много функций, требующих этого, то такая проверка в каждой функции была бы утомительной (и могла бы отрицательно повлиять на производительность).
Вместо этого можно создать новый тип и поместить проверки в функцию создания экземпляра этого типа, не повторяя их везде. Таким образом, функции могут использовать новый тип в своих сигнатурах и быть уверены в значениях, которые им передают. Листинг 9-13 показывает один из способов, как определить тип
Guess
, чтобы loop
{
// --snip-- let guess: i32
= match guess.trim().parse() {
Ok
(num) => num,
Err
(_) => continue
,
}; if guess <
1
|| guess >
100
{ println!
(
"The secret number will be between 1 and 100."
); continue
;
} match guess.cmp(&secret_number) {
// --snip--
}
экземпляр
Guess создавался только при условии, что функция new получает значение от
1 до 100.
Листинг 9-13. Тип
Guess
, который будет создавать экземпляры только для значений от 1 до 100
Сначала мы определяем структуру с именем
Guess
, которая имеет поле с именем value типа i32
, в котором будет храниться число.
Затем мы реализуем ассоциированную функцию new
, создающую экземпляры значений типа
Guess
. Функция new имеет один параметр value типа i32
, и возвращает
Guess
Код в теле функции new проверяет, что значение value находится между 1 и 100. Если value не проходит эту проверку, мы вызываем panic!
, которая оповестит программиста, написавшего вызывающий код, что в его коде есть ошибка, которую необходимо исправить, поскольку попытка создания
Guess со значением value вне заданного диапазона нарушает контракт, на который полагается
Guess::new
. Условия, в которых
Guess::new паникует, должны быть описаны в документации к API; мы рассмотрим соглашения о документации, указывающие на возможность появления panic!
в документации API, которую вы создадите в Главе 14. Если value проходит проверку, мы создаём новый экземпляр
Guess
, у которого значение поля value равно значению параметра value
, и возвращаем
Guess
Затем мы реализуем метод с названием value
, который заимствует self
, не имеет других параметров, и возвращает значение типа i32
. Этот метод иногда называют
извлекатель (getter), потому что его цель состоит в том, чтобы извлечь данные из полей структуры и вернуть их. Этот публичный метод является необходимым, поскольку поле value структуры
Guess является приватным. Важно, чтобы поле value было приватным,
чтобы код, использующий структуру
Guess
, не мог устанавливать value напрямую: код снаружи модуля должен использовать функцию
Guess::new для создания экземпляра
Guess
, таким образом гарантируя, что у
Guess нет возможности получить value
, не проверенное условиями в функции
Guess::new pub struct
Guess
{ value: i32
,
} impl
Guess { pub fn new
(value: i32
) -> Guess { if value <
1
|| value >
100
{ panic!
(
"Guess value must be between 1 and 100, got {}."
, value);
}
Guess { value }
} pub fn value
(&
self
) -> i32
{ self
.value
}
}
Guess создавался только при условии, что функция new получает значение от
1 до 100.
Листинг 9-13. Тип
Guess
, который будет создавать экземпляры только для значений от 1 до 100
Сначала мы определяем структуру с именем
Guess
, которая имеет поле с именем value типа i32
, в котором будет храниться число.
Затем мы реализуем ассоциированную функцию new
, создающую экземпляры значений типа
Guess
. Функция new имеет один параметр value типа i32
, и возвращает
Guess
Код в теле функции new проверяет, что значение value находится между 1 и 100. Если value не проходит эту проверку, мы вызываем panic!
, которая оповестит программиста, написавшего вызывающий код, что в его коде есть ошибка, которую необходимо исправить, поскольку попытка создания
Guess со значением value вне заданного диапазона нарушает контракт, на который полагается
Guess::new
. Условия, в которых
Guess::new паникует, должны быть описаны в документации к API; мы рассмотрим соглашения о документации, указывающие на возможность появления panic!
в документации API, которую вы создадите в Главе 14. Если value проходит проверку, мы создаём новый экземпляр
Guess
, у которого значение поля value равно значению параметра value
, и возвращаем
Guess
Затем мы реализуем метод с названием value
, который заимствует self
, не имеет других параметров, и возвращает значение типа i32
. Этот метод иногда называют
извлекатель (getter), потому что его цель состоит в том, чтобы извлечь данные из полей структуры и вернуть их. Этот публичный метод является необходимым, поскольку поле value структуры
Guess является приватным. Важно, чтобы поле value было приватным,
чтобы код, использующий структуру
Guess
, не мог устанавливать value напрямую: код снаружи модуля должен использовать функцию
Guess::new для создания экземпляра
Guess
, таким образом гарантируя, что у
Guess нет возможности получить value
, не проверенное условиями в функции
Guess::new pub struct
Guess
{ value: i32
,
} impl
Guess { pub fn new
(value: i32
) -> Guess { if value <
1
|| value >
100
{ panic!
(
"Guess value must be between 1 and 100, got {}."
, value);
}
Guess { value }
} pub fn value
(&
self
) -> i32
{ self
.value
}
}
Функция, которая принимает или возвращает только числа от 1 до 100, может объявить в своей сигнатуре, что она принимает или возвращает
Guess
, вместо i32
, таким образом не будет необходимости делать дополнительные проверки в теле такой функции.
Итоги
Функции обработки ошибок в Rust призваны помочь написанию более надёжного кода.
Макрос panic!
сигнализирует, что ваша программа находится в состоянии, которое она не может обработать, и позволяет сказать процессу чтобы он прекратил своё
выполнение, вместо попытки продолжить выполнение с некорректными или неверными значениями. Перечисление
Result использует систему типов Rust, чтобы сообщить, что операции могут завершиться неудачей, и ваш код мог восстановиться. Можно использовать
Result
, чтобы сообщить вызывающему коду, что он должен обрабатывать потенциальный успех или потенциальную неудачу. Использование panic!
и
Result правильным образом сделает ваш код более надёжным перед лицом неизбежных проблем.
Теперь, когда вы увидели полезные способы использования обобщённых типов
Option и
Result в стандартной библиотеке, мы поговорим о том, как работают обобщённые типы и как вы можете использовать их в своём коде.
Обобщённые типы, типажи и время
жизни
Каждый язык программирования имеет в своём арсенале эффективные средства борьбы с дублированием кода. В Rust одним из таким инструментом являются обобщённые типы данных - generics. Это абстрактные подставные типы на место которых возможно поставить какой-либо конкретный тип или другое свойство. Когда мы пишем код, мы можем выразить поведение обобщённых типов или их связь с другими обобщёнными типами, не зная какой тип будет использован на их месте при компиляции и запуске кода.
Функции могут принимать параметры некоторого "обобщённого" типа вместо привычных "конкретных" типов, вроде i32
или
String
. Аналогично, функция принимает параметры с неизвестными заранее значениями, чтобы выполнять одинаковые действия над несколькими конкретными значениями. На самом деле мы уже использовали обобщённые типы данных в Главе 6 (
Option
), в Главе 8 (
Vec
и
HashMap
) и в Главе 9 (
Result
). В этой главе вы узнаете, как определить собственные типы данных, функции и методы, используя возможности обобщённых типов.
Прежде всего, мы рассмотрим как для уменьшения дублирования извлечь из кода некоторую общую функциональность. Далее, мы будем использовать тот же механизм для создания обобщённой функции из двух функций, которые отличаются только типом их параметров. Мы также объясним, как использовать обобщённые типы данных при определении структур и перечислений.
После этого мы изучим как использовать типажи (traits) для определения поведения в обобщённом виде. Можно комбинировать типажи с обобщёнными типами, чтобы обобщённый тип мог принимать только такие типы, которые имеют определённое поведение, а не все подряд.
В конце мы обсудим времена жизни (lifetimes), вариации обобщённых типов, которые дают компилятору информацию о том, как сроки жизни ссылок относятся друг к другу.
Времена жизни позволяют нам указать дополнительную информацию об "одолженных"
(borrowed) значениях, которая позволит компилятору удостовериться в корректности используемых ссылок в тех ситуациях, когда компилятор не может сделать это автоматически.
Удаление дублирования кода с помощью выделения
общей функциональности
В обобщениях мы можем заменить конкретный тип на "заполнитель" (placeholder),
обозначающую несколько типов, что позволяет удалить дублирующийся код. Прежде чем углубляться в синтаксис обобщённых типов, давайте сначала посмотрим, как удалить дублирование, не задействует универсальные типы. Извлечём функцию, которая будет заменяет определённые значения заполнителем, представляющим несколько значений. Затем мы применим ту же технику для извлечения универсальной функции!
Изучив, как распознать дублированный код, который можно извлечь в функцию, вы начнёте распознавать дублированный код, который может использовать обобщённые типы.
Начнём с короткой программы в листинге 10-1, которая находит наибольшее число в списке.
Файл: src/main.rs
Листинг 10-1: Поиск наибольшего числа в списке чисел
Сохраним список целых чисел в переменной number_list и поместим первое значение из списка в переменную largest
. Далее, переберём все элементы списка, и, если текущий элемент больше числа сохранённого в переменной largest
, заменим значение в этой переменной. Если текущий элемент меньше или равен "наибольшему",
найденному ранее, значение переменной оставим прежним и перейдём к следующему элементу списка. После перебора всех элементов списка переменная largest должна содержать наибольшее значение, которое в нашем случае будет равно 100.
Теперь перед нами стоит задача найти наибольшее число в двух разных списках. Для этого мы можем дублировать код из листинга 10-1 и использовать ту же логику в двух разных местах программы, как показано в листинге 10-2.
Файл: src/main.rs fn main
() { let number_list = vec!
[
34
,
50
,
25
,
100
,
65
]; let mut largest = &number_list[
0
]; for number in
&number_list { if number > largest { largest = number;
}
} println!
(
"The largest number is {}"
, largest);
}
Листинг 10-2: Код для поиска наибольшего числа в двух списках чисел
Несмотря на то, что код программы работает, дублирование кода утомительно и подвержено ошибкам. При внесении изменений мы должны не забыть обновить каждое место, где код дублируется.
Для устранения дублирования мы можем создать дополнительную абстракцию с помощью функции которая сможет работать с любым списком целых чисел переданным ей в качестве входного параметра и находить для этого списка наибольшее число.
Данное решение делает код более ясным и позволяет абстрактным образом реализовать алгоритм поиска наибольшего числа в списке.
В листинге 10-3 мы извлекаем код, который находит наибольшее число, в функцию с именем largest
. Затем мы вызываем функцию, чтобы найти наибольшее число в двух списках из листинга 10-2. Мы также можем использовать эту функцию для любого другого списка значений i32
, который может встретиться позже.
Файл: src/main.rs fn main
() { let number_list = vec!
[
34
,
50
,
25
,
100
,
65
]; let mut largest = &number_list[
0
]; for number in
&number_list { if number > largest { largest = number;
}
} println!
(
"The largest number is {}"
, largest); let number_list = vec!
[
102
,
34
,
6000
,
89
,
54
,
2
,
43
,
8
]; let mut largest = &number_list[
0
]; for number in
&number_list { if number > largest { largest = number;
}
} println!
(
"The largest number is {}"
, largest);
}
Листинг 10-3: Абстрактный код для поиска наибольшего числа в двух списках
Функция largest имеет параметр с именем list
, который представляет любой срез значений типа i32
, которые мы можем передать в неё. В результате вызова функции,
код выполнится с конкретными, переданными в неё значениями.
Итак, вот шаги выполненные для изменения кода из листинга 10-2 в листинг 10-3:
1. Определить дублирующийся код.
2. Извлечь дублирующийся код и поместить его в тело функции, определив входные и выходные значения этого кода в сигнатуре функции.
3. Обновить и заменить два участка дублирующегося кода вызовом одной функции.
Далее, чтобы уменьшить дублирование кода, мы воспользуемся теми же шагами для обобщённых типов. Обобщённые типы позволяют работать над абстрактными типами таким же образом, как тело функции может работать над абстрактным списком list вместо конкретных значений.
Например, у нас есть две функции: одна ищет наибольший элемент внутри среза значений типа i32
, а другая внутри среза значений типа char
. Как уменьшить такое дублирование? Давайте выяснять!
fn largest
(list: &[
i32
]) -> &
i32
{ let mut largest = &list[
0
]; for item in list { if item > largest { largest = item;
}
} largest
} fn main
() { let number_list = vec!
[
34
,
50
,
25
,
100
,
65
]; let result = largest(&number_list); println!
(
"The largest number is {}"
, result); let number_list = vec!
[
102
,
34
,
6000
,
89
,
54
,
2
,
43
,
8
]; let result = largest(&number_list); println!
(
"The largest number is {}"
, result);
}