找回密码
 立即注册
首页 业界区 业界 Rust从入门到精通07-trait

Rust从入门到精通07-trait

掳诚 4 小时前
Rust 语言中,trait 是一个非常重要的概念,可以包含:函数、常量、类型等。
通俗一点理解,trait 以一种抽象的方式定义共享的行为,可以被认为是一些语言的接口,但是与接口也有一定区别,下面会介绍。
1、成员方法

trait 中可以定义方法。
  1. trait Shape {
  2.     fn area(&self) -> f64;
  3. }
复制代码
我们在一个名为 Shape 的 trait 中定义了一个方法 area。
1.1 方法参数

看上面定义的 Shape,方法的参数是 &self。
其实对于每个 trait 都有一个隐藏的类型 Self(大写的 S),代表实现此 trait 的具体类型。
Rust 中 Self 和 self 都是关键字,大写的Self是类型名,小写的 self 是变量名。
其实 area(&self) 等价于 area(self : &Self),只不过 rust 提供了简化的写法。
下面几种情况都是等价的。
  1. trait T {
  2.     fn method1(self : Self);
  3.     fn method2(self : &Self);
  4.     fn method3(self : &mut Self);
  5. }
  6. //等价于下面方法定义
  7. trait T {
  8.     fn method1(self);
  9.     fn method2(&self);
  10.     fn method3(&mut self);
  11. }
复制代码
1.2 调用实例

可以参考如下例子:
  1. trait Shape {
  2.     fn area(&self) -> f64;
  3. }struct Circle {    radius : f64,}impl Shape for Circle {    // Self 的类型就是 Circle    fn area(self : &Self) -> f64{        // 可以通过self.radius访问成员变量        std::f64::consts::PI * self.radius * self.radius    }}fn main() {    let circle = Circle{ radius : 2f64};    println!("The area is {}",circle.area())}
复制代码
①、通过 self.成员变量 来访问成员变量;
②、通过 实例.成员方法 来调用成员方法;
2、匿名 trait
  1. impl Circle {
  2.     fn get_radius(&self) -> f64 {
  3.         self.radius
  4.     }
  5. }
复制代码
impl 关键字后面直接接类型,没有 trait 的名字。
可以将上面代码看成是为 Circle 实现了一个匿名的 trait。
3、 静态方法

静态方法:第一个参数不是 self 参数的方法。
  1. impl Circle {
  2.     // 普通方法
  3.     fn get_radius(&self) -> f64 {
  4.         self.radius
  5.     }
  6.     // 静态方法
  7.     fn get_area(this : &Self) ->f64 {
  8.         std::f64::consts::PI * this.radius * this.radius
  9.     }
  10. }
  11. fn main() {
  12.     let c = Circle{ radius : 2f64};
  13.     // 调用普通方法
  14.     println!("The radius is {}",c.radius);
  15.     // 调用静态方法
  16.     println!("The area is {}",Circle::get_area(&c))
  17. }
复制代码
注意和普通方法的区别,参数命名不同,以及调用方式不同(普通方法是小数 实例.方法 ,静态方法是 类型::方法 )。
静态方法的调用可以 Type::FunctionName()。
4、扩展方法

利用 trait 给其它类型添加方法。
比如我们给内置类型 i32 添加一个方法:
  1. // 扩展方法
  2. trait Double {
  3.     fn double(&self) -> Self;
  4. }
  5. impl Double for i32 {
  6.     fn double(&self) -> i32{
  7.         self * 2
  8.     }
  9. }
  10. fn main() {
  11.     let x : i32 = 10.double();
  12.     println!("x double is {}",x);//20
  13. }
复制代码
5、泛型约束

在Rust中,静态分发(Static Dispatch)和动态分发(Dynamic Dispatch)是用于选择和调用函数的两种不同的机制。
5.1 静态分发

在编译时确定函数调用的具体实现。
它通过在编译阶段解析函数调用并选择正确的函数实现,从而实现高效的调用。
静态分发通常适用于使用泛型的情况,其中编译器可以根据具体的类型参数确定调用的函数。
  1. fn main() {
  2.     fn myPrint<T: ToString>(v: T) {
  3.         v.to_string();
  4.     }
  5.    
  6.     let c = 'a';
  7.     let s = String::from("hello");
  8.    
  9.     myPrint::<char>(c);
  10.     myPrint::<String>(s);
  11. }
复制代码
等价于:
  1. fn myPrint(c:char){
  2.     c.to_string();
  3. }
  4. fn myPrint(str:String){
  5.     str.to_string();
  6. }
复制代码
5.2 动态分发

在运行时根据对象的实际类型来选择函数的实现。
它适用于使用trait对象(通过使用dyn关键字)的情况,其中编译器在编译阶段无法确定具体的函数实现。
在运行时,程序会根据trait对象所包含的实际类型来动态地选择要调用的函数。
动态分发提供了更大的灵活性,但相对于静态分发,它可能会带来一些运行时开销。
下面代码分别演示静态分发和动态分发的区别:
  1. trait Animal {
  2.     fn make_sound(&self);
  3. }
  4. struct Cat;
  5. struct Dog;
  6. impl Animal for Cat {
  7.     fn make_sound(&self) {
  8.         println!("Meow!");
  9.     }
  10. }
  11. impl Animal for Dog {
  12.     fn make_sound(&self) {
  13.         println!("Woof!");
  14.     }
  15. }
  16. fn static_dispatch(animal: &impl Animal) {
  17.     animal.make_sound();
  18. }
  19. fn dynamic_dispatch(animal: &dyn Animal) {
  20.     animal.make_sound();
  21. }
  22. fn main() {
  23.     let cat = Cat;
  24.     let dog = Dog;
  25.     // 静态分发
  26.     static_dispatch(&cat);
  27.     static_dispatch(&dog);
  28.     // 动态分发
  29.     dynamic_dispatch(&cat as &dyn Animal);
  30.     dynamic_dispatch(&dog as &dyn Animal);
  31. }
复制代码
5、一致性原则

一致性原则,也称为孤儿原则(Orphan Rule):
Impl 块要么与 trait 块的声明在同一个 crate 中,要么与类型的声明在同一个 crate 中。
孤儿原则(Orphan Rule)是Rust语言中的一项重要设计原则,它有助于确保trait实现的可控性和可追溯性。遵守孤儿原则可以提高代码的可读性和可维护性,并降低潜在的冲突和混乱。
也就是说如果 trait 来自外部,而且类型也来自外部 crate,编译器是不允许你为这个类型 impl 这个 trait。它们当中至少有一个是在当前 crate 中定义的。
比如下面两种情况都是可以的:
  1. use std::fmt::Display;
  2. struct A;
  3. impl Display for A {}
复制代码
  1. trait TraitA {}
  2. impl TraitA for u32 {}
复制代码
但是下面这种情况就不可以:
  1. use std::fmt::Display;
  2. impl Display for u32 {}
复制代码
1.png

这也给我们提供了一个标准:上游开发者在写库的时候,一些比较常用的标准 trait,如 Display/Debug/ToString/Default 等,应该尽可能的提供好。
否则下游使用这个库的开发者是没法帮我们实现这些 trait 的。
6、trait 和 接口区别

开篇我们说为了便于理解 trait,可以想象为其它语言,比如Java中的接口。但是实际上他们还是有很大的区别的。
因为 rust 是一种用户可以对内存有着精确控制的强类型语言。在目前 Rust 版本中规定:
函数传参类型,返回值类型等都是要在编译期确定大小的。
而 trait 本身既不是具体类型,也不是指针类型,它只是定义了针对类型的、抽象的约束。不同的类型可以实现同一个 trait,满足同一个 trait 的类型可能具有不同的大小。
所以 trait 在编译阶段没有固定的大小,我们不能直接使用 trait 作为实例变量、参数以及返回值。
类似下面的写法都是错误的:
  1. trait Shape {
  2.     fn area(&self) -> f64;
  3. }impl Circle {    //错误1: trait(Shape)不能做参数的类型    fn use_shape(arg : Shape){    }    //错误2: trait(Shape)不能做返回值的类型    fn ret_shape() -> Shape{    }}fn main() {    // 错误3:trait(Shape)不能做局部变量的类型    let x : Shape = Circle::new();}
复制代码
可以看到编译器的错误提示:
2.png

7、derive

Rust 标准库内部实现了一些逻辑较为固定的 trait,通过 derive 配置可以帮助我们自动 impl 某些 trait,而无需手动编写对应的代码。
  1. #[derive(Debug)]
  2. struct Foo {
  3.     data : i32,
  4. }
  5. fn main() {
  6.     let v1 = Foo{data : 0};
  7.     println!("{:?}",v1)
  8. }
复制代码
加上 Debug 的trait 实现,便于格式化打印 struct。
[derive(Debug)] 等价于 impl Debug for Foo {}

目前,Rust 支持的可以自动 derive 的 trait 有如下:
  1. Copy,Clone,Default,Hash,
  2. Debug,PartialEq,Eq,PartialOrd,
  3. Ord,RustcEncodable,RustcDecodable,
  4. FromPrimitive,Send,Sync
复制代码
8、标准库中常见 trait

在介绍 derive 时,我们说明了内置的一些 trait,这都是标准库中比较常见的 trait,下面我们分别介绍这些 trait 是干什么的。
8.1 Display 和 Debug

可以分别看下源码定义:
【Display】
  1. pub trait Display {
  2.     /// Formats the value using the given formatter.
  3.     ///
  4.     /// # Examples
  5.     ///
  6.     /// ```
  7.     /// use std::fmt;
  8.     ///
  9.     /// struct Position {
  10.     ///     longitude: f32,
  11.     ///     latitude: f32,
  12.     /// }
  13.     ///
  14.     /// impl fmt::Display for Position {
  15.     ///     fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
  16.     ///         write!(f, "({}, {})", self.longitude, self.latitude)
  17.     ///     }
  18.     /// }
  19.     ///
  20.     /// assert_eq!("(1.987, 2.983)",
  21.     ///            format!("{}", Position { longitude: 1.987, latitude: 2.983, }));
  22.     /// ```
  23.     #[stable(feature = "rust1", since = "1.0.0")]
  24.     fn fmt(&self, f: &mut Formatter<'_>) -> Result;
  25. }
复制代码
【Debug】
  1. pub trait Debug {
  2.     /// Formats the value using the given formatter.
  3.     ///
  4.     /// # Examples
  5.     ///
  6.     /// ```
  7.     /// use std::fmt;
  8.     ///
  9.     /// struct Position {
  10.     ///     longitude: f32,
  11.     ///     latitude: f32,
  12.     /// }
  13.     ///
  14.     /// impl fmt::Debug for Position {
  15.     ///     fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
  16.     ///         f.debug_tuple("")
  17.     ///          .field(&self.longitude)
  18.     ///          .field(&self.latitude)
  19.     ///          .finish()
  20.     ///     }
  21.     /// }
  22.     ///
  23.     /// let position = Position { longitude: 1.987, latitude: 2.983 };
  24.     /// assert_eq!(format!("{:?}", position), "(1.987, 2.983)");
  25.     ///
  26.     /// assert_eq!(format!("{:#?}", position), "(
  27.     ///     1.987,
  28.     ///     2.983,
  29.     /// )");
  30.     /// ```
  31.     #[stable(feature = "rust1", since = "1.0.0")]
  32.     fn fmt(&self, f: &mut Formatter<'_>) -> Result;
  33. }
复制代码
①、只有实现了 Display trait 的类型,才能够用 {} 格式打印出来。
②、只有实现了 Debug trait 的类型,才能够用{:?} {:#?} 格式打印出来。
这两者区别如下:
1、Display 假定了这个类型可以用 utf-8 格式的字符串表示,它是准备给最终用户看的,并不是所有的类型都应该或者能够实现这个 trait。这个 trait 的 fmt 应该如何格式化字符串,完全取决于程序员自己,编译器不提供自动 derive 的功能。
2、标准库中还有一个常用 trait 叫作 std::string::ToString,对于所有实现了 Display trait 的类型,都自动实现了这个 ToString trait 。它包含了一个方法 to_string(&self) -> String。任何一个实现了 Display trait 的类型,我们都可以对它调用 to_string() 方法格式化出一个字符串。
3、Debug 则主要是为了调试使用,建议所有的作为 API 的“公开”类型都应该实现这个 trait,以方便调试。它打印出来的字符串不是以“美观易读”为标准,编译器提供了自动 derive 的功能。
  1. struct Color{
  2.     r:u8,
  3.     g:u8,
  4.     b:u8,
  5. }
  6. impl Default for Color{
  7.     fn default() -> Self{
  8.         Self{r:0,g:0,b:0}
  9.     }
  10. }
复制代码
等价于:
  1. #[derive(Default)]
  2. struct Color{
  3.     r:u8,
  4.     g:u8,
  5.     b:u8,
  6. }
复制代码
8.2 ToString

ToString 是 Rust 标准库中定义的一个非常常用的 trait,它的目的是将任何实现了它的类型转换为 String 类型的文本表示
  1. #[cfg_attr(not(test), rustc_diagnostic_item = "ToString")]
  2. #[stable(feature = "rust1", since = "1.0.0")]
  3. pub trait ToString {
  4.     /// Converts the given value to a `String`.
  5.     ///
  6.     /// # Examples
  7.     ///
  8.     /// ```
  9.     /// let i = 5;
  10.     /// let five = String::from("5");
  11.     ///
  12.     /// assert_eq!(five, i.to_string());
  13.     /// ```
  14.     #[rustc_conversion_suggestion]
  15.     #[stable(feature = "rust1", since = "1.0.0")]
  16.     #[cfg_attr(not(test), rustc_diagnostic_item = "to_string_method")]
  17.     fn to_string(&self) -> String;
  18. }
复制代码
自动实现

虽然 ToString 是一个 trait,但你几乎不需要手动实现它,因为标准库中已经为所有实现了 Display 的类型,自动实现了 ToString。
也就是说:
实现了 Display ⇒ 自动拥有 .to_string() 方法。
to_string() 本质上等价于 format!("{}", value)。
  1. #[cfg(not(no_global_oom_handling))]
  2. #[stable(feature = "rust1", since = "1.0.0")]
  3. impl<T: fmt::Display + ?Sized> ToString for T {
  4.     #[inline]
  5.     fn to_string(&self) -> String {
  6.         <Self as SpecToString>::spec_to_string(self)
  7.     }
  8. }
  9. impl<T: fmt::Display + ?Sized> SpecToString for T {
  10.     // A common guideline is to not inline generic functions. However,
  11.     // removing `#[inline]` from this method causes non-negligible regressions.
  12.     // See <https://github.com/rust-lang/rust/pull/74852>, the last attempt
  13.     // to try to remove it.
  14.     #[inline]
  15.     default fn spec_to_string(&self) -> String {
  16.         let mut buf = String::new();
  17.         let mut formatter =
  18.             core::fmt::Formatter::new(&mut buf, core::fmt::FormattingOptions::new());
  19.         // Bypass format_args!() to avoid write_str with zero-length strs
  20.         fmt::Display::fmt(self, &mut formatter)
  21.             .expect("a Display implementation returned an error unexpectedly");
  22.         buf
  23.     }
  24. }
复制代码
8.3 ParitialEq/Eq

在Rust中,PartialOrd、Ord、PartialEq和Eq是用于比较和排序的trait。通过使用derive宏,可以自动为结构体或枚举实现这些trait的默认行为。
下面是对这些trait的简要解释:

  • PartialOrd trait:用于部分顺序比较,即可以进行比较但不一定可以完全排序。它定义了partial_cmp方法,用于比较两个值并返回一个Option枚举,表示比较结果。
  • Ord trait:用于完全顺序比较,即可以进行完全排序。它是PartialOrd trait的超集,定义了cmp方法,用于比较两个值并返回Ordering枚举,表示比较结果。
  • PartialEq trait:用于部分相等性比较。它定义了eq、ne、lt、le、gt和ge等方法,用于比较两个值是否相等、不相等、小于、小于等于、大于、大于等于。
  • Eq trait:用于完全相等性比较,即可以进行完全相等性判断。它是PartialEq trait的超集,无需手动实现,通过自动实现PartialEq trait即可获得Eq trait的默认实现。
Eq定义为PartialEq的subtrait
  1. #[derive(PartialEq, Debug)]    // 注意这一句
  2. struct Point {
  3.     x: i32,
  4.     y: i32,
  5. }
  6. fn example_assert(p1: Point, p2: Point) {
  7.     assert_eq!(p1, p2);        // 比较
  8. }
复制代码
8.4 PartialOrd/Ord

PartialOrd和PartialEq差不多,PartialEq只判断相等或不相等,PartialOrd在这个基础上进一步判断是小于、小于等于、大于还是大于等于。可以看到,它就是为排序功能准备的。
PartialOrd被定义为 PartialEq的subtrait。它们在类型上可以用过程宏一起derive实现。
  1. #[derive(PartialEq, PartialOrd)]
  2. struct Point {
  3.     x: i32,
  4.     y: i32,
  5. }
  6. #[derive(PartialEq, PartialOrd)]
  7. enum Stoplight {
  8.     Red,
  9.     Yellow,
  10.     Green,
  11. }
复制代码
8.5 Clone

这个trait给目标类型提供了clone()方法用来完整地克隆实例。
  1. #[stable(feature = "rust1", since = "1.0.0")]
  2. #[lang = "clone"]
  3. #[rustc_diagnostic_item = "Clone"]
  4. #[rustc_trivial_field_reads]
  5. pub trait Clone: Sized {
  6.     /// Returns a copy of the value.
  7.     ///
  8.     /// # Examples
  9.     ///
  10.     /// ```
  11.     /// # #![allow(noop_method_call)]
  12.     /// let hello = "Hello"; // &str implements Clone
  13.     ///
  14.     /// assert_eq!("Hello", hello.clone());
  15.     /// ```
  16.     #[stable(feature = "rust1", since = "1.0.0")]
  17.     #[must_use = "cloning is often expensive and is not expected to have side effects"]
  18.     // Clone::clone is special because the compiler generates MIR to implement it for some types.
  19.     // See InstanceKind::CloneShim.
  20.     #[lang = "clone_fn"]
  21.     fn clone(&self) -> Self;
  22.     /// Performs copy-assignment from `source`.
  23.     ///
  24.     /// `a.clone_from(&b)` is equivalent to `a = b.clone()` in functionality,
  25.     /// but can be overridden to reuse the resources of `a` to avoid unnecessary
  26.     /// allocations.
  27.     #[inline]
  28.     #[stable(feature = "rust1", since = "1.0.0")]
  29.     fn clone_from(&mut self, source: &Self) {
  30.         *self = source.clone()
  31.     }
  32. }
复制代码
通过方法的签名,可以看到方法使用的是实例的不可变引用。
  1. fn clone(&self) -> Self;
复制代码
比如:
  1. #[derive(Clone)]
  2. struct Point {
  3.     x: u32,
  4.     y: u32,
  5. }
复制代码
因为每一个字段(u32类型)都实现了Clone,所以通过derive,自动为Point类型实现了Clone trait。实现后,Point的实例 point 使用 point.clone() 就可以把自己克隆一份了。
注意:clone() 是对象的深度拷贝,可能会有比较大的额外负载,但是就大多数情况来说其实还好。不要担心在Rust中使用clone(),先把程序功能跑通最重要。Rust的代码,性能一般都不会太差,毕竟起点很高。
8.6 Copy
  1. #[rustc_unsafe_specialization_marker]
  2. #[rustc_diagnostic_item = "Copy"]
  3. pub trait Copy: Clone {
  4.     // Empty.
  5. }
复制代码
定义为Clone的subtrait,并且不包含任何内容,仅仅是一个标记(marker)。
Rust标准库提供了Copy过程宏,可以让我们自动为目标类型实现Copy trait。
8.7 ToOwned

ToOwned相当于是Clone更宽泛的版本。ToOwned给类型提供了一个 to_owned() 方法,可以将引用转换为所有权实例。
  1. let a: &str = "123456";
  2. let s: String = a.to_owned();
复制代码
8.8 Drop

Drop trait用于给类型做自定义垃圾清理(回收)。
  1. trait Drop {
  2.     fn drop(&mut self);
  3. }
复制代码
实现了这个trait的类型的实例在走出作用域的时候,触发调用drop()方法,这个调用发生在这个实例被销毁之前。
  1. #[derive(PartialEq, Debug, Clone)]    // 注意这一句
  2. struct Point {
  3.     x: i32,
  4.     y: i32,
  5. }
  6. impl Drop for Point {
  7.     fn drop(&mut self) {
  8.         println!("Dropping point ({},{})",self.x,self.y);
  9.     }
  10. }
  11. fn main() {
  12.     let p = Point { x: 1, y: 2 };
  13.     println!("{:?}", p);
  14. }
复制代码
输出结果:
3.png

一般来说,我们不需要为自己的类型实现这个trait,除非遇到特殊情况,比如我们要调用外部的C库函数,然后在C那边分配了资源,由C库里的函数负责释放,这个时候我们就要在Rust的包装类型(对C库中类型的包装)上实现Drop,并调用那个C库中释放资源的函数。
8.9 From 和 Into

这两个 trait 用于类型转换。
From 可以把类型T转为自己,而 Into 可以把自己转为类型T。
  1. trait From<T> {
  2.     fn from(T) -> Self;
  3. }
  4. trait Into<T> {
  5.     fn into(self) -> T;
  6. }
复制代码
可以看到它们是互逆的trait。实际上,Rust只允许我们实现 From,因为实现了From后,自动就实现了Into,请看标准库里的这个实现。
  1. impl<T, U> Into<U> for T
  2. where
  3.     U: From<T>,
  4. {
  5.     fn into(self) -> U {
  6.         U::from(self)
  7.     }
  8. }
复制代码
8.10 TryFrom TryInto

TryFrom 和 TryInto 是 From 和 Into 的可失败版本。如果你认为转换可能会出现失败的情况,就选择这两个trait来实现。
  1. trait TryFrom<T> {
  2.     type Error;
  3.     fn try_from(value: T) -> Result<Self, Self::Error>;
  4. }
  5. trait TryInto<T> {
  6.     type Error;
  7.     fn try_into(self) -> Result<T, Self::Error>;
  8. }
复制代码
可以看到,调用 try_from() 和 try_into() 后返回的是Result,你需要对Result进行处理。
8.11 FromStr

从字符串类型转换到自身。
  1. trait FromStr {
  2.     type Err;
  3.     fn from_str(s: &str) -> Result<Self, Self::Err>;
  4. }
复制代码
比如字符串的 parse() 方法:
  1. use std::str::FromStr;
  2. fn example<T: FromStr>(s: &str) {
  3.     // 下面4种表达等价
  4.     let t: Result<T, _> = FromStr::from_str(s);
  5.     let t = T::from_str(s);
  6.     let t: Result<T, _> = s.parse();
  7.     let t = s.parse::<T>(); // 最常用的写法
  8. }
复制代码
8.12 as_ref
  1. trait AsRef<T> {
  2.     fn as_ref(&self) -> &T;
  3. }
复制代码
它把自身的引用转换成目标类型的引用。和Deref的区别是, deref() 是隐式调用的,而 as_ref() 需要你显式地调用。所以代码会更清晰,出错的机会也会更少。
AsRef 可以让函数参数中传入的类型更加多样化,不管是引用类型还是具有所有权的类型,都可以传递。比如;
  1. // 使用 &str 作为参数可以接收下面两种类型
  2. //  - &str
  3. //  - &String
  4. fn takes_str(s: &str) {
  5.     // use &str
  6. }
  7. // 使用 AsRef<str> 作为参数可以接受下面三种类型
  8. //  - &str
  9. //  - &String
  10. //  - String
  11. fn takes_asref_str<S: AsRef<str>>(s: S) {
  12.     let s: &str = s.as_ref();
  13.     // use &str
  14. }
  15. fn example(slice: &str, borrow: &String, owned: String) {
  16.     takes_str(slice);
  17.     takes_str(borrow);
  18.     takes_str(owned); // ❌
  19.     takes_asref_str(slice);
  20.     takes_asref_str(borrow);
  21.     takes_asref_str(owned); // ✅
  22. }
复制代码
在这个例子里,具有所有权的String字符串也可以直接传入参数中了,相对于 &str 的参数类型表达更加扩展了一步。
你可以把 Deref 看成是隐式化(或自动化)+弱化版本的 AsRef。

来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!
您需要登录后才可以回帖 登录 | 立即注册