Обобщённое программирование

Иногда, при написании функции или типа данных, мы можем захотеть, чтобы они работали для нескольких типов аргументов. К счастью, у Rust есть возможность, которая даёт нам лучший способ реализовать это: обобщённое программирование. Обобщённое программирование называется «параметрическим полиморфизмом» в теории типов. Это означает, что типы или функции имеют несколько форм (poly — кратно, morph — форма) по данному параметру («параметрический»).

В любом случае, хватит о теории типов; давайте рассмотрим какой-нибудь обобщённый код. Стандартная библиотека Rust предоставляет тип Option<T>, который является обобщённым типом:

fn main() { enum Option<T> { Some(T), None, } }
enum Option<T> {
    Some(T),
    None,
}

Часть <T>, которую вы раньше уже видели несколько раз, указывает, что это обобщённый тип данных. Внутри перечисления, везде, где мы видим T, мы подставляем вместо этого абстрактного типа тот, который используется в обобщении. Вот пример использования Option<T> с некоторыми дополнительными аннотациями типов:

fn main() { let x: Option<i32> = Some(5); }
let x: Option<i32> = Some(5);

В определении типа мы используем Option<i32>. Обратите внимание, что это очень похоже на Option<T>. С той лишь разницей, что, в данном конкретном Option, T имеет значение i32. В правой стороне выражения мы используем Some(T), где T равно 5. Так как 5 является представителем типа i32, то типы по обе стороны совпадают, поэтому компилятор счастлив. Если же они не совпадают, то мы получим ошибку:

fn main() { let x: Option<f64> = Some(5); // error: mismatched types: expected `core::option::Option<f64>`, // found `core::option::Option<_>` (expected f64 but found integral variable) }
let x: Option<f64> = Some(5);
// error: mismatched types: expected `core::option::Option<f64>`,
// found `core::option::Option<_>` (expected f64 but found integral variable)

Но это не значит, что мы не можем сделать Option<T>, который содержит f64! Просто типы должны совпадать:

fn main() { let x: Option<i32> = Some(5); let y: Option<f64> = Some(5.0f64); }
let x: Option<i32> = Some(5);
let y: Option<f64> = Some(5.0f64);

Это просто прекрасно. Одно определение — многостороннее использование.

Обобщать можно более, чем по одному параметру. Рассмотрим другой обобщённый тип из стандартной библиотеки Rust — Result<T, E>:

fn main() { enum Result<T, E> { Ok(T), Err(E), } }
enum Result<T, E> {
    Ok(T),
    Err(E),
}

Этот тип является обобщённым сразу для двух типов: T и E. Кстати, заглавные буквы могут быть любыми. Мы могли бы определить Result<T, E> как:

fn main() { enum Result<A, Z> { Ok(A), Err(Z), } }
enum Result<A, Z> {
    Ok(A),
    Err(Z),
}

если бы захотели. Соглашение гласит, что первый обобщённый параметр для 'типа' должен быть T, и что для 'ошибки' используется E. Но Rust не проверяет этого.

Тип Result<T, E> предназначен для того, чтобы возвращать результат вычисления, и имеет возможность вернуть ошибку, если произойдёт какой-либо сбой.

Обобщённые функции

Мы можем задавать функции, которые принимают обобщённые типы, с помощью аналогичного синтаксиса:

fn main() { fn takes_anything<T>(x: T) { // делаем что-то с x } }
fn takes_anything<T>(x: T) {
    // делаем что-то с x
}

Синтаксис состоит из двух частей: <T> говорит о том, что «эта функция является обобщённой по одному типу, T», а x: T говорит о том, что «х имеет тип T».

Несколько аргументов могут иметь один и тот же обобщённый тип:

fn main() { fn takes_two_of_the_same_things<T>(x: T, y: T) { // ... } }
fn takes_two_of_the_same_things<T>(x: T, y: T) {
    // ...
}

Мы можем написать версию, которая принимает несколько типов:

fn main() { fn takes_two_things<T, U>(x: T, y: U) { // ... } }
fn takes_two_things<T, U>(x: T, y: U) {
    // ...
}

Обобщённые функции наиболее полезны в связке с «ограничениями по типажам», о которых мы расскажем в главе Типажи.

Обобщённые структуры

Вы также можете задать обобщённый тип для struct:

fn main() { struct Point<T> { x: T, y: T, } let int_origin = Point { x: 0, y: 0 }; let float_origin = Point { x: 0.0, y: 0.0 }; }
struct Point<T> {
    x: T,
    y: T,
}

let int_origin = Point { x: 0, y: 0 };
let float_origin = Point { x: 0.0, y: 0.0 };

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