Обобщения, типажи и время жизни
В каждом языке программирования есть инструменты для эффективной работы с дублированием концепций. В Rust одним из таких инструментов являются обобщения (generics): абстрактные замены для конкретных типов или других свойств. Мы можем описать поведение обобщений или их взаимосвязь, не зная, какие конкретные типы будут подставлены при компиляции и выполнении кода.
Функции могут принимать параметры некоторого обобщённого типа вместо конкретного, такого как i32 или String, подобно тому как они принимают параметры с неизвестными значениями для выполнения одного и того же кода на разных конкретных данных. Фактически, мы уже использовали обобщения в главе 6 с Option<T>, в главе 8 с Vec<T> и HashMap<K, V> и в главе 9 с Result<T, E>. В этой главе вы узнаете, как определять собственные типы, функции и методы с обобщениями!
Сначала мы рассмотрим, как выделить функцию для уменьшения дублирования кода. Затем применим тот же приём, чтобы создать обобщённую функцию из двух функций, которые отличаются только типами своих параметров. Мы также объясним, как использовать обобщённые типы в определениях структур и перечислений.
Затем вы узнаете, как использовать типажи (traits) для определения поведения в обобщённом виде. Вы можете комбинировать типажи с обобщёнными типами, чтобы ограничить обобщённый тип только теми типами, которые обладают определённым поведением, а не любым типом.
Наконец, мы обсудим время жизни (lifetimes): разновидность обобщений, которые дают компилятору информацию о том, как ссылки соотносятся друг с другом. Время жизни позволяет дать компилятору достаточно сведений о заимствованных значениях, чтобы он мог гарантировать, что ссылки будут действительными в большем числе ситуаций, чем мог бы без нашей помощи.
Устранение дублирования путём выделения функции
Обобщения позволяют заменить конкретные типы заполнителем, представляющим множество типов, чтобы устранить дублирование кода. Прежде чем углубляться в синтаксис обобщений, давайте сначала посмотрим, как устранить дублирование без использования обобщённых типов, выделив функцию, которая заменяет конкретные значения заполнителем, представляющим множество значений. Затем мы применим тот же приём для создания обобщённой функции! Поняв, как распознать дублирующийся код для выделения в функцию, вы начнёте распознавать дублирующийся код, который может использовать обобщения.
Начнём с короткой программы в Листинге 10-1, которая находит наибольшее число в списке.
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, мы заменяем ссылку в этой переменной. Однако, если текущее число меньше или равно наибольшему числу,seen so far, переменная не изменяется, и код переходит к следующему числу в списке. После рассмотрения всех чисел в списке largest должна ссылаться на наибольшее число, которое в данном случае равно 100.
Теперь нам поручено найти наибольшее число в двух разных списках чисел. Для этого мы можем продублировать код из Листинга 10-1 и использовать ту же логику в двух разных местах программы, как показано в Листинге 10-2.
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, который у нас может появиться в будущем.
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. Как нам устранить это дублирование? Давайте выясним!