1 rust中的trait学习
在substrate的开发中,或者说pallet的开发中,trait的使用是非常常见的,所以理解Rust中的trait非常重要。本节不会从头介绍trait的各种知识,如果你对Rust中的trait还不太了解,建议先学习trait基础知识后,再来学习本教程接下来的内容。接下来的内容都是假定你已经对trait有了基本的了解。
1.1 trait的孤儿规则
Rust中的trait在使用上和其它编程语言中的接口类似,为一个类型实现某个trait就类似于在其它编程语言中为某个类型实现对应的接口。但是在使用trait的时候,有一条 非常重要的原则(为什么重要?因为你如果不知道话,那么在某些时候会发现哪都应该ok,但是就是编译不过),那就是:
如果你想要为类型A实现trait T,那么A或者T至少有一个是在当前作用域中定义的
举两个例子。
- 正确的例子:
//例子1 | |
pub trait MyTrait { | |
fn print(); | |
} | |
pub struct MyType; | |
impl MyTrait for MyType { | |
fn print() { | |
println!("This is ok."); | |
} | |
} |
- 上面的例子1能够正确的编译,因为它遵循孤儿规则。
- 错误的例子:
//例子2 | |
use std::fmt::{Error, Formatter}; | |
impl std::fmt::Debug for () { | |
fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), Error> { | |
Ok(()) | |
} | |
} |
- 例子2无法编译通过,因为不管是trait Debug的定义还是类型()的定义都是外部的,所以无法在我们的代码中为类型()实现trait Debug。
1.2 trait对象
Rust中不直接将trait当作数据类型使用,但是可以将实现了某个trait的具体的类型当作trait对象使用。看以下例子:
trait Drive{ | |
fn drive(&self); | |
} | |
struct Truck; | |
impl Drive for Truck { | |
fn drive(&self) { | |
println!("Truck run!"); | |
} | |
} | |
struct MotorCycle; | |
impl Drive for MotorCycle { | |
fn drive(&self) { | |
println!("MotorCycle run!"); | |
} | |
} | |
fn use_transportation(t: Box<dyn Drive>) { | |
t.drive(); | |
} | |
fn main() { | |
let truck = Truck; | |
use_transportation(Box::new(truck)); | |
let moto = MotorCycle; | |
use_transportation(Box::new(moto)); | |
} |
在上面的例子中,use_transportation
的参数就是一个trait对象,不关注具体的类型,只需要它具备drive能力即可。
1.3 trait的继承
Rust只支持Trait之间的继承,比如Trait A继承Trait B,语法为:
trait B{} | |
trait A: B{} |
Trait A继承Trait B后,当某个类型C想要实现Trait A时,还必须要同时也去实现trait B。
1.4 关联类型
关联类型是在trait定义的语句块中,申明一个自定义类型,这样就可以在trait的方法签名中使用该类型。如下:
pub trait Iterator { | |
type Item; | |
fn next(&mut self) -> Option<Self::Item>; | |
} |
当为某个类型实现具有关联类型的trait时,需要指定关联类型为具体的类型,就像下面这样:
struct Counter(u32); | |
impl Iterator for Counter { | |
type Item = u32; | |
fn next(&mut self) -> Option<Self::Item> { | |
// --snip-- | |
} | |
} | |
fn main() { | |
let c = Counter(1); | |
c.next(); | |
} |
2 一个例子
下面我们来看一个Rust的例子:
trait SystemConfig { | |
fn system_configure(&self) { | |
println!("system configure."); | |
} | |
} | |
trait Config: SystemConfig { | |
type Event: ToString; | |
type Balance: ToString; | |
type Currency: ToString; | |
fn configure_event(&self, event: Self::Event); | |
fn configure_balance(&self, balance: Self::Balance); | |
fn configure_currency(&self, currency: Self::Currency); | |
} | |
struct Pallet { | |
event: u64, | |
balance: String, | |
currency: String, | |
} | |
impl SystemConfig for Pallet {} | |
impl Config for Pallet { | |
type Event = u64; | |
type Balance = String; | |
type Currency = String; | |
fn configure_event(&self, event: Self::Event) { | |
println!("configure, event is: {:?}", event); | |
} | |
fn configure_balance(&self, balance: Self::Balance) { | |
println!("configure, balance is: {:?}", balance); | |
} | |
fn configure_currency(&self, currency: Self::Currency) { | |
println!("configure, currency is: {:?}", currency); | |
} | |
} | |
impl Pallet { | |
fn new(event: u64, balance: String, currency: String) -> Self { | |
Pallet {event, balance, currency} | |
} | |
fn init(&self) { | |
self.configure_event(self.event); | |
self.configure_balance(self.balance.clone()); | |
self.configure_currency(self.currency.clone()); | |
} | |
} | |
fn main() { | |
let my_pallet = Pallet::new(1, "my balance".to_string(), "my currency".to_string()); | |
my_pallet.init(); | |
} |
上述代码中,我们定义了Config trait,然后为Pallet实现了相应的trait,最后在main函数中使用了它。
为什么要写这个例子?因为我觉得这个例子对后续我们写pallet时,涉及到的一些类型能有很好的理解。