Обобщенные типы, типажи и время жизни
Каждый язык программирования имеет инструменты для эффективной работы с дублированием концепций. В Rust одним из таких инструментов являются обобщенные типы (generics): абстрактные заместители конкретных типов или других свойств. Мы можем выразить поведение обобщенных типов или их связь с другими обобщенными типами, не зная, что будет на их месте при компиляции и запуске кода.
Функции могут принимать параметры некоторого обобщенного типа, вместо конкретного типа, такого как i32 или String, аналогично тому, как они принимают параметры с неизвестными значениями для выполнения одного и того же кода с несколькими конкретными значениями. Фактически, мы уже использовали обобщенные типы в Главе 6 с Option<T>, в Главе 8 с Vec<T> и HashMap<K, V>, а в Главе 9 с Result<T, E>. В этой главе вы узнаете, как определять свои собственные типы, функции и методы с использованием обобщенных типов!
Сначала мы рассмотрим, как выделить функцию для уменьшения дублирования кода. Затем мы используем тот же метод, чтобы создать обобщенную функцию из двух функций, которые отличаются только типами своих параметров. Мы также объясним, как использовать обобщенные типы в определениях структур и перечислений.
Затем вы узнаете, как использовать типажи для определения поведения в обобщенном виде. Вы можете комбинировать типажи с обобщенными типами, чтобы ограничить обобщенный тип только теми типами, которые имеют определенное поведение, а не просто любым типом.
Наконец, мы обсудим время жизни (lifetimes): разновидность обобщенных типов, которая предоставляет компилятору информацию о том, как ссылки соотносятся друг с другом. Время жизни позволяет нам предоставить компилятору достаточно информации о заимствованных значениях, чтобы он мог гарантировать, что ссылки будут действительны в большем количестве ситуаций, чем без нашего вмешательства.
Устранение дублирования путем выделения функции
Обобщенные типы позволяют нам заменить конкретные типы заполнителем, который представляет множество типов, чтобы устранить дублирование кода. Прежде чем погрузиться в синтаксис обобщенных типов, давайте сначала посмотрим, как устранить дублирование способом, не связанным с обобщенными типами, путем выделения функции, которая заменяет конкретные значения заполнителем, представляющим множество значений. Затем мы применим тот же метод для выделения обобщенной функции! Научившись распознавать повторяющийся код, который можно выделить в функцию, вы начнете распознавать повторяющийся код, который можно использовать с обобщенными типами.
Мы начнем с короткой программы в Листинге 10-1, которая находит наибольшее число в списке.
<Листинг number="10-1" file-name="src/main.rs" caption="Поиск наибольшего числа в списке чисел">
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}"); assert_eq!(*largest, 100); }
</Листинг>
Мы сохраняем список целых чисел в переменной number_list и помещаем ссылку на первое число списка в переменную с именем largest. Затем мы перебираем все числа в списке, и если текущее число больше числа, хранящегося в largest, мы заменяем ссылку в этой переменной. Однако, если текущее число меньше или равно наибольшему числу, увиденному до сих пор, переменная не изменяется, и код переходит к следующему числу в списке. После рассмотрения всех чисел в списке largest должна ссылаться на наибольшее число, которое в данном случае равно 100.
Теперь нам поручили найти наибольшее число в двух разных списках чисел. Чтобы сделать это, мы можем продублировать код из Листинга 10-1 и использовать ту же логику в двух разных местах программы, как показано в Листинге 10-2.
<Листинг number="10-2" file-name="src/main.rs" caption="Код для поиска наибольшего числа в двух списках чисел">
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. Затем мы вызываем функцию, чтобы найти наибольшее число в двух списках из Листинга 10-2. Мы также могли бы использовать функцию на любом другом списке значений i32, который может у нас появиться в будущем.
<Листинг number="10-3" file-name="src/main.rs" caption="Абстрагированный код для поиска наибольшего числа в двух списках">
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}"); assert_eq!(*result, 100); let number_list = vec![102, 34, 6000, 89, 54, 2, 43, 8]; let result = largest(&number_list); println!("The largest number is {result}"); assert_eq!(*result, 6000); }
</Листинг>
Функция largest имеет параметр с именем list, который представляет любой конкретный срез значений i32, который мы можем передать в функцию. В результате, когда мы вызываем функцию, код выполняется на конкретных значениях, которые мы передаем.
В итоге, вот шаги, которые мы предприняли, чтобы изменить код из Листинга 10-2 в Листинг 10-3:
- Определили дублирующийся код.
- Выделили дублирующийся код в тело функции и указали входные и возвращаемые значения этого кода в сигнатуре функции.
- Обновили два экземпляра дублированного кода, чтобы вместо них вызывалась функция.
Далее мы используем эти же шаги с обобщенными типами для сокращения дублирования кода. Точно так же, как тело функции может работать с абстрактным list вместо конкретных значений, обобщенные типы позволяют коду работать с абстрактными типами.
Например, предположим, что у нас есть две функции: одна, которая находит наибольший элемент в срезе значений i32, и другая, которая находит наибольший элемент в срезе значений char. Как мы можем устранить это дублирование? Давайте выясним!