新乡做网站的多吗,辽宁建设集团招聘信息网站,如何备份网站程序,手机商城网站源码特征 Trait
定义特征
如果不同的类型具有相同的行为#xff0c;那么我们就可以定义一个特征#xff0c;然后为这些类型实现该特征。定义特征是把一些方法组合在一起#xff0c;目的是定义一个实现某些目标所必需的行为的集合。
例如#xff0c;我们现在有文章 Post 和微…特征 Trait
定义特征
如果不同的类型具有相同的行为那么我们就可以定义一个特征然后为这些类型实现该特征。定义特征是把一些方法组合在一起目的是定义一个实现某些目标所必需的行为的集合。
例如我们现在有文章 Post 和微博 Weibo 两种内容载体而我们想对相应的内容进行总结也就是无论是文章内容还是微博内容都可以在某个时间点进行总结那么总结这个行为就是共享的因此可以用特征来定义
pub trait Summary {fn summarize(self) - String;
}这里使用 trait 关键字来声明一个特征Summary 是特征名。在大括号中定义了该特征的所有方法在这个例子中是 fn summarize(self) - String。
特征只定义行为看起来是什么样的而不定义行为具体是怎么样的。因此我们只定义特征方法的签名而不进行实现此时方法签名结尾是 ;而不是一个 {}。
为类型实现特征
因为特征只定义行为看起来是什么样的因此我们需要为类型实现具体的特征定义行为具体是怎么样的。
首先来为 Post 和 Weibo 实现 Summary 特征
pub trait Summary {fn summarize(self) - String;
}
pub struct Post {pub title: String, // 标题pub author: String, // 作者pub content: String, // 内容
}impl Summary for Post {fn summarize(self) - String {format!(文章{}, 作者是{}, self.title, self.author)}
}pub struct Weibo {pub username: String,pub content: String
}impl Summary for Weibo {fn summarize(self) - String {format!({}发表了微博{}, self.username, self.content)}
}实现特征的语法与为结构体、枚举实现方法很像impl Summary for Post读作“为 Post 类型实现 Summary 特征”然后在 impl 的花括号中实现该特征的具体方法。
接下来就可以在这个类型上调用特征的方法
fn main() {let post Post{title: Rust语言简介.to_string(),author: Sunface.to_string(), content: Rust棒极了!.to_string()};let weibo Weibo{username: sunface.to_string(),content: 好像微博没Tweet好用.to_string()};println!({},post.summarize());println!({},weibo.summarize());
}运行输出
文章 Rust 语言简介, 作者是Sunface
sunface发表了微博好像微博没Tweet好用特征定义与实现的位置(孤儿规则)
上面我们将 Summary 定义成了 pub 公开的。这样如果他人想要使用我们的 Summary 特征则可以引入到他们的包中然后再进行实现。
关于特征实现与定义的位置有一条非常重要的原则如果你想要为类型 A 实现特征 T那么 A 或者 T 至少有一个是在当前作用域中定义的 例如我们可以为上面的 Post 类型实现标准库中的 Display 特征这是因为 Post 类型定义在当前的作用域中。同时我们也可以在当前包中为 String 类型实现 Summary 特征因为 Summary 定义在当前作用域中。
默认实现
你可以在特征中定义具有默认实现的方法这样其它类型无需再实现该方法或者也可以选择重载该方法
pub trait Summary {fn summarize(self) - String {String::from((Read more...))}
}上面为 Summary 定义了一个默认实现下面我们编写段代码来测试下
impl Summary for Post {}impl Summary for Weibo {fn summarize(self) - String {format!({}发表了微博{}, self.username, self.content)}
}可以看到Post 选择了默认实现而 Weibo 重载了该方法调用和输出如下 println!({},post.summarize());println!({},weibo.summarize());
(Read more...)
sunface发表了微博好像微博没Tweet好用
默认实现允许调用相同特征中的其他方法哪怕这些方法没有默认实现。如此特征可以提供很多有用的功能而只需要实现指定的一小部分内容。例如我们可以定义 Summary 特征使其具有一个需要实现的 summarize_author 方法然后定义一个 summarize 方法此方法的默认实现调用 summarize_author 方法
pub trait Summary {fn summarize_author(self) - String;fn summarize(self) - String {format!((Read more from {}...), self.summarize_author())}
}为了使用 Summary只需要实现 summarize_author 方法即可
impl Summary for Weibo {fn summarize_author(self) - String {format!({}, self.username)}
}
println!(1 new weibo: {}, weibo.summarize());weibo.summarize() 会先调用 Summary 特征默认实现的 summarize 方法通过该方法进而调用 Weibo 为 Summary 实现的 summarize_author 方法最终输出1 new weibo: (Read more from horse_ebooks…)。
使用特征作为函数参数
pub fn notify(item: impl Summary) {println!(Breaking news! {}, item.summarize());
}impl Summary只能说想出这个类型的人真的是起名鬼才简直太贴切了顾名思义它的意思是 实现了Summary特征 的 item 参数。
你可以使用任何实现了 Summary 特征的类型作为该函数的参数同时在函数体内还可以调用该特征的方法例如 summarize 方法。具体的说可以传递 Post 或 Weibo 的实例来作为参数而其它类如 String 或者 i32 的类型则不能用做该函数的参数因为它们没有实现 Summary 特征。
特征约束(trait bound)
虽然 impl Trait 这种语法非常好理解但是实际上它只是一个语法糖
pub fn notifyT: Summary(item: T) {println!(Breaking news! {}, item.summarize());
}真正的完整书写形式如上所述形如 T: Summary 被称为特征约束。
在简单的场景下 impl Trait 这种语法糖就足够使用但是对于复杂的场景特征约束可以让我们拥有更大的灵活性和语法表现能力例如一个函数接受两个 impl Summary 的参数
pub fn notify(item1: impl Summary, item2: impl Summary) {}如果函数两个参数是不同的类型那么上面的方法很好只要这两个类型都实现了 Summary 特征即可。但是如果我们想要强制函数的两个参数是同一类型呢上面的语法就无法做到这种限制此时我们只能使特征约束来实现
pub fn notifyT: Summary(item1: T, item2: T) {}泛型类型 T 说明了 item1 和 item2 必须拥有同样的类型同时 T: Summary 说明了 T 必须实现 Summary 特征。
多重约束
除了单个约束条件我们还可以指定多个约束条件例如除了让参数实现 Summary 特征外还可以让参数实现 Display 特征以控制它的格式化输出
pub fn notify(item: (impl Summary Display)) {}除了上述的语法糖形式还能使用特征约束的形式
pub fn notifyT: Summary Display(item: T) {}通过这两个特征就可以使用 item.summarize 方法以及通过 println!(“{}”, item) 来格式化输出 item。
Where 约束
当特征约束变得很多时函数的签名将变得很复杂
fn some_functionT: Display Clone, U: Clone Debug(t: T, u: U) - i32 {}严格来说上面的例子还是不够复杂但是我们还是能对其做一些形式上的改进通过 where
fn some_functionT, U(t: T, u: U) - i32where T: Display Clone,U: Clone Debug
{}使用特征约束有条件地实现方法或特征
特征约束可以让我们在指定类型 指定特征的条件下去实现方法例如
use std::fmt::Display;struct PairT {x: T,y: T,
}implT PairT {fn new(x: T, y: T) - Self {Self {x,y,}}
}implT: Display PartialOrd PairT {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);}}
}
cmp_display 方法并不是所有的 PairT 结构体对象都可以拥有只有 T 同时实现了 Display PartialOrd 的 PairT 才可以拥有此方法。 该函数可读性会更好因为泛型参数、参数、返回值都在一起可以快速的阅读同时每个泛型参数的特征也在新的代码行中通过特征约束进行了约束。
也可以有条件地实现特征, 例如标准库为任何实现了 Display 特征的类型实现了 ToString 特征
implT: Display ToString for T {// --snip--
}我们可以对任何实现了 Display 特征的类型调用由 ToString 定义的 to_string 方法。例如可以将整型转换为对应的 String 值因为整型实现了 Display
let s 3.to_string();函数返回中的 impl Trait
可以通过 impl Trait 来说明一个函数返回了一个类型该类型实现了某个特征
fn returns_summarizable() - impl Summary {Weibo {username: String::from(sunface),content: String::from(m1 max太厉害了电脑再也不会卡,)}
}因为 Weibo 实现了 Summary因此这里可以用它来作为返回值。要注意的是虽然我们知道这里是一个 Weibo 类型但是对于 returns_summarizable 的调用者而言他只知道返回了一个实现了 Summary 特征的对象但是并不知道返回了一个 Weibo 类型。
这种 impl Trait 形式的返回值在一种场景下非常非常有用那就是返回的真实类型非常复杂你不知道该怎么声明时(毕竟 Rust 要求你必须标出所有的类型)此时就可以用 impl Trait 的方式简单返回。例如闭包和迭代器就是很复杂只有编译器才知道那玩意的真实类型如果让你写出来它们的具体类型估计内心有一万只草泥马奔腾好在你可以用 impl Iterator 来告诉调用者返回了一个迭代器因为所有迭代器都会实现 Iterator 特征。
但是这种返回值方式有一个很大的限制只能有一个具体的类型例如
fn returns_summarizable(switch: bool) - impl Summary {if switch {Post {title: String::from(Penguins win the Stanley Cup Championship!,),author: String::from(Iceburgh),content: String::from(The Pittsburgh Penguins once again are the best \hockey team in the NHL.,),}} else {Weibo {username: String::from(horse_ebooks),content: String::from(of course, as you probably already know, people,),}}
}通过 derive 派生特征
形如 #[derive(Debug)] 的代码已经出现了很多次这种是一种特征派生语法被 derive 标记的对象会自动实现对应的默认特征代码继承相应的功能。
例如 Debug 特征它有一套自动实现的默认代码当你给一个结构体标记后就可以使用 println!(“{:?}”, s) 的形式打印该结构体的对象。
再如 Copy 特征它也有一套自动实现的默认代码当标记到一个类型上时可以让这个类型自动实现 Copy 特征进而可以调用 copy 方法进行自我复制。
总之derive 派生出来的是 Rust 默认给我们提供的特征在开发过程中极大的简化了自己手动实现相应特征的需求当然如果你有特殊的需求还可以自己手动重载该实现。
调用方法需要引入特征
在一些场景中使用 as 关键字做类型转换会有比较大的限制因为你想要在类型转换上拥有完全的控制例如处理转换错误那么你将需要 TryInto
use std::convert::TryInto;fn main() {let a: i32 10;let b: u16 100;let b_ b.try_into().unwrap();if a b_ {println!(Ten is less than one hundred.);}
}特征对象
pub struct Button {pub width: u32,pub height: u32,pub label: String,
}impl Draw for Button {fn draw(self) {// 绘制按钮的代码}
}struct SelectBox {width: u32,height: u32,options: VecString,
}impl Draw for SelectBox {fn draw(self) {// 绘制SelectBox的代码}
}
深入了解特征
关联类型
关联类型是在特征定义的语句块中申明一个自定义类型这样就可以在特征的方法签名中使用该类型
pub trait Iterator {type Item;fn next(mut self) - OptionSelf::Item;
}以上是标准库中的迭代器特征 Iterator它有一个 Item 关联类型用于替代遍历的值的类型。
同时next 方法也返回了一个 Item 类型不过使用 Option 枚举进行了包裹假如迭代器中的值是 i32 类型那么调用 next 方法就将获取一个 Optioni32 的值。 impl Iterator for Counter {type Item u32;fn next(mut self) - OptionSelf::Item {// --snip--}
}fn main() {let c Counter{..}c.next()
}在上述代码中我们为 Counter 类型实现了 Iterator 特征变量 c 是特征 Iterator 的实例也是 next 方法的调用者。 结合之前的黑体内容可以得出对于 next 方法而言Self 是调用者 c 的具体类型 Counter而 Self::Item 是 Counter 中定义的 Item 类型: u32。
pub trait IteratorItem {fn next(mut self) - OptionItem;
}答案其实很简单为了代码的可读性当你使用了泛型后你需要在所有地方都写 IteratorItem而使用了关联类型你只需要写 Iterator当类型定义复杂时这种写法可以极大的增加可读性
pub trait CacheableItem: Clone Default fmt::Debug Decodable Encodable {type Address: AsRef[u8] Clone fmt::Debug Eq Hash;fn is_null(self) - bool;
}例如上面的代码Address 的写法自然远比 AsRef[u8] Clone fmt::Debug Eq Hash 要简单的多而且含义清晰。
再例如如果使用泛型你将得到以下的代码
trait ContainerA,B {fn contains(self,a: A,b: B) - bool;
}fn differenceA,B,C(container: C) - i32whereC : ContainerA,B {...}可以看到由于使用了泛型导致函数头部也必须增加泛型的声明而使用关联类型将得到可读性好得多的代码
trait Container{type A;type B;fn contains(self, a: Self::A, b: Self::B) - bool;
}fn differenceC: Container(container: C) {}默认泛型类型参数
当使用泛型类型参数时可以为其指定一个默认的具体类型例如标准库中的 std::ops::Add 特征
trait AddRHSSelf {type Output;fn add(self, rhs: RHS) - Self::Output;
}它有一个泛型参数 RHS但是与我们以往的用法不同这里它给 RHS 一个默认值也就是当用户不指定 RHS 时默认使用两个同样类型的值进行相加然后返回一个关联类型 Output。
可能上面那段不太好理解下面我们用代码来举例
use std::ops::Add;#[derive(Debug, PartialEq)]
struct Point {x: i32,y: i32,
}impl Add for Point {type Output Point;fn add(self, other: Point) - Point {Point {x: self.x other.x,y: self.y other.y,}}
}fn main() {assert_eq!(Point { x: 1, y: 0 } Point { x: 2, y: 3 },Point { x: 3, y: 3 });
}上面的代码主要干了一件事就是为 Point 结构体提供 的能力这就是运算符重载不过 Rust 并不支持创建自定义运算符你也无法为所有运算符进行重载目前来说只有定义在 std::ops 中的运算符才能进行重载。
跟 对应的特征是 std::ops::Add我们在之前也看过它的定义 trait AddRHSSelf但是上面的例子中并没有为 Point 实现 AddRHS 特征而是实现了 Add 特征没有默认泛型类型参数这意味着我们使用了 RHS 的默认类型也就是 Self。换句话说我们这里定义的是两个相同的 Point 类型相加因此无需指定 RHS。
与上面的例子相反下面的例子我们来创建两个不同类型的相加
use std::ops::Add;struct Millimeters(u32);
struct Meters(u32);impl AddMeters for Millimeters {type Output Millimeters;fn add(self, other: Meters) - Millimeters {Millimeters(self.0 (other.0 * 1000))}
}这里是进行 Millimeters Meters 两种数据类型的 操作因此此时不能再使用默认的 RHS否则就会变成 Millimeters Millimeters 的形式。使用 AddMeters 可以将 RHS 指定为 Meters那么 fn add(self, rhs: RHS) 自然而言的变成了 Millimeters 和 Meters 的相加。
默认类型参数主要用于两个方面
减少实现的样板代码扩展类型但是无需大幅修改现有的代码
调用同名的方法
不同特征拥有同名的方法是很正常的事情你没有任何办法阻止这一点甚至除了特征上的同名方法外在你的类型上也有同名方法
trait Pilot {fn fly(self);
}trait Wizard {fn fly(self);
}struct Human;impl Pilot for Human {fn fly(self) {println!(This is your captain speaking.);}
}impl Wizard for Human {fn fly(self) {println!(Up!);}
}impl Human {fn fly(self) {println!(*waving arms furiously*);}
}
这里不仅仅两个特征 Pilot 和 Wizard 有 fly 方法就连实现那两个特征的 Human 单元结构体也拥有一个同名方法 fly
优先调用类型上的方法
当调用 Human 实例的 fly 时编译器默认调用该类型中定义的方法
fn main() {let person Human;person.fly();
}这段代码会打印 waving arms furiously说明直接调用了类型上定义的方法。
调用特征上的方法
为了能够调用两个特征的方法需要使用显式调用的语法
fn main() {let person Human;Pilot::fly(person); // 调用Pilot特征上的方法Wizard::fly(person); // 调用Wizard特征上的方法person.fly(); // 调用Human类型自身的方法
}
运行后依次输出
This is your captain speaking.
Up!
*waving arms furiously*因为 fly 方法的参数是 self当显式调用时编译器就可以根据调用的类型( self 的类型)决定具体调用哪个方法。
这个时候问题又来了如果方法没有 self 参数呢稍等估计有读者会问还有方法没有 self 参数
trait Animal {fn baby_name() - String;
}struct Dog;impl Dog {fn baby_name() - String {String::from(Spot)}
}impl Animal for Dog {fn baby_name() - String {String::from(puppy)}
}fn main() {println!(A baby dog is called a {}, Dog::baby_name());
}Dog::baby_name() 的调用方式显然不行因为这只是狗妈妈对宝宝的爱称可能你会想到通过下面的方式查询其他动物对狗狗的称呼
fn main() {println!(A baby dog is called a {}, Animal::baby_name());
}error[E0283]: type annotations needed // 需要类型注释-- src/main.rs:20:43|
20 | println!(A baby dog is called a {}, Animal::baby_name());| ^^^^^^^^^^^^^^^^^ cannot infer type // 无法推断类型| note: cannot satisfy _: Animal
完全限定语法
完全限定语法是调用函数最为明确的方式
fn main() {println!(A baby dog is called a {}, Dog as Animal::baby_name());
}在尖括号中通过 as 关键字我们向 Rust 编译器提供了类型注解也就是 Animal 就是 Dog而不是其他动物因此最终会调用 impl Animal for Dog 中的方法获取到其它动物对狗宝宝的称呼puppy。
特征定义中的特征约束
有时我们会需要让某个特征 A 能使用另一个特征 B 的功能(另一种形式的特征约束)这种情况下不仅仅要为类型实现特征 A还要为类型实现特征 B 才行这就是 supertrait (实在不知道该如何翻译有大佬指导下嘛)
例如有一个特征 OutlinePrint它有一个方法能够对当前的实现类型进行格式化输出
use std::fmt::Display;trait OutlinePrint: Display {fn outline_print(self) {let output self.to_string();let len output.len();println!({}, *.repeat(len 4));println!(*{}*, .repeat(len 2));println!(* {} *, output);println!(*{}*, .repeat(len 2));println!({}, *.repeat(len 4));}
}等等这里有一个眼熟的语法: OutlinePrint: Display感觉很像之前讲过的特征约束只不过用在了特征定义中而不是函数的参数中是的在某种意义上来说这和特征约束非常类似都用来说明一个特征需要实现另一个特征这里就是如果你想要实现 OutlinePrint 特征首先你需要实现 Display 特征。
想象一下假如没有这个特征约束那么 self.to_string 还能够调用吗 to_string 方法会为实现 Display 特征的类型自动实现编译器肯定是不愿意的会报错说当前作用域中找不到用于 Self 类型的方法 to_string
struct Point {x: i32,y: i32,
}impl OutlinePrint for Point {}因为 Point 没有实现 Display 特征会得到下面的报错
error[E0277]: the trait bound Point: std::fmt::Display is not satisfied-- src/main.rs:20:6|
20 | impl OutlinePrint for Point {}| ^^^^^^^^^^^^ Point cannot be formatted with the default formatter;
try using :? instead if you are using a format string| help: the trait std::fmt::Display is not implemented for Point既然我们有求于编译器那只能选择满足它咯
use std::fmt;impl fmt::Display for Point {fn fmt(self, f: mut fmt::Formatter) - fmt::Result {write!(f, ({}, {}), self.x, self.y)}
}上面代码为 Point 实现了 Display 特征那么 to_string 方法也将自动实现最终获得字符串是通过这里的 fmt 方法获得的。
在外部类型上实现外部特征(newtype)
有提到孤儿规则简单来说就是特征或者类型必需至少有一个是本地的才能在此类型上定义特征。
这里提供一个办法来绕过孤儿规则那就是使用newtype 模式简而言之就是为一个元组结构体创建新类型。该元组结构体封装有一个字段该字段就是希望实现特征的具体类型。
该封装类型是本地的因此我们可以为此类型实现外部的特征。
newtype 不仅仅能实现以上的功能而且它在运行时没有任何性能损耗因为在编译期该类型会被自动忽略。
use std::fmt;struct Wrapper(VecString);impl fmt::Display for Wrapper {fn fmt(self, f: mut fmt::Formatter) - fmt::Result {write!(f, [{}], self.0.join(, ))}
}fn main() {let w Wrapper(vec![String::from(hello), String::from(world)]);println!(w {}, w);
}其中struct Wrapper(VecString) 就是一个元组结构体它定义了一个新类型 Wrapper代码很简单相信大家也很容易看懂。
既然 new type 有这么多好处它有没有不好的地方呢答案是肯定的。注意到我们怎么访问里面的数组吗self.0.join(, )是的很啰嗦因为需要先从 Wrapper 中取出数组: self.0然后才能执行 join 方法。
类似的任何数组上的方法你都无法直接调用需要先用 self.0 取出数组然后再进行调用。
当然解决办法还是有的要不怎么说 Rust 是极其强大灵活的编程语言Rust 提供了一个特征叫 Deref实现该特征后可以自动做一层类似类型转换的操作可以将 Wrapper 变成 VecString 来使用。这样就会像直接使用数组那样去使用 Wrapper而无需为每一个操作都添加上 self.0。
同时如果不想 Wrapper 暴露底层数组的所有方法我们还可以为 Wrapper 去重载这些方法实现隐藏的目的。