Перейти к содержанию

Объяснение дженериков

Posted on:2 февраля 2023 г. at 17:32

Example Dynamic OG Image link Изучение универсальных шаблонов в Rust: написание гибкого и многократно используемого кода с дженериками типов.

Универсальные шаблоны (дженерики) — это способ написания гибкого и многократно используемого кода, позволяющий указать типы дженериков, которые могут быть заполнены позже при использовании кода. Это похоже на то, как шаблоны работают в C++ или как параметры типа работают в Java и других языках.

С помощью дженериков можно написать код, который сможет работать с любым типом, а не быть специфичным для определенного типа. Это облегчает написание кода, который разработчик может использовать в различных ситуациях без необходимости переписывать код для каждого конкретного типа.

Например, напишите функцию, которая принимает два значения и возвращает их сумму. Без дженериков вам нужно было бы написать отдельную функцию для каждого типа, который вы хотите поддерживать, например:

fn add_i32(a: i32, b: i32) -> i32 {  
    a + b  
}  
  
fn add_f64(a: f64, b: f64) -> f64 {  
    a + b  
}

Дженерики в функциях

Чтобы использовать дженерики в Rust, необходимо сначала объявить параметр универсального типа в определении функции или структуры с помощью <T> синтаксиса.

Например, вот простая функция, которая принимает дженерик тип T и возвращает его:

fn identity<T>(x: T) -> T {  
    x  
}

Затем эту функцию можно использовать с любым типом, указав параметр типа при вызове функции. Например:

let a = identity(8); // a имеет тип i32  
let b = identity("Ржавый код"); // b имеет тип &str

Можно также указать несколько параметров дженериков, разделив их запятой:

fn pair<T, U>(a: T, b: U) -> (T, U) {  
    (a, b)  
}  
  
let x = pair(1, "Ржавый код"); // x имеет тип кортежа (i32, &str)

Дженерики в структурах

Помимо использования дженериков в определениях функций, их также можно использовать в определениях структур. Например, вот простая реализация связанного списка, использующая дженерики:

struct Node<T> {  
    value: T,  
    next: Option<Box<Node<T>>>,  
}

В этом примере структура ‘Node’ имеет общий параметр типа ‘T’, который представляет тип значения, содержащий узел. Это означает, что вы можете использовать структуру ‘Node’ с любым типом, например:

let node1 = Node { value: 5, next: None }; // node1 имеет тип Node<i32>  
let node2 = Node { value: "hello", next: Some(Box::new(node1)) }; // node2 имеет тип Node<&str>

Дженерики в trait

Можно также использовать дженерики в определениях trait, чтобы указать типы, для которых разработчик может реализовать характеристику (trait). Например, вот простой trait, которая определяет метод сравнения двух значений:

trait Comparable<T> {  
    fn cmp(&self, other: &T) -> Ordering;  
}

Этот trait может быть реализован для любого типа T, который можно сравнить с помощью метода cmp. Например, вы можете реализовать эту характеристику для типа ‘i32’ следующим образом:

impl Comparable<i32> for i32 {  
    fn cmp(&self, other: &i32) -> Ordering {  
        self.cmp(other)  
    }  
}

Затем вы можете использовать этот trait с типом ‘i32’ следующим образом:

let a = 10;  
let b = 100;  
  
if a.cmp(&b) == Ordering::Less {  
    println!("a меньше b");  
}

Ограничения

Универсальные шаблоны также могут иметь ограничения, которые определяют требования, которым должен соответствовать универсальный тип для использования с функцией или структурой. Например, может потребоваться написать функцию, работающую с любым типом, реализующим атрибут Add, который позволяет добавлять два значения одного типа. Вы можете указать это ограничение, используя ключевое слово where:

fn add<T>(a: T, b: T) -> T  
where T: Add<Output=T>  
{  
    a + b  
}

Эта функция принимает два значения типа T и возвращает значение типа T, и у нее есть ограничение, что T должен реализовывать признак Add. Это означает, что вы можете использовать эту функцию только с типами, реализующими Add, такими как целые числа или числа с плавающей запятой.

Помимо использования ключевого слова where, вы также можете использовать границы trait. Привязка trait — это способ указать, что универсальный тип должен реализовывать определенную характеристику (trait). Например:

fn add<T: Add<Output=T>>(a: T, b: T) -> T {  
    a + b  
}

Вот пример того, как вы можете использовать эту функцию:

let a = 1;  
let b = 2;  
  
let sum = add(a, b); // sum имеет тип i32  
  
println!("Сумма: {}", sum);