Раннее в одной из предыдущих статей рассматривалось применение трейта в качестве параметра функции:
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);
}
}