函数是Rust的顶级成员,也是实现程序逻辑复用的主要工具之一。因为Rust中所有权和生命期机制的存在,使得Rust中的函数在编写的时候,其参数和返回值的类型和标注变得时而简单时而复杂。简单的时候,Rust中的函数与其他语言中的函数无异,但是复杂的时候,繁琐的包装类型和生命期标注能让人看的眼花缭乱。本文尝试从日常编程中取得的经验出发,记录处理Rust函数和返回值的一些经验。
以下经验会不定期更新,而且并不保证一定正确,它们大部分时间只是看起来正确而且可以通过编译并获得正确的运行结果。
函数的参数
Rust中函数分为两种,一种是独立函数,不依附于任何结构体、枚举存在;一种是方法函数,一般作为结构体、枚举类型的成员存在,通常都会定义在impl
语句块里。
独立函数的参数类型确定
独立函数所接受的参数类型通常都是与函数的行为息息相关的。根据函数可能存在的不同行为可以按照以下集中情况进行选择。不过以下函数参数类型的选择同样也适用于方法函数中的普通参数。
参数可以被消耗掉
可以被消耗掉的参数是指在调用函数的环境中已经不再需要传入函数的实参,或者已经做好了不再需要传入函数的实参的准备。这种情况下不需要判断函数是否是运行在多线程的条件下,因为可以被消耗掉的实参可以使用转移所有权的方式传入函数,此时对于函数所使用的实参来说是不存在争抢的问题的。
能够将所有权转移进入函数的参数类型主要有以下这些。
- 如
i8
、usize
、bool
等基本类型,基本类型在传入任何函数的时候都是采用复制后转移副本所有权的方式,所以可以安全接受所有权转入。 - 实现了
Copy
特征的类型,这些类型在转移所有权的时候也是采用复制后转移副本所有权的方式。 - 实现了
Clone
特征的类型,在传入函数的时候可以通过克隆自身来转移副本的所有权。但是需要注意的是,克隆操作本身就是比较浪费性能的,如果没有需要消耗传入实参的要求,可以采用传入引用的方式代替以提升性能。 - 实现了
Into<T>
特征的类型,这些类型在被传入函数的时候可以自动进行类型转换,实参会在转换的过程中被消耗掉。 - 函数需要接受字符串类型,如果接受的是字符串字面量或者仅仅是使用字符串内容,那么可以直接使用
&str
类型,否则需要使用String
类型。
&str
与String
的主要区别在于&str
是不可变的,String
是可以修改的。所以如果函数不需要修改字符串可以直接传入&str
。
实参是与其他函数共享的
函数不能取得实参所有权的情况也是非常常见的,对于大部分的自定义类型和复杂类型、内部带有引用的结构体等,一般都是需要通过引用来将其传入到函数中的。共享使用的资源在程序中从来都是控制起来最复杂的内容,不过其所需要考虑的最核心的问题是资源的抢占问题。
对于单线程程序来说,资源的共享使用是比较简单的,这里可以使用一个原则来约束:共享读,单一写。所以如果函数或者方法不需要写入实参,那么是可以放心使用引用来传递实参的。
- 如果函数接受的是基本类型,可以不使用引用,具体原因可参考前面一节。
- 可以使用
AsRef<T>
的形式来代替&
的显式引用形式,并且使用此形式可以是函数同时接受实例和引用两种形式的实参,并在函数中统一以引用形式来使用。 - 如果不是为了避免内存复制而提升性能需要,尽量不要选择传入引用。
- 如果需要在许多位置共享资源,可以考虑将资源使用
Rc<T>
封装,并将引用副本传入函数(使用Rc::clone()
作为实参,不会自动克隆的)。 - 如果是在闭包中返回引用,可以借助HRTBs的语法,使用
for<'a>
进行标注,不过尽量不要采用这种形式,也不要在序列中保存引用,要尽量让序列持有其中元素的所有权。 - 闭包会自动捕获上下文中的实例,如果只是在闭包中使用其值,尽量在闭包中使用引用的形式,以防止所有权被移入闭包。
在多线程的情况下,资源的共享就要复杂很多的,不过这里也可以借用一个类似于段子的原则来约束:先加锁。
- 如果函数接受的是基本类型,依旧可以不使用引用。
- 需要对引用内容使用
Mutex<T>
等线程安全的结构包裹,并且在使用资源之前先进行加锁操作,而且加锁的范围要尽可能的小,以避免死锁的出现。 - 只有实现了
Sync
特征的类型才能直接将其引用传入函数。 - 如果需要在许多位置共享资源,可以考虑将资源使用
Arc<T>
封装,使用时情况类似于Rc
。
无法确定实参的所有权
函数是否可以拥有实参的所有权在大部分情况下并不是可以被分析的那么清晰的,尤其是在多个函数嵌套调用的情况下。此时传入函数的实参有可能来自外部函数的参数,所以无法确定函数是否能够拥有其所有权。在这种情况下,可以将参数使用std::borrow::Cow
包裹,Cow<T>
写时复制所实现的大量From<T>
特征方法可以自动判别是否拥有传入参数的所有权,并调用相应的方法产生供函数使用的内容。
Cow
的时候需要注意它的定义签名为Cow<'a, B>
,所以在使用的时候需要为其显式指定函数的生命期标注。
函数需要修改实参内容
原则上来说,如果需要修改实参的内容是需要向函数中传入&mut
可变引用的。但是由于Rust中对于可变引用的限制,所以不加节制的使用可变引用很快就会获得编译错误。对于需要修改实参内容的函数,通常可以采用以下方法。
- 通过返回实参类型的新实例来代替对原有实参本身的修改,这也是标准库中大部分功能类型的做法。
- 使用
AsMut<T>
特征约束来代替&mut
形式的可变引用,来使函数可以同时接受实例和引用两种形式的实参。
方法函数的参数类型确定
方法函数的参数类型与独立函数不同的只有一个位置,方法函数的第一个参数可以是所依附的对象本身,通常都是使用self
来指代,根据方法函数是否需要对所依附的对象本身进行操作,这个参数可以有self
、&self
和&mut self
三种形式。
- 如果方法函数只是使用所依附的对象中持有的内容,但并不需要消耗掉所依附的对象,那么可以使用
&self
。 - 如果方法函数需要对所依附的对象进行变换,并且在变换以后之前的对象就不再需要,那么可以使用
self
,此时方法函数将持有其所依附的对象的所有权。 - 如果方法函数需要对其所依附的对象内容进行修改,就必须使用
&mut self
。但是这种情况在使用的时候需要注意,根据Rust的可变引用使用规则,同一时间仅可以存在一个可变引用,所以这种情况常常可能会导致编译错误出现。
&mut self
的可变引用形式,使用self
然后返回一个全新的实例是一种更好的选择,或者可以使用Cell
和RefCell
带来的内部可变性来改变方法函数所依附对象中保存的内容。
函数的生命期确定
函数的生命期通常都是其调用环境位置的生命期,一般使用'a
标注,而且大部分情况下,函数的实参都至少满足这个生命期,函数的返回值也一定是这个生命期。对于函数生命期的标注通常需要注意以下几点。
- 如果函数返回的是一个字符串字面量,那么可以使用
&'static str
来作为返回值。 - 尽量不要标注函数使用
'static
生命期,这样会使函数能够接受的实参受到比较大的限制。 - 明确函数的实参会来自多个作用域的时候,才需要使用到多个生命期标注,通常只需要选择能够满足函数执行的最小生命期进行标注即可。
函数的返回值
函数的返回值通常与函数的类型无关,但是函数的返回值通常与函数的生命期是相关的。构建函数的返回值时,一般可以参考以下经验。
- 函数所返回的内容,必须是函数拥有所有权的内容。
- 如果函数需要返回一个可以在许多位置被使用的内容,可以使用
Box
或者Rc
、Arc
将其分配到堆上。 - 如果需要返回一个生成在函数内部的内容的引用,必须使用
Rc
或者Arc
之类的引用,将其转移到堆上,保证其存活。 - 如果函数需要返回一个对于外部传来的值的引用,建议尽量选择使用
Weak
进行包装,以明确标识其不可靠。 - 如果需要实现链式调用,那么函数应该尽量返回一个全新的实例,而不是引用。
- 在闭包中尽量不要返回引用,如果需要返回引用,要借助HRTBs。
生成器和工厂函数类
生成器和工厂函数都是用来从无到有的产生一个符合条件的新值的,从生成器和工厂函数中返回的内容一般是需要采用返回所有权的形式的。例如Rust中常见的构造器new()
、from()
、into()
等方法,都是属于这种类型。
绝对不可能完成的事情
在Rust中,函数是不可以返回生命期小于函数本身的内容的引用的。在函数中创建的内容,其生命期通常都小于或者等于函数本身的生命期,当函数结束的时候,这些内容作为函数运行现场的成员,就会被丢弃。如果从函数中返回引用这些内容的引用,那么这个引用直接就是一个悬垂指针,这在Rust中是不允许的。
比如以下示例,这个示例是无法通过编译的。
|
|
但是可以改成下面这样。
|
|