Trait или типаж - это способ определения общего поведения для типов. Трейты позволяют абстрагироваться от конкретных типов и сосредоточиться на том, что эти типы умеют делать. Это как знак отличия, стикер с надписью, что у типа есть некое свойство:
traitPrintable{// объявление трейта
fnprint(&self)-> String;// определяем, что Trait делает. Трейт утверждает печать, значит, по контракту обязан быть метод печати.
fninto_tuple(self)-> (i32,i32);// модель владения &self или self
}structPoint{x: i32,y: i32,}implPrintableforPoint{// применяем Trait для типа данных
fnprint(&self)-> String{format!("Point: ({}, {})",self.x,self.y)// пишем сам код
}fninto_tuple(self)-> (i32,i32){// self вместо &self = владение
(self.x,self.y)// Point поглощён, превращён tuple
}}fnmain(){letp=Point{x: 3,y: 4};println!("{}",p.print());// Вывод: Point: (3, 4)
}
По модели владения: обычно данными нужно пользоваться ещё после применения методов, поэтому всегда нужно объявлять &self и менять на self только в случае ошибок компилятора (в задачах трансформации данных, перемещения/отправки по сети, явного уничтожения).
Инициализация типажа
При объявлении типажа можно оставить обязательную реализацию на потом, либо вписать реализацию функций в типаже по-умолчанию:
// типаж задаёт метод и ограничения по входным/выходным типам
traitLandVehicle{fnLandDrive(&self)-> String;}// типаж задаёт методы плюс их реализация по умолчанию
traitWaterVehicle{fnWaterDrive(&self){println!("Default float");}}
Применение типажей к структурам данных
Во время применения, если реализация по умолчанию была задана, то можно её переделать под конкретную структуру, либо использовать эту реализацию:
structSedan{}structRocketship{}// типаж LandVehicle не имеет реализации по умолчанию, реализуем тут
implLandVehicleforSedan{fnLandDrive(&self)-> String{format!("Car zoom-zoom!")}}// типаж WaterVehicle имеет выше реализацию по умолчанию, используем её
implWaterVehicleforRocketship{}
Наследование и объединение типажей
Типажи могут наследовать другие типажи с помощью синтаксиса trait NewTrait: OldTrait:
Любой тип, реализующий Displayable, обязан также реализовать Printable.
При объединении типажей, создаётся ярлык (alias). При этом каждый входящий в него типаж нужно отдельно применить к структуре данных. При этом можно также использовать реализацию определённых в типаже методов по умолчанию, либо написать свою.
// создание ярлыка
traitAmphibiousVehicle: LandVehicle+WaterVehicle{}// применение типажей к структуре
implAmphibiousVehicleforCarrier{}implLandVehicleforCarrier{fnLandDrive(&self)-> String{format!("Use air thrust to travel on land")}}implWaterVehicleforCarrier{}
Вызов методов экземпляра структуры определённого типажа
Типажи определяют, какие возможности есть у generic типа T:
fnmy_function<T>(value: T)-> TwhereT: SomeTrait+AnotherTrait{/* function body */}
Eq (Equality)
Проверяет равенство == или неравенство !=;
Включает PartialEq (проверяет только равенство) + гарантирует тождество;
// Без Eq типажа:
// ❌ не будет компилироваться ==
fnfind_value<T>(items: &[T],target: T)-> bool{items.iter().any(|x|x==&target)// Error
}// Добавим Eq trait:
fnfind_value<T>(items: &[T],target: T)-> boolwhereT: Eq// теперь можно использовать ==
{items.iter().any(|x|x==&target)// ✅ работает!
}
std::hash::Hash
Позволяет конвертировать значения в хэш;
Требуется для хранения в HashMap, HashSet, или других коллекций с хэшом;
Тип с Hash может быть ключом или значением словаря:
usestd::collections::HashSet;// Без Hash:
// ❌ не будет компилироваться - HashSet требует Hash
fncreate_set<T>(items: Vec<T>)-> HashSet<T>{items.into_iter().collect()// Error: T doesn't implement Hash
}// С Hash:
fncreate_set<T>(items: Vec<T>)-> HashSet<T>whereT: Eq+std::hash::Hash// требуется для HashSet
{items.into_iter().collect()// ✅ работает!
}
Clone
Позволяет дублировать значение;
Разрешает метод .clone();
Нужно для копирования значений без взятия владения.
// Без Clone:
fnduplicate_first<T>(items: &[T])-> Option<T>{items.first().map(|x|x)// Error: can't return T from &T
}// Включаем Clone:
fnduplicate_first<T>(items: &[T])-> Option<T>whereT: Clone// можно клонировать
{items.first().cloned()// ✅ работает! - создаёт копию
}
// Converting different errors into one type
implFrom<NetworkError>forAppError{fnfrom(error: NetworkError)-> Self{AppError::Network(error.to_string())}}// Now functions can use ? operator automatically:
fndo_stuff()-> Result<(),AppError>{letdata=connect_to_server()?;// NetworkError automatically becomes AppError!
Ok(())}
// Anyone with the "Flying" badge must have a method called fly
traitFlying{fnfly(&self);// Takes a reference to itself, returns nothing
}// Trait implementation
// Our hero
structSuperHero{name: String,}// Our enemy
structWickedWitch{name: String,}// We give both the Flying badge, but they implement it differently
implFlyingforSuperHero{fnfly(&self){println!("{} soars through the sky majestically!",self.name);}}implFlyingforWickedWitch{fnfly(&self){println!("{} cackles loudly as her broomstick rattles into the air!",self.name);}}// ONE function, that works on ANYTHING with the Flying badge:
// This function doesn't care if it's a hero or a witch.
// It just says: "Bring me anything with the 'Flying' badge."
fnrace_to_the_castle(flyer: &implFlying){println!("The race starts!");flyer.fly();// We know it can fly, because it has the badge!
println!("We made it to the castle!");}fnmain(){lethero=SuperHero{name: String::from("Captain Soar")};letwitch=WickedWitch{name: String::from("Elvira")};// Both work perfectly!
race_to_the_castle(&hero);race_to_the_castle(&witch);}
Turbofish
При этом можно написать функцию race_to_the_castle двумя способами:
// короткий способ, использовался ранее:
fnrace_to_the_castle(flyer: &implFlying){flyer.fly();}// Способ turbofish way (полный generic syntax):
fnrace_to_the_castle<F: Flying>(flyer: &F){flyer.fly();}
Короткий способ подходит для простых случаев, но ломается на сложных. Примеры, когда нужно переходить на turbofish:
Несколько параметров должны быть ОДИНАКОВОГО типа
Нужны два животных для битвы, и они обязаны быть одинакового типа:
// SHORTHAND: This allows a Cat and a Dog to be shuffled around
fnshuffle_pair(a: &implAnimal,b: &implAnimal){// 'a' could be a Cat, 'b' could be a Dog
}// TURBOFISH: This FORCES both to be the same type
fnshuffle_pair<T: Animal>(a: &T,b: &T){// a and b MUST be the same kind of animal
}fnmain(){letcat=Cat{name: String::from("Whiskers")};letdog=Dog{name: String::from("Rover")};// Works with shorthand (different types allowed)
shuffle_pair(&cat,&dog);// OK
// ERROR with turbofish (cat and dog are different types!)
// shuffle_pair::<Cat>(&cat, &dog); // 💥 Dog ≠ Cat
}
Тип данных на выходе равен типу на входе
// You put in a specific animal, you get THAT same type back
fnclone_animal<A: Animal+Clone>(original: &A)-> A{original.clone()// Returns the same type that went in
}fnmain(){letcat=Cat{name: String::from("Whiskers")};letcloned_cat=clone_animal(&cat);// We know cloned_cat is a Cat
}
Нужно НЕСКОЛЬКО типажей одновременно
// This thing must be Flying AND have a BattleCry
fndramatic_entrance<F: Flying+BattleCry>(fighter: &F){fighter.battle_cry();fighter.fly();}
Подготовим пример: есть рыцарь и маг, оба могут держать оружие, но оружие у каждого своё.
// Define a weapon type
structSword{damage: u32,}structWand{magic_power: u32,}// This trait says "I can hold some type of item W"
// The <W> is a placeholder waiting to be filled
traitCanHold<W>{fnhold(&self,item: W);fndescribe_held_item(&self)-> String;}
В данном случае c параметром типаж как пустой стикер или пустой знак отличия, где поле ещё не заполнено конкретикой. Заполним конкретику:
structKnight{name: String,}structWizard{name: String,}// The knight can hold a Sword specifically
implCanHold<Sword>forKnight{fnhold(&self,item: Sword){println!("{} grips the sword firmly! Damage: {}",self.name,item.damage);}fndescribe_held_item(&self)-> String{String::from("A heavy blade")}}// The wizard can hold a Wand specifically
implCanHold<Wand>forWizard{fnhold(&self,item: Wand){println!("{} twirls the wand gracefully! Power: {}",self.name,item.magic_power);}fndescribe_held_item(&self)-> String{String::from("A mystical wand")}}
Теперь добавим, что рыцарь может держать в руках ещё щит - и это тоже попадает в типаж, остаётся лишь +1 параметр добавить:
structShield{defense: u32,}// The knight can ALSO hold a Shield!
implCanHold<Shield>forKnight{fnhold(&self,item: Shield){println!("{} raises the shield! Defense: {}",self.name,item.defense);}fndescribe_held_item(&self)-> String{String::from("A sturdy shield")}}fnmain(){letknight=Knight{name: String::from("Arthur")};// The same knight can hold different items!
knight.hold(Sword{damage: 50});// Uses CanHold<Sword>
knight.hold(Shield{defense: 30});// Uses CanHold<Shield> - same trait, different weapon!
}
Без параметра типаж работает НЕ БУДЕТ: потому что нельзя тот же типаж CanHold несколько раз применять просто так. А с параметром - МОЖНО, главное, чтобы параметр W отличался. Тогда один рыцарь может иметь несколько знаков из одной семьи знаков.
// AsRef is defined like this (simplified):
traitAsRef<T>{fnas_ref(&self)-> &T;// "Turn myself into a reference to T"
}// String implements AsRef<str> - "I can act as a string slice reference"
implAsRef<str>forString{fnas_ref(&self)-> &str{// String can easily give a &str view of its contents
&self[..]}}// &str also implements AsRef<str> - "I'm already a str reference!"
implAsRef<str>for&str{fnas_ref(&self)-> &str{self// Just return myself, I'm already what you want
}}// PathBuf (a file path type) implements AsRef<Path>
implAsRef<Path>forPathBuf{fnas_ref(&self)-> &Path{// Give a Path view of the PathBuf
self.as_path()}}
то это значит “приму любой тип N, если у N есть знак/стикер AsRef”, и это означает, что N может превратиться в &str.
Пример с превращениями
Представим игру, где персонажи могут превращаться в драконов или зайцев:
structPlayer{name: String,}structDragon{fire_power: u32,}structRabbit{speed: u32,}// The polymorph trait: "I can transform into T"
traitPolymorph<T>{fntransform(&self)-> T;}// Player can polymorph into a Dragon
implPolymorph<Dragon>forPlayer{fntransform(&self)-> Dragon{println!("{} grows scales and wings!",self.name);Dragon{fire_power: 100}}}// Player can ALSO polymorph into a Rabbit
implPolymorph<Rabbit>forPlayer{fntransform(&self)-> Rabbit{println!("{} shrinks and grows fluffy ears!",self.name);Rabbit{speed: 200}}}fnmain(){letplayer=Player{name: String::from("Alex")};// Same player, same trait name, completely different transformations!
letdragon_form=player.transform();// Rust infers we want Dragon here
letrabbit_form: Rabbit=player.transform();// Explicitly ask for Rabbit
}