Язык программирования Rust
Скачать 7.02 Mb.
|
Создание пользовательских типов для проверки Давайте разовьём идею использования системы типов 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 } } Функция, которая принимает или возвращает только числа от 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); } Обобщённые типы данных Мы можем использовать обобщённые типы данных для функций или структур, которые затем можно использовать с различными конкретными типами данных. Давайте сначала посмотрим, как объявлять функции, структуры, перечисления и методы, используя обобщённые типы данных. Затем мы обсудим, как обобщённые типы данных влияют на производительность кода. В объявлении функций Когда мы объявляем функцию с обобщёнными типами, мы размещаем обобщённые типы в сигнатуре функции, где мы обычно указываем типы данных аргументов и возвращаемое значение. Используя обобщённые типы, мы делаем код более гибким, и предоставляем большую функциональность при вызове нашей функции, предотвращая дублирование кода. Рассмотрим пример с функцией largest . Листинг 10-4 показывает две функции, каждая из которых находит самое большое значение в срезе своего типа. Файл: src/main.rs Листинг 10-4: Две функции, отличающихся только именем и типом обрабатываемых данных Функция largest_i32 уже встречалась нам: мы извлекли её в листинге 10-3, когда боролись с дублированием кода, она находит наибольшее значение типа i32 в срезе. Функция largest_char находит самое большое значение типа char в срезе. Тело у этих функций одинаковое, поэтому давайте избавимся от дублируемого кода, добавив обобщённые типы данных. Для параметризации типов данных в новой объявляемой функции, нам нужно дать имя обобщённому типу, также как мы это делаем для аргументов функций. Можно использовать любой идентификатор для имени параметра типа. Но мы будем использовать T , потому что, по соглашению, имена параметров в Rust должны быть короткими (обычно длиной в один символ) и именование типов в Rust делается в нотации CamelCase. Сокращение слова "type" до одной буквы T является стандартным выбором большинства программистов использующих язык Rust. fn largest_i32 (list: &[ i32 ]) -> & i32 { let mut largest = &list[ 0 ]; for item in list { if item > largest { largest = item; } } largest } fn largest_char (list: &[ char ]) -> & char { 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_i32(&number_list); println! ( "The largest number is {}" , result); let char_list = vec! [ 'y' , 'm' , 'a' , 'q' ]; let result = largest_char(&char_list); println! ( "The largest char is {}" , result); } Когда мы используем параметр в теле функции, мы должны объявить имя параметра в сигнатуре, так компилятор будет знать, что означает имя. Аналогично, когда мы используем имя параметра в сигнатуре функции, мы должны объявить имя параметра раньше, чем мы его используем. Чтобы определить обобщённую функцию largest , поместим объявление имён параметров в треугольные скобки, <> , между именем функции и списком параметров, как здесь: Объявление читается так: функция largest является обобщённой по типу T . Эта функция имеет один параметр с именем list , который является срезом значений с типом данных T . Функция largest возвращает данные такого же типа T Листинг 10-5 показывает определение функции largest с использованием обобщённых типов данных в её сигнатуре. Листинг также показывает, как мы можем вызвать функцию со срезом данных типа i32 или char . Данный код пока не будет компилироваться, но мы исправим это к концу раздела. Файл: src/main.rs Листинг 10-5: определение функции largest с использованием обобщённых типов, но код пока не компилируется Если мы скомпилируем программу сейчас, мы получим следующую ошибку: fn largest 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 char_list = vec! [ 'y' , 'm' , 'a' , 'q' ]; let result = largest(&char_list); println! ( "The largest char is {}" , result); } В подсказке упоминается std::cmp::PartialOrd , который является типажом. Мы поговорим про типажи в следующей секции. Сейчас, ошибка в функции largest указывает, что функция не будет работать для всех возможных типов T . Так как мы хотим сравнивать значения типа T в теле функции, то можно использовать только те типы, данные которых можно упорядочить: можем упорядочить, значит можем и сравнить. Для возможности сравнения, стандартная библиотека имеет типаж std::cmp::PartialOrd , который вы можете реализовать для типов (смотрите Дополнение С для большей информации про данный типаж). Вы узнаете, как потребовать чтобы обобщённый тип реализовывал определённый типаж в секции "Типажи как параметры" , но сначала давайте рассмотрим другие варианты использования обобщённых типов. |