Обобщённые типы, трейты и времена жизни
Каждый язык программирования имеет в своём арсенале эффективные средства борьбы с дублированием кода. В Rust одним из таких инструментов являются обобщения — абстрактные заместители, на место которых возможно поставить какой-либо конкретный тип или другое свойство. Когда мы пишем код, мы можем выразить поведение обобщений или их связь с другими обобщениями, не зная, что будет использовано на их месте при компиляции и запуске кода.
Функции могут принимать параметры некоторого обобщённого, а не конкретного типа (вроде i32
или String
), аналогично тому, что функция принимает параметры с неизвестными заранее значениями, чтобы выполнять одинаковые действия над различными конкретными значениями. На самом деле, мы уже использовали обобщённые типы данных в Главе 6 (Option<T>
), в Главе 8 (Vec<T>
и HashMap<K, V>
) и в Главе 9 (Result<T, E>
). В этой главе вы узнаете, как определять собственные обобщённые типы данных, функции и методы.
Первым делом, мы рассмотрим как для уменьшения дублирования извлечь из кода некоторую общую функциональность. Далее, мы будем использовать тот же механизм для создания обобщённой функции из двух функций, которые отличаются только типами их параметров. Мы также объясним, как использовать обобщённые типы данных при определении структур и перечислений.
После этого мы изучим, как использовать трейты для определения поведения в обобщённом виде. Можно комбинировать трейты с обобщёнными типами, чтобы обобщённый тип мог принимать только такие типы, которые имеют определённое поведение, а не все подряд.
В конце мы обсудим времена жизни — разновидность обобщения, которая даёт компилятору информацию о том, как ссылки относятся друг к другу. Времена жизни позволяют нам указать дополнительную информацию о заимствованных значениях, которая позволит компилятору удостовериться в корректности используемых ссылок в тех ситуациях, когда компилятор не может сделать это автоматически.
Избавление от дублирования кода с помощью выделения общей функциональности
Обобщения позволяют нам заменять определённые типы заполнителями, представляющими множество типов, чтобы устранять дублирование кода. Прежде чем углубиться в синтаксис обобщений, давайте сначала рассмотрим, как устранить дублирование без использования обобщённых типов, а лишь извлекая функцию, которая заменяет определённые значения заполнителем, представляющим несколько значений. Затем мы применим ту же технику для извлечения обобщённой функции. Изучив, как распознавать повторяющийся код, который вы можете извлечь в функцию, вы начнёте распознавать повторяющийся код, в котором могут использоваться обобщения.
Начнём с короткой программы в Листинге 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}"); }
Несмотря на то, что код программы работает, дублирование кода утомительно и подвержено опечаткам. Кроме того, при внесении изменений мы должны не забыть обновить каждое место, где код дублируется.
Для устранения дублирования мы можем создать дополнительную абстракцию с помощью функции, которая сможет работать с любым списком целых чисел, переданным ей в качестве входного параметра, и находить для этого списка наибольшее число. Данное решение делает код более ясным и позволяет абстрактным образом реализовать алгоритм поиска наибольшего числа в списке.
В Листинге 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!("Наибольшее число: {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
. Как избавиться от такого дублирования? Давайте выяснять!