Обобщённые типы, трейты и времена жизни
Каждый язык программирования имеет в своём арсенале эффективные средства борьбы с дублированием кода. В Rust одним из таких инструментов являются обобщения — абстрактные заместители, на место которых возможно поставить какой-либо конкретный тип или другое свойство. Когда мы пишем код, мы можем выразить поведение обобщений или их связь с другими обобщениями, не зная, что будет использовано на их месте при компиляции и запуске кода.
Functions can take parameters of some generic type, instead of a concrete type like i32 or String, in the same way they take parameters with unknown values to run the same code on multiple concrete values. In fact, we already used generics in Chapter 6 with Option<T>, in Chapter 8 with Vec<T> and HashMap<K, V>, and in Chapter 9 with Result<T, E>. In this chapter, you’ll explore how to define your own types, functions, and methods with generics!
First, we’ll review how to extract a function to reduce code duplication. We’ll then use the same technique to make a generic function from two functions that differ only in the types of their parameters. We’ll also explain how to use generic types in struct and enum definitions.
Then, you’ll learn how to use traits to define behavior in a generic way. You can combine traits with generic types to constrain a generic type to accept only those types that have a particular behavior, as opposed to just any type.
Finally, we’ll discuss lifetimes: a variety of generics that give the compiler information about how references relate to each other. Lifetimes allow us to give the compiler enough information about borrowed values so that it can ensure that references will be valid in more situations than it could without our help.
Избавление от дублирования кода с помощью выделения общей функциональности
Generics allow us to replace specific types with a placeholder that represents multiple types to remove code duplication. Before diving into generics syntax, let’s first look at how to remove duplication in a way that doesn’t involve generic types by extracting a function that replaces specific values with a placeholder that represents multiple values. Then, we’ll apply the same technique to extract a generic function! By looking at how to recognize duplicated code you can extract into a function, you’ll start to recognize duplicated code that can use generics.
Начнём с короткой программы в Листинге 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!("Наибольшее число: {largest}");
assert_eq!(*largest, 100);
}
Мы храним список целых чисел в переменной number_list и помещаем первое значение из списка в переменную largest. Далее, перебираем все элементы списка, и, если текущий элемент больше числа, сохранённого в переменной largest, заменяем им значение в этой переменной. Если текущий элемент меньше или равен наибольшему, найденному ранее, значение переменной оставлям прежним и переходим к следующему элементу списка. После перебора всех элементов списка переменная 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!("Наибольшее число: {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!("Наибольшее число: {largest}");
}
Although this code works, duplicating code is tedious and error-prone. We also have to remember to update the code in multiple places when we want to change it.
Для устранения дублирования мы можем создать дополнительную абстракцию с помощью функции, которая сможет работать с любым списком целых чисел, переданным ей в качестве входного параметра, и находить для этого списка наибольшее число. Данное решение делает код более ясным и позволяет абстрактным образом реализовать алгоритм поиска наибольшего числа в списке.
In Listing 10-3, we extract the code that finds the largest number into a function named largest. Then, we call the function to find the largest number in the two lists from Listing 10-2. We could also use the function on any other list of i32 values we might have in the future.
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!("Наибольшее число: {result}");
assert_eq!(*result, 100);
let number_list = vec![102, 34, 6000, 89, 54, 2, 43, 8];
let result = largest(&number_list);
println!("Наибольшее число: {result}");
assert_eq!(*result, 6000);
}
Функция largest имеет параметр с именем list, который представляет любой срез значений типа i32, которые мы можем передать в неё. В результате вызова функции, код исполнится с конкретными, переданными в неё значениями.
Обобщая, вот шаги, выполненные нами для изменения Листинга 10-2 до Листинга 10-3:
- Определить повторяющийся код.
- Извлечь повторяющийся код и поместить его в тело функции, определив входные и выходные значения этого кода в сигнатуре функции.
- Заменить два участка повторяющегося кода вызовом одной функции.
Далее мы применим эти же шаги, чтобы избавляться от дублирования кода с помощью обобщений. Обобщения позволяют работать над абстрактными типами таким же образом, каким тело функции позволяет работать над абстрактным списком list вместо конкретных значений.
Например, у нас есть две функции: одна ищет наибольший элемент внутри среза значений типа i32, а другая — внутри среза значений типа char. Как избавиться от такого дублирования? Давайте выяснять!