English | 简体中文 | 繁體中文 | Русский язык | Français | Español | Português | Deutsch | 日本語 | 한국어 | Italiano | بالعربية
Генерики — это необходимый механизм для языков программирования.
В языке программирования C++ генерики реализуются с помощью шаблонов, в то время как в языке программирования C отсутствует механизм генериков, что делает сложные проекты с типами данных сложными.
Механизм генериков является механизмом, используемым языками программирования для выражения абстрактных типов, обычно используется для классов с определенными функциями и неопределенными типами данных, таких как списки, таблицы и т.д.
Это метод выбора для целых чисел:
fn max(array: &[i32]) -> i32 { let mut max_index = 0; let mut i = 1; while i < array.len() { if array[i] > array[max_index] { max_index = i; } i += 1; } array[max_index] } fn main() { let a = [2, 4, 6, 3, 1]; println!("max = {}", max(&a)); }
Результат выполнения:
max = 6
Это простая программа для нахождения максимального значения, которая может использоваться для обработки данных типа i32, но не может быть использована для данных типа f64. Используя генериков, мы можем сделать эту функцию полезной для различных типов. Но на самом деле не все типы данных могут быть сравнимы между собой, поэтому следующая часть кода не предназначена для выполнения, а для описания синтаксиса генериков функции:
fn max<T>(array: &[T]) -> T { let mut max_index = 0; let mut i = 1; while i < array.len() { if array[i] > array[max_index] { max_index = i; } i += 1; } array[max_index] }
В предыдущих уроках мы изучали генные классы Option и Result.
Структуры и энумы в Rust могут реализовывать механизм генериков.
struct Point<T> { x: T, y: T }
Это структура координат точки, T представляет собой тип данных, описывающий координаты точки. Мы можем использовать его таким образом:
let p1 = Point { x: 1, y: 2}; let p2 = Point { x: 1.0, y: 2.0};
При использовании не нужно объявлять тип, здесь используется автоматическая типизация, но не допускается типовая несовместимость, как это:
let p = Point { x: 1, y: 2.0};
Когда x привязан к 1, T уже определен как i32, поэтому не допускается的出现 f64 типа. Если мы хотим, чтобы x и y представляли разные типы данных, мы можем использовать два генерика:
struct Point<T1, T2> { x: T1, y: T2 }
В энумах методы генериков, такие как Option и Result:
enum Option<T> { Some(T), None, } enum Result<T, E> { Ok(T), Err(E), }
Структуры и энумы могут определять методы, поэтому методы также должны реализовывать механизм генериков, иначе классы с генериками не могут быть эффективно обработаны методами.
struct Point<T> { x: T, y: T, } impl<T> Point<T> { fn x(&self) -> &T { &self.x } } fn main() { let p = Point { x: 1, y: 2 }; println!("p.x = {}", p.x()); }
Результат выполнения:
p.x = 1
Внимание, после ключевого слова impl必须有 <T>, так как T в нем является образцом. Но мы также можем добавить метод одному из генериков:
impl Point<f64> { fn x(&self) -> f64 { self.x } }
Блок impl本身的 генерик не阻碍ывает его внутренние методы от использования генериков:
impl<T, U> Point<T, U> { fn mixup<V, W>(self, other: Point<V, W>) -> Point<T, W> { Point { x: self.x, y: other.y, } } }
Метод mixup融合了一个Point<T, U>点的x坐标和Point<V, W>点的y坐标,生成一个新点,其类型为Point<T, W>。
Концепция traits (трейты) близка к интерфейсам (Interface) в Java, но они не совсем одинаковы. Traits и интерфейсы похожи в том, что они оба представляют собой спецификацию поведения, которая может использоваться для идентификации методов, которые имеют классы.
Свойства в Rust представляют собой trait:
trait Descriptive { fn describe(&self) -> String; }
Descriptive определяет, что у реализующего его класса必须有 метод describe(&self) -> String.
Мы используем его для реализации структуры:
struct Person { name: String, age: u8 } impl Descriptive for Person { fn describe(&self) -> String { format!("{} {}", self.name, self.age) } }
Формат:
impl <имя_свойства> for <имя_типа_реализации>;
В Rust один класс может реализовывать несколько свойств, каждый блок impl может реализовывать только одно.
Разница между свойствами и интерфейсами: интерфейс может определять методы, но не может определять методы, а свойства могут определять методы в качестве метода по умолчанию, так как это "по умолчанию", объект может переопределять метод или использовать метод по умолчанию, не переопределяя его:
trait Descriptive { fn describe(&self) -> String { String::from("[Object]") } } struct Person { name: String, age: u8 } impl Descriptive for Person { fn describe(&self) -> String { format!("{} {}", self.name, self.age) } } fn main() { let cali = Person { имя: String::from("Cali"), возраст: 24 }; println!("{}", cali.describe()); }
Результат выполнения:
Cali 24
Если мы удалим содержимое блока impl Descriptive for Person, результат выполнения будет следующим:
[Object]
Во многих случаях нам нужно передавать функцию в качестве параметра, например, обратную функцию, установку события кнопки и т.д. В Java функция должна передаваться в виде примера класса, реализующего интерфейс, в Rust это можно сделать с помощью передачи параметров свойств:
fn output(object: impl Descriptive) { println!("{}", object.describe()); }
Любой объект, реализующий функциональность Descriptive, может быть передан в качестве параметра этой функции. Эта функция не должна знать, есть ли у переданного объекта другие свойства или методы, ей достаточно знать, что у объекта есть методы, соответствующие спецификации Descriptive. Естественно, в этом функции также нельзя использовать другие свойства и методы.
Параметры свойств также могут быть реализованы с помощью этого эквивалентного синтаксиса:
fn output<T: Descriptive>(object: T) { println!("{}", object.describe()); }
Это стиль грамматики, аналогичный генерикам, который очень полезен, когда у нескольких параметров типы являются свойствами:
fn output_two<T: Descriptive>(arg1: T, arg2: T) { println!("{}", arg1.describe()); println!("{}", arg2.describe()); }
Если типы,涉及的 несколько свойств, можно использовать символ +, например:
fn notify(item: impl Summary + Display) fn notify<T: Summary + Display>(item: T)
Внимание:Если используется только для представления типов, это не означает, что его можно использовать в блоке impl.
Комплексные отношения реализации можно упростить с помощью ключевого слова where, например:
fn some_function<T: Display + Clone, U: Clone + Debug>(t: T, u: U)
Это можно упростить до:
fn some_function<T, U>(t: T, u: U) -> i32 where T: Display + Clone, U: Clone + Debug
После изучения этой грамматики, пример "нахождения максимального значения" из раздела о генериках можно реализовать на практике:
trait Comparable { fn compare(&self, object: &Self) -> i8; } fn max<T: Comparable>(array: &[T]) -> &T { let mut max_index = 0; let mut i = 1; while i < array.len() { if array[i].compare(&array[max_index]) > 0 { max_index = i; } i += 1; } &array[max_index] } impl Comparable for f64 { fn compare(&self, object: &f64) -> i8 { if &self > &object { 1 } else if &self == &object { 0 } else { -1 } } } fn main() { let arr = [1.0, 3.0, 5.0, 4.0, 2.0]; println!("Максимальное значение arr равно {}", max(&arr)); }
Результат выполнения:
максимальное значение arr равно 5
Совет: Поскольку необходимо声明 вторым параметром функции compare тип, который реализует эту характеристику, поэтому ключевое слово Self (обратите внимание на大小енную) означает текущий тип (не пример) сам по себе.
Формат возвращения характеристики:
fn person() -> impl Descriptive { Person { имя: String::from("Cali"), возраст: 24 } }
Но есть одно условие: при возврате характеристики только объекты, реализующие эту характеристику, могут быть возвращены, и все возможные типы возвращаемых значений в одной функции должны быть полностью одинаковыми. Например, структуры A и B оба реализуют характеристику Trait, и следующая функция является неверной:
fn some_function(bool bl) -> impl Descriptive { if bl { else { return A {}; } } return B {}; } }
Функциональность impl очень мощная, и мы можем использовать ее для реализации методов класса. Но для генериков класса иногда нам нужно отличать методы, которые уже реализованы для принадлежащего им генерика, чтобы решить, какие методы нужно реализовать дальше:
struct A<T> {} impl<T: B + C> A<T> { fn d(&self) {} }
Этот код объявляет, что тип A<T> может быть эффективно реализован только в том случае, если T уже реализует характеристики B и C.