Дженерики: типы данных, трейты и времена жизни
Кратко
Способ 1: Data Type
// объявление дженерика - struct struct Point<T> { x: T, y: T, } let point = Point { x: 5, y: 4 }; // тип должен быть одним и тем же struct PointDiffType<T, U> { x: T, y: U, } let point_diff_type = PointDiffType{ x: 5, y: 4.0 }; // разные типы // объявление дженерика - impl impl<T> Point<T> { fn x(&self) -> &T { &self.x } } // объявление дженерика - enum enum Option<T> { Some(T), None, } enum Result<T, E> { Ok(T), Err(E), } // одна из реализаций mix-up struct Point<T, U> { x: T, y: U, } impl<T, U> Point<T, U> { // благодаря гибкости дженериков здесь сделано хитрое перекрёстное смешение field, дающее третий новый дженерик-экземпляр // этот синтаксис можно использовать в middleware fn mixup<V, W>(self, other: Point<V, W>) -> Point<T, W> { Point { x: self.x, y: other.y, } } } fn main() { let p1 = Point { x: 5, y: 10.4 }; let p2 = Point { x: "Hello", y: 'c' }; let p3 = p1.mixup(p2); println!("p3.x = {}, p3.y = {}", p3.x, p3.y); }Использование дженериков не влияет на производительность во время выполнения
потому что на этапе компиляции процесс мономорфизации (Monomorphization) уже превращает дженерики в конкретные типы
// например, код с дженериком Some let integer = Some(5); let float = Some(5.0); // на этапе компиляции произойдёт мономорфизация до следующего кода enum Option_i32 { Some(i32), None, } enum Option_f64 { Some(f64), None, } fn main() { let integer = Option_i32::Some(5); let float = Option_f64::Some(5.0); }Способ 2: Traits & Trait Bounds
Trait означает «признак»; в базовой записи он позволяет навесить признак на struct, параметры функции и возвращаемое значение функции
Для struct признак уточняет, что у этого struct есть объявленные в признаке method, аналогично
interfaceв Javaimpl structA for traitAДля generic type признак указывает: соответствующий набор функций доступен, только если у конкретного типа есть эти признаки (их также называют trait bound)
impl<T: traitA + traitB + ...> structA<T> {...}Для параметров функции признак ограничивает: параметром может быть только аргумент с соответствующим признаком (полиморфизм)
pub fn notify(item: &impl Summary) {...}Для возвращаемого значения функции признак уточняет: возвращаемое значение точно обладает этим признаком
// Defining a Trait pub trait Summary { fn summarize(&self) -> String; } // Implementing a Trait on a Type pub struct NewsArticle { pub headline: String, pub location: String, pub author: String, pub content: String, } impl Summary for NewsArticle { fn summarize(&self) -> String { format!("{}, by {} ({})", self.headline, self.author, self.location) } } // Default Implementations pub trait Summary { fn summarize(&self) -> String { String::from("(Read more...)") } } // Default Implementations могут вызывать другие impl pub trait Summary { fn summarize_author(&self) -> String; fn summarize(&self) -> String { format!("(Read more from {}...)", self.summarize_author()) } } // Traits as Parameters pub fn notify(item: &impl Summary) { println!("Breaking news! {}", item.summarize()); } // можно указать trait bounds через generic type pub fn notify<T: Summary>(item1: &T, item2: &T) {} // несколько трейтов, случай 1 - через оператор + pub fn notify(item: &(impl Summary + Display)) { pub fn notify<T: Summary + Display>(item: &T) { // несколько трейтов, случай 2 - через where fn some_function<T, U>(t: &T, u: &U) -> i32 where T: Display + Clone, U: Clone + Debug // Returning Types that Implement Traits fn returns_summarizable() -> impl Summary { Tweet { username: String::from("horse_ebooks"), content: String::from( "of course, as you probably already know, people", ), reply: false, retweet: false, } } // благодаря trait bounds можно сделать так: соответствующий method есть только у generic type с нужным трейтом use std::fmt::Display; struct Pair<T> { x: T, y: T, } impl<T> Pair<T> { fn new(x: T, y: T) -> Self { Self { x, y } } } impl<T: Display + PartialOrd> Pair<T> { fn cmp_display(&self) { if self.x >= self.y { println!("The largest member is x = {}", self.x); } else { println!("The largest member is y = {}", self.y); } } }Способ 3: Lifetimes
&i32 // a reference &'a i32 // a reference with an explicit lifetime &'a mut i32 // a mutable reference with an explicit lifetime // обычно используется в определении function fn longest<'a>(x: &'a str, y: &'a str) -> &'a str { if x.len() > y.len() { x } else { y } } // можно использовать в struct struct ImportantExcerpt<'a> { part: &'a str, } // Lifetime Elision - применение 3 правил fn first_word(s: &str) -> &str {...} // время жизни static доступно на протяжении всего времени работы программы let s: &'static str = "I have a static lifetime.";Компилятор использует 3 правила, чтобы вычислить времена жизни ссылок при отсутствии явных аннотаций
Первое правило: каждый параметр, являющийся ссылкой, получает собственный параметр времени жизни.
Первое правило: каждый входной параметр-ссылка имеет собственный параметр времени жизни.
Второе правило: если входной параметр времени жизни ровно один, это время жизни присваивается всем выходным параметрам времени жизни
Второе правило: если входной параметр времени жизни один, он присваивается всем выходным параметрам времени жизни.
Третье правило: если входных параметров времени жизни несколько, но один из них —
&selfили&mut self, потому что это method, то время жизниselfприсваивается всем выходным параметрам времени жизни.Третье правило: если входных параметров времени жизни несколько и один из них —
&selfили&mut self(method), то параметр времени жизниselfприсваивается всем выходным параметрам времени жизни.Совместное использование трёх способов
use std::fmt::Display; fn longest_with_an_announcement<'a, T>( x: &'a str, y: &'a str, ann: T, ) -> &'a str where T: Display, { println!("Announcement! {}", ann); if x.len() > y.len() { x } else { y } }
Источник
…
Производительность кода с дженериками
Возможно, вас интересует, есть ли накладные расходы во время выполнения при использовании параметров обобщённого типа. Хорошая новость: Rust реализует дженерики так, что код с обобщёнными типами не работает медленнее, чем с конкретными.
Rust добивается этого, выполняя на этапе компиляции мономорфизацию кода, использующего дженерики. Мономорфизация — это процесс превращения обобщённого кода в конкретный путём подстановки конкретных типов, используемых при компиляции.
…
Компилятор использует три правила, чтобы определить, какие времена жизни имеют ссылки, когда явных аннотаций нет. Первое правило относится к входным временам жизни, второе и третье — к выходным. Если компилятор доходит до конца трёх правил, а для каких-то ссылок время жизни определить всё ещё не может, он останавливается с ошибкой. Эти правила применяются и к определениям fn, и к блокам impl.
Первое правило: каждый параметр-ссылка получает собственный параметр времени жизни. Иначе говоря, функция с одним параметром получает один параметр времени жизни: fn foo<'a>(x: &'a i32); функция с двумя параметрами получает два отдельных параметра времени жизни: fn foo<'a, 'b>(x: &'a i32, y: &'b i32) и так далее.
Второе правило: если входной параметр времени жизни ровно один, это время жизни присваивается всем выходным параметрам времени жизни: fn foo<'a>(x: &'a i32) -> &'a i32.
Третье правило: если входных параметров времени жизни несколько, но один из них — &self или &mut self, потому что это method, время жизни self присваивается всем выходным параметрам времени жизни. Это третье правило делает methods гораздо удобнее для чтения и записи: нужно меньше символов.