专栏首页Rust语言学习交流Rust学习:如何解读函数签名?

Rust学习:如何解读函数签名?

Rust中,函数签名类似“讲故事”。经验丰富的Rust程序员,只需浏览一个函数的签名,就可以知道该函数大部分的行为。

在本文中,我们将探讨一些函数签名,并讨论如何读它们并从中提取信息。在探索的同时,你可以在 Rust API 文档中找到许多出色的函数签名示例。你也可以在 练习场 实践。

“婴儿起步”

你在Rust中的定义的第一个函数,几乎是这样的:

fn main() {}

那我们就从这里开始吧!

  • fn:是告诉Rust,我们声明一个函数的语法。
  • main:是函数的名词。只是main是特殊的,它是在构建和运行二进制程序时调用的。函数名称总是蛇形命名snake case,而不是驼峰命名camel case
  • ():是参数列表。示例表示,main不接受任何参数。
  • {}:是函数的分隔符。示例表示,函数体是空的。

可见性

默认情况下,所有函数都是私有的,不能在其所在的模块之外使用它们。但使它们可以由不同模块使用,是件简单的事。

mod dog {
    fn private_function() {}
    pub fn public_function() {}
}

// 可选的,避免使用foo::
use dog::public_function;

fn main() {
    dog::public_function();
    // 如果使用use方式,就可以这样调用
    public_function();
}

就像可变性一样,Rust在可见性上的假定也是保守的。如果你尝试使用私有函数,编译器将让你知道并帮助你,它会指出哪个函数需要设置为public

如果你的项目中有一个像foo::bar::baz::rad()这样的函数,并希望可以用foo::rad()来使用它,请添加pub use bar::baz::rad;到你的foo模块。这称为重新导出。

简单的参数

不再满意do_nothing_useful()函数,你决定收养一只狗。祝你好运!现在你遇到了一个新问题,你必须遛它并陪它玩!

fn walk_dog(dog_name: String) {}
fn play_with(dog_name: String, game_name: String) {}

参数声明,变量名: 类型,多个参数以逗号分隔。但继续,我们的狗不是一个字符串!好消息,你也可以使用自己的类型。

struct Dog;  // 简单起见,我们用空结构体
struct Game;

fn walk_dog(dog: Dog) {}
fn play_with(dog: Dog, game: Game) {}

很好,看起来已经比较好了。让我们开始那美好的一天吧。

fn main() {
    let rover = Dog;
    walk_dog(rover);

    let fetch = Game;
    play_with(rover, fetch); // 编译错误!
}

哇哇!这是一个完美的好日子,编译器完全毁了我们!Rover将会非常难过。

我们来看看错误:

error[E0382]: use of moved value: `rover`
  --> src/main.rs:12:15
   |
8  |     let rover = Dog;
   |         ----- move occurs because `rover` has type `Dog`, which does not implement the `Copy` trait
9  |     walk_dog(rover);
   |              ----- value moved here
...
12 |     play_with(rover, fetch);
   |               ^^^^^ value used here after move

编译器告诉我们,当我们将rover传递给walk_dog()时,它的所有权被转移了。这是因为fn walk_dog(dog: Dog){}接受Dog值时,我们没有告诉编译器它们是可复制的!传递参数给函数时,可复制的值会被隐式复制。你可以通过在类型的声明上方添加#[derive(Copy)]来实现可复制。

我们要保持狗不可复制,因为,天哪,你不能复制狗。那么我们如何解决这个问题呢?

我们可以克隆rover。但我们的Dog结构体也不是Clone的!克隆意味着我们可以明确地制作一个对象的副本。你可以像复制一样实现克隆。要克隆我们的狗,你可以rover.clone()

但实际上,这些可能的解决方案都没有解决真正的问题:我们想和同一只狗一起走路和玩耍!

借用

我可以借你的狗吗?

代替将我们的Dog移动到walk_dog()函数中,我们只想借用我们的Dog到函数中。当你遛狗时,通常狗最终会和你一起回到家里,对吧?

Rust使用来表示借用。借用某个值告诉编译器,当函数调用完后,值的所有权将返回给调用者。

fn walk_dog(dog: &Dog) {}
fn play_with(dog: &Dog, game: Game) {}

有不可变借用以及可变借词(&mut)。你可以将一个不可变借用传递给任意数量的对象,而可变借用一次只能传递给一个对象。这确保了数据的安全性。

所以我们新的借用功能并没有真正解决问题,不是吗?我们甚至不能改变狗!让我们试着看看错误信息。

error[E0594]: cannot assign to `dog.walked` which is behind a `&` reference
 --> src/main.rs:6:5
  |
5 | fn walk_dog(dog: &Dog) {
  |                  ---- help: consider changing this to be a mutable reference: `&mut Dog`
6 |     dog.walked = true;
  |     ^^^^^^^^^^^^^^^^^ `dog` is a `&` reference, so the data it refers to cannot be written

将函数签名更改为fn walk_dog(dog: &mut Dog){},并更新我们的main()函数,我们可以解决这个问题。

fn main() {
    let mut rover = Dog { walked: false };
    walk_dog(&mut rover);
    assert_eq!(rover.walked, true);
}

正如你所看到的,函数签名告诉程序员一个值是否可变以及该值是否已被使用或引用。

返回值

让我们重新审视我们如何获得Rover,这是我们探索如何返回类型!假设我们想要一个函数adopt_dog(),它接收一个名字,并返回给我们一只Dog

struct Dog {
    name: String,
    walked: bool,
}

fn adopt_dog(name: String) -> Dog {
    Dog { name: name, walked: false }
}

fn main() {
    let rover = adopt_dog(String::from("Rover"));
    assert_eq!(rover.name, "Rover");
}

所以函数签名中的-> Dog部分告诉我们函数返回一个Dog。请注意,名称name将转移并赋值给Dog,而不是复制或克隆。

内置trait

如果你在trait中实现函数,你可以访问以下两个“元素”:

  • Self,类型,表示当前类型。
  • self,参数,指定结构体实例的借用/移动/可变性。

在下面的walk()中,我们采取可变借用,self移动值。一个例子:

// 沿用上面的Dog结构体
impl Dog {
    pub fn adopt(name: String) -> Self {
        Dog { name: name, walked: false }
    }
    pub fn walk(&mut self) {
        self.walked = true
    }
}

fn main() {
    let mut rover = Dog::adopt(String::from("Rover"));
    assert_eq!(rover.name, "Rover");
    rover.walk();
    assert_eq!(rover.walked, true);
}

泛型

在我们现实生活中,会有很多不同种类的狗!还有很多类型的动物!其中一些我们可能也想遛,比如我们的熊。

泛型可以让我们这样做。我们可以有实现Walk特性的DogBear结构体,然后让walk_pet()函数接受任何具有Walk特性的结构体!

在函数名称和参数列表之间,可以使用尖括号指定泛型的名称。关于泛型的重要注意事项是,当你接受泛型参数时,你只能使用函数中约束的类型。这意味着如果将Read传递给想要Write的函数,除非约束包含它,否则它仍然无法读入Read

struct Dog { walked: bool, }
struct Bear { walked: bool, }

trait Walk {
    fn walk(&mut self);
}
impl Walk for Dog {
    fn walk(&mut self) {
        self.walked = true
    }
}
impl Walk for Bear {
    fn walk(&mut self) {
        self.walked = true
    }
}

fn walk_pet<W: Walk>(pet: &mut W) {
    pet.walk();
}

fn walk_pet_2(pet: &mut Walk) {
    pet.walk();
}

fn main() {
    let mut rover = Dog { walked: false, };
    walk_pet(&mut rover);
    assert_eq!(rover.walked, true);
}

你还可以使用不同的方式,where语法来指定泛型,因为复杂泛型的函数签名可能会变得相当长。

fn walk_pet<W>(pet: &mut W)
where W: Walk {
    pet.walk();
}

如果有多个泛型,你可以用逗号分隔它们。如果你有多个特性约束,你可以使用加号组合它们。

fn stuff<R, W>(r: &R, w: &mut W)
where W: Write, R: Read + Clone {}

看看你可以从该函数签名中获得的所有信息!虽然函数名没有帮助性信息,但你还是几乎可以确定它的作用!

还有一些称为关联类型的内容,它们用于像Iterator这样的事物。当书写函数签名时,你想使用像Iterator<Item = Dog>这样的语句来表明一个Dog的迭代器。

传递函数

有时需要将函数传递给其他函数。在Rust中,接受函数作为参数是相当简单的。函数具有特征,它们像泛型一样传递!

在这种情况下,你应该使用where语法。

struct Dog {
    walked: bool
}

fn do_with<F>(dog: &mut Dog, action: F)
where F: Fn(&mut Dog) {
    action(dog);
}

fn walk(dog: &mut Dog) {
    dog.walked = true;
}

fn main() {
    let mut rover = Dog { walked: false, };
    // 函数
    do_with(&mut rover, walk);
    // 闭包Closure
    do_with(&mut rover, |dog| dog.walked = true);
}

Rust中的函数实现特性,编译器会检测它们是如何传递的:

  • FnOnce - 采用按值(T)方式接受。
  • FnMut - 采用可变引用(&mut T)方式接受。
  • Fn - 采用不可变引用(&T)方式接受。

闭包|...| ...将自动实现(在满足使用需求的前提下)尽量以限制最多的方式捕获。

  • 所有闭包实现FnOnce:如果闭包仅实现FnOnce,则只能调用一次。
  • 不转移捕获变量所有权的闭包实现FnMut,允许多次调用它们。
  • 不需要对其捕获变量唯一/可变访问的闭包实现Fn,允许它们在任何地方被调用。

生命周期Lifetimes

你现在可能自我感觉良好。我的意思是,看看那个滚动条,它几乎到了页面的底部!你很快就会成为Rust函数签名大师!

让我们谈谈一些有关生命周期的话题,因为你最终会遇到它们,并且可能会变得很困惑。

让我在这里诚实地对你说。生命周期对我来说是一种神秘的艺术。我在Rust 0.7-0.10使用了它们,之后我就没使用它们了。如果你真的知道任何关于它们的事情,你就比我更有资格写这个部分了。

现代Rust有一个非常强大和有效的生命周期,它去掉了我们过去需要关注的绝大多数生命周期的“体力活”。

所以,如果你开始处理很多生命周期,你的第一步应该是坐下来思考它。除非你的代码非常复杂,否则你很可能不需要处理生命周期。如果你在一个简单的例子中碰到生命周期,你的问题可能是不正确的。

这是一个Option实现的具有生命周期的函数。

as_slice<'a> (&'a self) -> &'a [T]

生命周期用'表示,并给出一个名称。这里是'a。但如果你更喜欢开笑话,它们也可以是'burrito。基本上这个函数签名是说:调用Option<T>的生命周期与返回的[T]的生命周期相同。

挑战时间

下面,你将看到从标准库中提取的一组函数以及指向其文档的链接。你能从他们的函数签名中看出他们做了什么吗?为了增加乐趣,我删除了函数名!

fn name<P: AsRef<Path>>(path: P) -> Result<File>

fn name<E, T>(self, err: E) -> Result<T, E>

// In `Iterator<Item=T>`
fn name<B: FromIterator<Self::Item>>(self) -> B
where Self: Sized

fn name<B, F>(self, init: B, f: F) -> B
where Self: Sized, F: FnMut(B, Self::Item) -> B

fn name<F, O: FnOnce(E) -> F>(self, op: O) -> Result<T, F>

我希望这是奇妙的,我在这里为你欢呼!

本文翻译自“Andrew Hobden - Reading Rust Function Signatures”,点击「阅读原文」查看。

本文分享自微信公众号 - Rust语言学习交流(rust-china),作者:知行之录

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2019-08-31

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • 【Rust日报】 2019-06-21:Typestate 模式

    MikeLoveRust
  • 【Rust投稿】从零实现消息中间件(1)

    消息中间件在现代系统中非常关键,包括阿里云,腾讯云都有直接的消息中间件服务,也就是你不用自己搭建服务器,直接使用它提供的服务就可以了.那么我们今天就从零开始一步...

    MikeLoveRust
  • 【Rust每周一库】Rocket - 流行的网络开发框架

    Rocket是一个基于Rust编写的上层网络框架,是目前rust主流的网络框架之一,有8.8k的star。而它的http部分就是基于之前提到的hyper。按官方...

    MikeLoveRust
  • 如何在CentOS 7上安装和使用PostgreSQL

    关系数据库管理系统是许多网站和应用程序的关键组件。它们提供了一种存储,组织和访问信息的结构化方法。

    八十岁的背影
  • Docker安装mysql8主从结构 顶

    因为我用的镜像是docker.io/cytopia/mysql-8.0,所以我们需要先把该镜像给pull下来。

    算法之名
  • Python 当前时间增加或减少一个月

    今天在之前的代码中发现了一个bug,有个计算当前时间减少一个月的函数,其报出下面的异常信息:

    用户2398817
  • 【干货来了】!Oracle及普通软件卸载详解!

    电脑用的久了,里面的软件安装也会越来越多,但总有一些软件在使用过程中,甚至我们安装的过程中出现或多或少的问题,导致我们总是装了卸,卸了装(老实说,有时候纠结症都...

    Java学习
  • 苹果手机短信删除了怎么恢复?简洁又好用

      苹果手机短信删除了怎么恢复?现在觉得使用短信互发消息实在是太low了,如今的社交软件数不胜数,谁还会想到这么古老的方法呢?这些都是一些80后使用的,不过使用...

    科技第六人
  • 解决无法安装SQL Server 2008 Management Studio Express的问题

    我的sql server 2008 express是visual studio 2010自带的,所以当然它没有management studio ,自己下了一个...

    williamwong
  • VBA/VB6/VB.NET 采用金山词霸在线翻译函数(自动识别语言种类)

    巴西_prince

扫码关注云+社区

领取腾讯云代金券