特征 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 notify<T: 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 notify<T: Summary>(item1: &T, item2: &T) {}
泛型类型 T 说明了 item1 和 item2 必须拥有同样的类型,同时 T: Summary 说明了 T 必须实现 Summary 特征。
多重约束
除了单个约束条件,我们还可以指定多个约束条件,例如除了让参数实现 Summary 特征外,还可以让参数实现 Display 特征以控制它的格式化输出:
pub fn notify(item: &(impl Summary + Display)) {}
除了上述的语法糖形式,还能使用特征约束的形式:
pub fn notify<T: Summary + Display>(item: &T) {}
通过这两个特征,就可以使用 item.summarize 方法,以及通过 println!(“{}”, item) 来格式化输出 item。
Where 约束
当特征约束变得很多时,函数的签名将变得很复杂:
fn some_function<T: Display + Clone, U: Clone + Debug>(t: &T, u: &U) -> i32 {}
严格来说,上面的例子还是不够复杂,但是我们还是能对其做一些形式上的改进,通过 where:
fn some_function<T, U>(t: &T, u: &U) -> i32 where T: Display + Clone, U: Clone + Debug {}
使用特征约束有条件地实现方法或特征
特征约束,可以让我们在指定类型 + 指定特征的条件下去实现方法,例如:
use std::fmt::Display; struct Pair<T> { x: T, y: T, } impl<T> Pair<T> { fn new(x: T, y: T) -> Self { Self { x, y, } } } impl<T: Display + PartialOrd> Pair<T> { 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 方法,并不是所有的 Pair 结构体对象都可以拥有,只有 T 同时实现了 Display + PartialOrd 的 Pair 才可以拥有此方法。 该函数可读性会更好,因为泛型参数、参数、返回值都在一起,可以快速的阅读,同时每个泛型参数的特征也在新的代码行中通过特征约束进行了约束。
也可以有条件地实现特征, 例如,标准库为任何实现了 Display 特征的类型实现了 ToString 特征:
impl<T: Display> ToString for T { // --snip-- }
我们可以对任何实现了 Display 特征的类型调用由 ToString 定义的 to_string 方法。例如,可以将整型转换为对应的 String 值,因为整型实现了 Display:
let s = 3.to_string();
rust 泛型和特征(二)https://developer.aliyun.com/article/1391935