Раннее в одной из предыдущих статей рассматривалось применение трейта в качестве параметра функции:
trait Printer{ fn print(&self); } struct Person { name: String, age: u8} impl Printer for Person{ fn print(&self){ println!("Person {}; age: {}", self.name, self.age); } } fn display(printable: &impl Printer) { printable.print(); } fn main(){ let tom = Person {name: String::from("Tom"), age: 36}; display(&tom); }
В частности, обратим внимание на функцию display()
, которая в качестве параметра принимает трейт Printer:
fn display(printable: &impl Printer) { printable.print(); }
В действительности синтаксис impl Trait
представляет упрощенный синтаксис того, что называется
trait bound или ограничение трейта, которое устанавливает для параметров типа ограничения по применяемым типам.
И в реальности эта функция разворачивается в следующую:
fn display<T: Printer>(printable: &T) { printable.print(); }
Выражение <T: Printer>
говорит, что параметр T обязательно должен представлять тип, который реализует трейт Printer. То есть устанавливается
ограничение на применяемые типы. Но в целом нет разницы, какой способ определения функции использовать.
Кроме выше предложенных двух вариантов функции Rust также предоставляет альтернативный синтаксис с использованием оператора where:
fn название_функции<T>(obj: &T) where T: трейт{ }
После оператора where
указывается какой трейт представляет параметр.
Так функцию display()
можно было бы переписать следующим образом:
fn display<T>(printable: &T) where T: Printer { printable.print(); }
Выше в примере параметр функции представлял один трейт, однако мы можем определить параметр, который должен реализовать сразу несколько трейтов. Для установки ограничения типа на несколько трейтов применяется операция +:
fn название_функции(obj: &(impl трейт1 + трейт2 + .... + трейтN)) { }
Рассмотрим на примере:
trait Printer{ fn print(&self); } trait Sender{ fn send(&self); } struct Message { text: String} impl Printer for Message{ fn print(&self){ println!("Сообщение: {}", self.text); } } impl Sender for Message{ fn send(&self){ println!("Сообщение отправлено"); } } fn process(obj: &(impl Printer + Sender)) { obj.print(); obj.send(); } fn main(){ let mes = Message {text: String::from("Hello Rust")}; process(&mes); }
Консольный вывод:
Сообщение: Hello Rust Сообщение отправлено
Здесь функция process
принимает в качестве параметра объект, который реализует сразу два трейта - Printer и Sender:
fn process(obj: &(impl Printer + Sender)) { obj.print(); obj.send(); }
Также мы могли использовать более длинную форму данной функции:
fn process<T: Printer + Sender>(obj: &T) { obj.print(); obj.send(); }
Также можно использовать альтернативный синтаксис с оператором where:
fn process<T>(obj: &T) where T: Printer + Sender{ obj.print(); obj.send(); }
Пример функции, которая применяет несколько параметров типа разных трейтов. Вариант с полным синтаксисом:
fn process<T: Printer + Editor, S: Printer + Sender>(obj1: &T, obj2: &S){ }
В данном случае функция process()
типизированна двумя параметрами T
и S
, при
этом тип T
должен представлять одновременную реализацию трейтов Printer и Editor, а параметр S
-
реализацию трейтов Printer и Sender.
Вариант той же фукции с сокращенным синтаксисом:
fn process(obj1: &(impl Printer + Editor), obj2: &(impl Printer + Sender)){ }
Вариант с оператором where
:
fn process<T, S>(obj1: &T, obj2: &S) where T: Printer + Editor, S: Printer + Sender{ }
Подобно тому как применяется ограничение трейтов к обобщенным функциям, она также может применяться при определении обобщенных типов. Например:
struct Person<T: Sender + Printer>{ device: T} trait Printer{ fn print(&self, message: &str); } trait Sender{ fn send(&self, message: &str); } struct Smartphone {} impl Printer for Smartphone{ fn print(&self, message: &str){ println!("{}", message); } } impl Sender for Smartphone{ fn send(&self, message: &str){ println!("Сообщение {} отправлено", message); } } fn main(){ let iphone = Smartphone{}; let tom = Person{device: iphone}; tom.device.print("Hello Rust!"); tom.device.send("Hello Rust!"); }
Здесь поле device
структуры Person представляет тип, который реализуется сразу два трейта: Printer и Sender.
struct Person<T: Sender + Printer>{ device: T}
И в данном примере как раз таким типом является структура Smartphone
.
Причем в данном случае также можно применять альтернативный вариант с оператором where
:
struct Person<T> where T: Sender + Printer{ device: T}
Также можно использовать ограничения трейтов в реализациях методов:
struct Person<T>{ device: T} impl<T: Sender + Printer> Person<T>{ fn send_message(&self, message: &str){ self.device.print(message); self.device.send(message); } } trait Printer{ fn print(&self, message: &str); } trait Sender{ fn send(&self, message: &str); } struct Smartphone {} impl Printer for Smartphone{ fn print(&self, message: &str){ println!("{}", message); } } impl Sender for Smartphone{ fn send(&self, message: &str){ println!("Сообщение {} отправлено", message); } } fn main(){ let iphone = Smartphone{}; let tom = Person{device: iphone}; tom.send_message("Hello Rust!"); }
Здесь определен метод send_message()
для типа Person, для которого параметр T
представляет тип,
который одновременно реализует трейты Printer и Sender:
impl<T: Sender + Printer> Person<T>{ fn send_message(&self, message: &str){ self.device.print(message); self.device.send(message); } }
В данном примере таким типом является структура Smartphone.
Также можно применять альтернативный синтаксис с оператором where
:
impl<T> Person<T> where T: Sender + Printer{ fn send_message(&self, message: &str){ self.device.print(message); self.device.send(message); } }