欢迎光临散文网 会员登陆 & 注册

Rust 从入门到精通08-trait

2022-10-26 23:32 作者:程序员_可乐  | 我要投稿

Rust 语言中,trait 是一个非常重要的概念,可以包含:函数、常量、类型等。

通俗一点理解,trait 以一种抽象的方式定义共享的行为,可以被认为是一些语言的接口,但是与接口也有一定区别,下面会介绍。

1、成员方法

trait 中可以定义方法。

trait Shape {
    fn area(&self) -> f64;
}

我们在一个名为 Shape 的 trait 中定义了一个方法 area。

1.1 方法参数

看上面定义的 Shape,方法的参数是 &self。其实对于每个 trait 都有一个隐藏的类型 Self(大写的 S),代表实现此 trait 的具体类型。Rust 中 Self 和 self 都是关键字,大写的Self是类型名,小写的 self 是变量名。其实 area(&self) 等价于 area(self : &Self),只不过 rust 提供了简化的写法。下面几种情况都是等价的。

trait T {
    fn method1(self : Self);
    fn method2(self : &Self);
    fn method3(self : &mut Self);
}
//等价于下面方法定义
trait T {
    fn method1(self);
    fn method2(&self);
    fn method3(&mut self);
}

1.2 调用实例

可以参考如下例子:

trait Shape {
    fn area(&self) -> f64;
}

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

impl Circle {
    fn get_radius(&self) -> f64 {
        self.radius
    }
}

impl 关键字后面直接接类型,没有 trait 的名字。

可以将上面代码看成是为 Circle 实现了一个匿名的 trait。

3、 静态方法

静态方法:第一个参数不是 self 参数的方法。

impl Circle {
    // 普通方法
    fn get_radius(&self) -> f64 {
        self.radius
    }

    // 静态方法
    fn get_area(this : &Self) ->f64 {
        std::f64::consts::PI * this.radius * this.radius
    }
}

fn main() {
    let c = Circle{ radius : 2f64};
    // 调用普通方法
    println!("The radius is {}",c.radius);
    // 调用静态方法
    println!("The area is {}",Circle::get_area(&c))

}

注意和普通方法的区别,参数命名不同,以及调用方式不同(普通方法是小数 实例.方法 ,静态方法是 类型::方法 )。

静态方法的调用可以 Type::FunctionName()。

4、扩展方法

利用 trait 给其它类型添加方法。

比如我们给内置类型 i32 添加一个方法:

// 扩展方法
trait Double {
    fn double(&self) -> Self;
}
impl Double for i32 {
    fn double(&self) -> i32{
        self * 2
    }
}

fn main() {
    let x : i32 = 10.double();
    println!("x double is {}",x);//20

}

5、泛型约束

5.1 静态分发

在编译期通过单态化分别生成具体类型的实例,所以调用 trait 限定中的方法也都是运行时零成本的,因为不需要在运行时再 进行方法查找 。

fn main() {
    fn myPrint<T: ToString>(v: T) {
        v.to_string();
    }
    
    let c = 'a';
    let s = String::from("hello");
    
    myPrint::<char>(c);
    myPrint::<String>(s);
}

等价于:

fn myPrint(c:char){
    c.to_string();
}
fn myPrint(str:String){
    str.to_string();
}

5.2 动态分发

在运行时查找相应类型的方法 , 会带来一定的运行时开销。

fn dyn_Print(t:&ToString){
        t.to_string();
}
let c = 'a';
let s = String::from("hello");
    
dyn_Print(&c);
dyn_Print(&s)

5、一致性原则

一致性原则,也称为孤儿原则:

Impl 块要么与 trait 块的声明在同一个 crate 中,要么与类型的声明在同一个 crate 中。

也就是说如果 trait 来自外部,而且类型也来自外部 crate,编译器是不允许你为这个类型 impl 这个 trait。它们当中至少有一个是在当前 crate 中定义的。

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

6、trait 和 接口区别

开篇我们说为了便于理解 trait,可以想象为其它语言,比如Java中的接口。但是实际上他们还是有很大的区别的。

因为 rust 是一种用户可以对内存有着精确控制的强类型语言。在目前 Rust 版本中规定:

函数传参类型,返回值类型等都是要在编译期确定大小的。

而 trait 本身既不是具体类型,也不是指针类型,它只是定义了针对类型的、抽象的约束。不同的类型可以实现同一个 trait,满足同一个 trait 的类型可能具有不同的大小。

所以 trait 在编译阶段没有固定的大小,我们不能直接使用 trait 作为实例变量、参数以及返回值。

类似下面的写法都是错误的:

trait Shape {
    fn area(&self) -> f64;
}

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();
}

可以看到编译器的错误提示:

7、derive

Rust 标准库内部实现了一些逻辑较为固定的 trait,通过 derive 配置可以帮助我们自动 impl 某些 trait。

#[derive(Debug)]
struct Foo {
    data : i32,
}
fn main() {
    let v1 = Foo{data : 0};
    println!("{:?}",v1)
}

加上 Debug 的trait 实现,便于格式化打印 struct。

#[derive(Debug)] 等价于 impl Debug for Foo {}

目前,Rust 支持的可以自动 derive 的 trait 有如下:

Copy,Clone,Default,Hash,Debug,PartialEq,Eq,PartialOrd,Ord,RustcEncodable,RustcDecodable,FromPrimitive,Send,Sync

8、标准库中常见 trait

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

8.1 Display 和 Debug

可以分别看下源码定义:

【Display】

pub trait Display {
    /// Formats the value using the given formatter.
    ///
    /// # Examples
    ///
    /// ```
    /// use std::fmt;
    ///
    /// struct Position {
    ///     longitude: f32,
    ///     latitude: f32,
    /// }
    ///
    /// impl fmt::Display for Position {
    ///     fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
    ///         write!(f, "({}, {})", self.longitude, self.latitude)
    ///     }
    /// }
    ///
    /// assert_eq!("(1.987, 2.983)",
    ///            format!("{}", Position { longitude: 1.987, latitude: 2.983, }));
    /// ```
    #[stable(feature = "rust1", since = "1.0.0")]
    fn fmt(&self, f: &mut Formatter<'_>) -> Result;
}

【Debug】

pub trait Debug {
    /// Formats the value using the given formatter.
    ///
    /// # Examples
    ///
    /// ```
    /// use std::fmt;
    ///
    /// struct Position {
    ///     longitude: f32,
    ///     latitude: f32,
    /// }
    ///
    /// impl fmt::Debug for Position {
    ///     fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
    ///         f.debug_tuple("")
    ///          .field(&self.longitude)
    ///          .field(&self.latitude)
    ///          .finish()
    ///     }
    /// }
    ///
    /// let position = Position { longitude: 1.987, latitude: 2.983 };
    /// assert_eq!(format!("{:?}", position), "(1.987, 2.983)");
    ///
    /// assert_eq!(format!("{:#?}", position), "(
    ///     1.987,
    ///     2.983,
    /// )");
    /// ```
    #[stable(feature = "rust1", since = "1.0.0")]
    fn fmt(&self, f: &mut Formatter<'_>) -> Result;
}

①、只有实现了 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 的功能。


Rust 从入门到精通08-trait的评论 (共 条)

分享到微博请遵守国家法律