Rust学习笔记#5:函数和trait

Stella981
• 阅读 737

Rust学习笔记#5:函数和trait

函数

基本语法

Rust的函数使用fn关键字开头,函数可以有一系列的输入参数,还有一个返回类型。函数返回可以使用return语句,可以使用表达式。下面是一个标准函数的示例,add函数接受两个i32的参数,然后计算它们的和并返回:

fn add(a: i32, b: i32) -> i32 {
    a + b
}
println!("{}", add(1, 2)); // 输出:3

函数返回值如果不显示标明,默认是()。函数返回值类型也可以是never类型!,这一类函数叫做发散函数,代表这个函数不能够正常返回,例如:

fn diverges() -> ! {
    panic!("This function never return!")
}

Rust编写的可执行程序的入口是fn main() -> ()函数。一般情况下,一个进程开始执行的时候可以接受一系列的参数,退出的时候也可以返回一个错误码,所以很多编程语言会为main函数设计参数和返回值类型,例如C语言中的int main(int argc, char **argv)。但是,Rust的main函数无参数也无返回值,其传递参数和返回状态码都通过单独的API来完成,示例如下:

fn main() {
    for arg in std::env::args() {
        println!("Arg: {}", arg);
    }
}

可以通过std::env::args()函数获取参数,通过exit()函数的参数传递错误码,通过std::env::var()读取环境变量。

函数递归与TCO

函数递归是我们常用的一种思维方式,Rust也支持函数递归调用。下面用经典的Fibonacci数列来举例:

fn fib(index: u32) -> u64 {
    match index {
        1 | 2 => 1,
        _ => fib(index - 1) + fib(index - 2),
    }
}

println!("{}", fib(25)); // 输出:75025

谈起递归,我们都会想到尾递归优化的概念(TCO,Tail Call Optimization)。TCO可以把尾递归在编译阶段转换为迭代循环从而降低时间和空间开销,但Rust并不支持TCO,某个RFC的作者给出了以下理由:

  • 可移植性问题,LLVM当时在某些指定架构上特别是MIPS和WebAssembly,不支持正确尾调用。
  • LLVM中正确尾调用实际上可能会由于它们当时的实现方式而造成性能损失。
  • TCO让调试变得更加困难,因为它重写了栈上的值。

方法

在一些编程语言中,函数和方法往往是对同一种东西的两种称呼,但在Rust中,它们是有明确区分的。方法和函数的语法完全相同:它们使用 fn 关键字和名称声明,可以拥有参数和返回值,同时包含在某处调用该方法时会执行的代码。但是,只有在结构体上下文中被定义的函数才能称为方法。

方法分为成员方法和静态方法,它们的区别在于第一个参数是否为self。Rust中的Selfself都是关键字,其中Self是类型名,self是变量名,self代表调用该方法的结构体实例,静态方法属于结构体类型所有,所以不需要self。常见的Selfself的组合有: self: Self(获得所有权)、self: &self(仅仅读取)、self: &mut self(做出修改),因为它们的使用频率很高,Rust也提供了相应的语法糖来简写:self&self&mut self

成员方法

可以使用impl为结构体实现成员方法,如下面的代码所示。将函数移到impl块中,并将第一个参数改成self,就把函数变成了成员方法。和其他编程语言类似,调用成员方法只需在结构体示例后加.即可。每个结构体可以拥有多个impl块。

struct Rectangle {
    width: u32,
    height: u32,
}

impl Rectangle {
    fn area(&self) -> u32 {
        self.width * self.height
    }
}

fn main() {
    let rect = Rectangle { width: 30, height: 50 };

    println!(
        "The area of the rectangle is {} square pixels.",
        rect.area()
    );
}

静态方法

impl块中定义的不以self作为参数的方法就是静态方法,见下面的代码。可以使用结构体名和::运算符来调用静态方法,例如let sq = Rectangle::square(3);来获得一个大小为3的正方形。

// 接上面
impl Rectangle {
    fn square(size: u32) -> Rectangle {
        Rectangle { width: size, height: size }
    }
}

trait

基本语法

trait的功能类似于Java中的接口,trait可以翻译为“特性”,它可以为多种类型抽象出共同拥有的一些功能。一个类型的行为由其可供调用的方法构成。如果可以对不同类型调用相同的方法的话,这些类型就可以共享相同的行为了。trait 定义将方法签名组合起来,目的是定义一个实现某些目的所必需的行为的集合。这些方法既可以是成员方法,也可以是静态方法。trait可以用关键字trait来声明,例如:

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

上面的代码声明了一个名为Shape的trait,它有一个名为area的方法,即,若某个类型拥有Shape这个特性,那么它一定可以求面积。为某个类型实现trait使用impl...for关键字,例如:

struct Rectangle {
    width: u32,
    height: u32,
}

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

impl Shape for Rectangle {
    fn area(&self) -> u32 {
        self.width * self.height
    }

    fn hello() {
        println!("Just say hello!")
    }
}

fn main() {
    let rect = Rectangle {
        width: 30,
        height: 50,
    };

    println!(
        "The area of the rectangle is {} square pixels.",
        rect.area()
    );

    Rectangle::hello();
}

trait中的方法也可以有默认实现,那么在针对具体类型实现的时候,就可以不用重写。我们也可以利用trait为其他类型扩展方法,哪怕这个类型不是我们自己写的。例如,可以为内置类型i32添加一个方法:

trait Double {
    fn double(&self) -> i32;
}

impl Double for i32 {
    fn double(&self) -> i32 {
        *self * 2
    }
}

fn main() {
    let i: i32 = 5;
    println!("{}", i.double()); // 输出:10
}

孤儿规则

使用trait为类型扩展方法也要受到一定的限制,在声明trait和impl trait的时候,Rust规定了一个一致性规则,也称为孤儿规则:impl块要么与trait的声明在同一个crate中,要么与类型的声明在同一个crate中。也就是说,如果trait和类型都来自于外部的crate,那么便不允许为这个类型实现该trait。这是因为,该类型没有实现该trait,这可能是该类型作者有意的设计,强行实现可能会导致bug。

trait和接口的区别

之前我们说trait和接口在功能上类似,但它们在使用中是有区别的。trait本身不是具体类型,也不是指针类型,它只是定义了针对类型的抽象的约束,不同的类型可以实现同一个trait,而这些类型可能具有不同的大小,因此trait在编译阶段没有固定大小,所以,Rust中不能直接使用trait作为实例变量、参数和返回值,这一点和接口的习惯用法是不同的。例如,下面的代码就是编译错误的:

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

// error[E0277]: the size for values of type `(dyn Shape + 'static)` cannot be known at compilation time
fn use_shape(arg: Shape) {} 

trait继承

trait允许继承,例如:

trait Base{}
trait Derived: Base {}

这表示Derived继承了Base,它意味着,满足Derived的类型,必然也满足Base,所以,在针对一个具体类型impl Derived的时候,编译器也会要求同时impl Base,否则会报编译错误:

trait Base {}
trait Derived: Base {}
struct T;
// error[E0277]: the trait bound `T: Base` is not satisfied
impl Derived for T {}

fn main() {}

常见trait简介

标准库中有很多常见且很有用的trait,我们一起学习一下。

Display和Debug

DisplayDebug的定义如下:

pub trait Display {
    fn fmt(&self, f: &mut Formatter<'_>) -> Result;
}
pub trait Debug {
    fn fmt(&self, f: &mut Formatter<'_>) -> Result;
}

这两个trait主要用在类似println!这样进行输出的地方。只有实现了Display的类型,才能用{}格式打印出来;只有实现了Debug的类型,才能用{:?}{:#?}格式打印出来。它们之间的更多区别如下:

  • Display假定了这个类型可以用utf-8格式的字符串表示,它是准备给最终用户看的,并不是所有的类型都应该实现这个trait。标准库中还有一个常用的trait叫作std::string::ToString,对于所有实现Display的类型, 都自动实现了这个ToStringtrait,它包含了一个to_string(&self)->String方法。
  • Debug主要是为了调试使用,建议所有的作为API的公开类型都应当实现这个trait,以方便调试。

PartialOrd/Ord/PartialEq/Eq

我们首先介绍一下全序和偏序的概念。对于集合X中的元素a,b,c

  • 如果a < b则一定有!(a > b),称为反对称性;
  • 如果a < bb < c则一定有a < c,称为传递性;
  • 对于X中的所有元素,都存在a < ba > b或者a == b,三者必居其一,称为完全性。

如果集合中的元素只具备上述前两条特征,则称X是偏序;同时具备以上所有特征,则称X是全序。

Rust设计了两个trait来对全序和偏序进行描述,PartialOrd代表偏序,Ord代表全序。只有满足全序的类型才可以进行排序,像浮点数这样的偏序类型就无法排序。同理,PartialEq用来描述只能部分元素进行相等比较,Eq表示全部元素都可以进行相等比较。这样的设计可以让我们在更早的阶段发现错误。

Sized

这个trait表示类型是否有大小,它定义在std::marker模块中,它没有任何的成员方法,它与普通trait不同,编译器对它有特殊处理,用户也不能针对自己的类型实现这个trait。一个类型是否满足Sized约束完全是由编译器推导的,用户无权指定。

Default

Rust中没有C++中构造函数的概念,因为相比普通函数,构造函数本身并没有提供额外的抽象能力,反倒增加了语法上的负担,因此,Rust推荐使用普通的静态函数作为类型的构造器,例如String::new()。对于那种无参数无错误处理的简单情况,标准库提供了Default来做统一抽象,其定义如下:

pub trait Default: Sized {
    fn default() -> Self;
}

属性与derive

Rust中有一种语法,属性(attribute),基本格式类似于#[xxx]。属性可以用来注释声明,类似于Java中的注解。有一种非常实用的属性derive,可以帮助我们自动impl某些trait,因为实现某些trait的时候,逻辑是非常机械化的,例如Debug。示例如下:

#[derive(Debug)]
struct Rectangle {
    width: u32,
    height: u32,
}

fn main() {
    let rect = Rectangle {
        width: 10,
        height: 20,
    };
    println!("{:?}", rect); // 输出:Rectangle { width: 10, height: 20 }
}

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

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

参考文献

  • 《Rust编程之道》张汉东
  • 《深入浅出Rust》范长春
点赞
收藏
评论区
推荐文章
blmius blmius
3年前
MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1
文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s
皕杰报表之UUID
​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为
待兔 待兔
4个月前
手写Java HashMap源码
HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程22
Jacquelyn38 Jacquelyn38
3年前
2020年前端实用代码段,为你的工作保驾护航
有空的时候,自己总结了几个代码段,在开发中也经常使用,谢谢。1、使用解构获取json数据let jsonData  id: 1,status: "OK",data: 'a', 'b';let  id, status, data: number   jsonData;console.log(id, status, number )
Wesley13 Wesley13
3年前
mysql中时间比较的实现
MySql中时间比较的实现unix\_timestamp()unix\_timestamp函数可以接受一个参数,也可以不使用参数。它的返回值是一个无符号的整数。不使用参数,它返回自1970年1月1日0时0分0秒到现在所经过的秒数,如果使用参数,参数的类型为时间类型或者时间类型的字符串表示,则是从1970010100:00:0
Stella981 Stella981
3年前
JS 对象数组Array 根据对象object key的值排序sort,很风骚哦
有个js对象数组varary\{id:1,name:"b"},{id:2,name:"b"}\需求是根据name或者id的值来排序,这里有个风骚的函数函数定义:function keysrt(key,desc) {  return function(a,b){    return desc ? ~~(ak
Stella981 Stella981
3年前
HIVE 时间操作函数
日期函数UNIX时间戳转日期函数: from\_unixtime语法:   from\_unixtime(bigint unixtime\, string format\)返回值: string说明: 转化UNIX时间戳(从19700101 00:00:00 UTC到指定时间的秒数)到当前时区的时间格式举例:hive   selec
Wesley13 Wesley13
3年前
PHP中的NOW()函数
是否有一个PHP函数以与MySQL函数NOW()相同的格式返回日期和时间?我知道如何使用date()做到这一点,但是我问是否有一个仅用于此的函数。例如,返回:2009120100:00:001楼使用此功能:functiongetDatetimeNow(){
Wesley13 Wesley13
3年前
MySQL部分从库上面因为大量的临时表tmp_table造成慢查询
背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_
Python进阶者 Python进阶者
10个月前
Excel中这日期老是出来00:00:00,怎么用Pandas把这个去除
大家好,我是皮皮。一、前言前几天在Python白银交流群【上海新年人】问了一个Pandas数据筛选的问题。问题如下:这日期老是出来00:00:00,怎么把这个去除。二、实现过程后来【论草莓如何成为冻干莓】给了一个思路和代码如下:pd.toexcel之前把这