Traits: Rust 中的统一概念
Traits: Rust 中的统一概念
Rust 的traits来自 Haskell 的type classes,它是下边这些的概念的统一:
-
Interfaces
接口
-
Abstract classes
抽象类
-
Mix-ins
混入
-
Operator overloading
运算符重载
-
Constraints on generics / Concepts 对泛型/概念的限制
-
Behavioral markers / Attributes 行为标记/属性
-
(bonus) Method overloading (奖励)方法重载
这真得很神奇!
我使用过的所有其他语言 [1] 都有上面列表的一些子集。我不认为他们中的任何一个都拥有所有这些,并且它们是具有自己的关键字和语法的不同概念。坦率地说,能够将所有这些统一起来真是太棒了。
Rust 没有特殊的接口语法,因为trait涵盖了这一点。 Rust 没有用于运算符重载的特殊语法,因为trait涵盖了这一点。等等。
为了强调这个想法的灵活性,以下是如何仅使用trait来完成上面列表中的所有操作:
Interfaces 接口
Logger
是一个具有两个方法的接口。它看起来与其他语言非常相似,只不过它显示的是 trait
而不是 interface
。
trait Logger {
fn log(&self, s: &str);
fn err(&self, e: &str);
}
struct StdoutLogger {}
impl Logger for StdoutLogger {
fn log(&self, s: &str) {
println!("{}", s);
}
fn err(&self, e: &str) {
self.log(e);
}
}
fn run(l: &dyn Logger) {
l.log("Hello");
l.err("Oops");
}
fn main() {
let l = StdoutLogger {};
run(&l);
}
很简单。
Abstract classes 抽象类
抽象类是不能直接实例化的,你必须扩展它来填充缺少的方法。在 Rust 中,这看起来像是具有默认实现的trait。继续上面的例子:
trait Logger {
fn log(&self, s: &str);
fn err(&self, e: &str) {
self.log(e);
}
}
struct StdoutLogger {}
impl Logger for StdoutLogger {
fn log(&self, s: &str) {
println!("{}", s);
}
}
Logger
现在提供 err
的默认实现,因此 StdoutLogger
不必这样做。 StdoutLogger
仍然可以提供它自己的实现,这将覆盖trait默认值。
Mix-ins 混入
混合是添加到现有类型的行为。在上面的示例中,我们定义了 trait
和 struct
。您当然可以从不同的包中实现trait。在这里,我们实现了标准库的 Display
特征,这就是 Rust 如何将类型转换为用户友好的字符串(如 Go 的 String()
或 Java 的 toString()
)。
struct StdoutLogger {}
impl std::fmt::Display for StdoutLogger {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
writeln!(f, "I am StdoutLogger")
}
}
fn main() {
let l = StdoutLogger {};
println!("{}", l);
}
但你可以用另一种方式做到这一点,这起初让我感到非常惊讶。您可以实现在其他包的类型上定义的trait,从而向它们添加行为(“混合”新行为)。该行为仅对导入您的实现的包可见。
trait SurelyNot {
fn can_we_build_it(&self);
}
impl SurelyNot for String {
fn can_we_build_it(&self) {
println!("Yes we can");
}
}
fn main() {
let s = String::new();
s.can_we_build_it();
}
您必须拥有 trait
或 struct
才能实现其中一个。你不能为你不拥有的类型实现你不拥有的trait。
Operator overloading 运算符重载
Rust 将其所有运算符( +
、 %
等)映射到 the ops crate中的trait。如果您的类型实现了该trait,则可以使用该运算符。
struct Sponge {
name: String,
}
impl std::ops::Add for Sponge {
type Output = Self;
fn add(self, other: Self) -> Self {
Sponge {
name: self.name + "X" + &other.name,
}
}
}
fn main() {
let m1 = Sponge {
name: "m1".to_string(),
};
let m2 = Sponge {
name: "m2".to_string(),
};
let m3 = m1 + m2;
println!("{}", m3.name);
}
仍然只是trait。没什么额外的。
Constraints on generics 对泛型的限制
这就是 C++ 所说的概念,Java 所说的“有界类型参数 bounded type parameters”,Go 泛型提案所说的“类型约束 type constraints”。如果您可以将任何接口定义为trait,并且还可以将任何运算符定义为trait,那么您现在就拥有了约束泛型类型参数所需的一切。
Here’s a Nameable
trait for things that can have their name set. We restrict it to being called with types that can be converted to a string (that implement the ToString
trait).
这是可以设置名称的事物的 Nameable
特征。我们将其限制为使用可以转换为字符串的类型(实现 ToString
特征)来调用。
trait Nameable<T: std::string::ToString> {
fn set_name(&mut self, T);
}
多亏了trait,这很简单。通常情况并非如此!限制泛型通常是一个难题。
C++ 在 90 年代中期出现了泛型(STL)。它几乎在 C++11 中得到了概念,在 C++17 中再次非常接近,并最终在 15 年后将它们合并到 C++20 中。
十年来,Go 一直在尝试添加泛型。多年来,它一直在努力通过多种形式的contract来限制泛型,然后才决定回避这个问题。在最新的提案中,您可以提供泛型类型可能的具体类型列表,而不是一般约束。
Behavioral markers / Attributes 行为标记/属性
Rust 定义了一些空marker traits来通知编译器有关类型的信息。这感觉类似于 C/C++/C# 的 attributes。
由于 Rust 也具有实际attributes,这有点复杂。公平地说,Rust 几乎没有什么东西是不具备的。它是一门大语言。
行为marker的一个例子是 Copy
特征。它说你的类型可以通过复制它的位来复制。例如,您可以为仅保存整数的类型实现此功能(实际上您“派生”它,因为它没有方法)。一个反例是保存内存或文件引用的类型;那不会是 Copy
。
Bonus: Method overloading, with generics
奖励:使用泛型的方法重载
Rust 没有正式的方法重载,出于同样的原因许多语言没有:如果方法不同,给它们不同的名称。然而,有两种不同的方法可以让你做一些感觉相同的事情。
如果您实现多个定义具有相同名称的方法的trait,Rust 有一种解决歧义的方法(单个trait不能多次使用同一个名称,即使参数不同)。这需要新的语法( TheTrait::method(&myObj)
),因此我没有将其包含在简介的列表中。它不再是“只是trait”。
更好的方法是使用泛型。我也没有将其包含在原始列表中,因为它不再只是提供功能的trait,而是泛型+trait。下面是一个可以采用 字符串或整数的方法 ( set_name
) 示例。
trait Nameable<T> {
fn set_name(&mut self, T);
}
struct Cyborg{
name: Option<String>,
}
impl Nameable<&str> for Cyborg {
fn set_name(&mut self, s: &str) {
self.name = Some(s.to_string());
}
}
impl Nameable<usize> for Cyborg {
fn set_name(&mut self, serial_number: usize) {
self.name = Some(serial_number.to_string());
}
}
fn main() {
let mut mostly_human = Cyborg{name: None};
mostly_human.set_name("Bob");
let mut mostly_machine = Cyborg{name: None};
mostly_machine.set_name(2077);
println!("{} vs {}",
mostly_human.name.unwrap(),
mostly_machine.name.unwrap(),
);
}
请注意,实际上您只需实现一次,接受任何实现 ToStr
的内容(如上面的“泛型约束”部分所示),然后调用 to_string()
。这个例子有点难,不过,还好。
它变得更好了。方法的返回类型可以是泛型的。这是 Rust 中我最喜欢的东西之一。调用的函数取决于您将输出分配给的变量的类型。一探究竟:
trait Position<T> {
fn pos(&self) -> T;
}
struct Location {
lat: f32,
lon: f32,
}
impl Position<String> for Location {
fn pos(&self) -> String {
format!("{},{}", self.lat, self.lon)
}
}
#[derive(Debug)]
struct Point {
x: f32,
y: f32,
}
impl Position<Point> for Location {
fn pos(&self) -> Point {
Point {
x: self.lat,
y: self.lon,
}
}
}
fn main() {
let l = Location {
lat: 37.05655,
lon: -121.88823,
};
let s: String = l.pos();
let p: Point = l.pos();
println!("As string: {}", s);
println!("As point: {:?}", p);
}
看看我们在底部如何调用 l.pos()
两次,它返回不同的东西!我喜欢它。
标准库在解析中充分利用了这一点,它将字符串解析为大量其他类型,如果您实现 FromStr
,则包括您自己的类型。它就像 atoi
但 ato*
。我使用它的次数远远超出了我应该使用的范围,因为它让我感到快乐。
结论
Rust 官方博客对in-depth discussion of traits有更深入的讨论。如果您已经读到这里,那么接下来您将阅读一篇很棒的文章。
鉴于您可以将所有不同的想法纳入traits中,有什么理由为什么未来的语言不使用它们呢?
转载自:https://juejin.cn/post/7352857362984157196