应用篋:作为函数参数对象的生命周期
本节将类型String
对象、类型String
引用和类型str
作为实例,分析它们作为函数参数的生命周期。
学习内容
- 了解和学习Rust语言含可变引用参数的函数借用实例
篇目
可变类型String
引用的生命周期
先把下面程序的两个对象进行一下说明:
- 对象
instance
是类型String
的可变对象,因为在使用let
绑定该对象instance
时,使用了关键词mut
。下面将对象instance
称之为可变对象; - 对象
mut_ref
是可变对象instance
的可变引用,因为在使用&
绑定对象值时,使用了关键词mut
。下面将对象mut_ref
称之为可变引用; - 对象
instance
是所有者,而对象mut_ref
仅仅是从所有者那里租用者,它们所拥有的内容是一样的; - 在对象
mut_ref
生命周期里,它可以操作对象instance
所拥有的所有内容。从这个意义上说,它们是完全一样的;
# #![allow(unused_variables)] #fn main() { // File: ./examples/mut_fn/base_string.rs // #[cfg(feature = "ok")] // FIXED fn one(input: &mut String) { input.push_str(", world"); println!("one() input = {:p}", input); } fn two(input: &mut String) { input.push('!'); println!("two() input = {:p}", input); } let mut instance = String::new(); println!("instance = {:p}", &instance); instance.push_str("Hello"); let mut_ref = &mut instance; println!("mut_ref = {:p}", mut_ref); one(mut_ref); two(mut_ref); println!("instance = {:p}", &instance); println!("{}", instance); #}
上面程序的运行结果如下。从该结果可以分析该程序,无论是可变对象instance
,还是可变引用mut_ref
或者方法参数input
,都是指向相同的内存地址,即可变对象instance
的内容。这里反映了Rust语言调用函数的借用机制。
───────┬──────────────────────────────────────────────────────
│ STDIN
───────┼──────────────────────────────────────────────────────
1 │ instance = 0x7fff5a219e70
2 │ mut_ref = 0x7fff5a219e70
3 │ one() input = 0x7fff5a219e70
4 │ two() input = 0x7fff5a219e70
5 │ instance = 0x7fff5a219e70
6 │ Hello, world!
───────┴──────────────────────────────────────────────────────
尽管主程序里,在创建可变引用mut_ref
以后,并没有看到对可变引用mut_ref
或者可变对象instance
进行赋值,但是可变对象instance
的内容一直在变化。
当方法借用可变引用mut_ref
以后,还是归还了所有权到主程序,而不是把该可变引用mut_ref
消费掉了。因此,第二个方法还可以继续使用该可变引用mut_ref
。
通过给两个方法传递可变引用mut_ref
,它们不仅可以借用其内容(这是因为绑定可变引用mut_ref
时,使用了关键词&
),而且还可以修改其内容(这是因为绑定可变引用mut_ref
时,在&
之后使用了关键词mut
)。同时方法还将其内容返回到可变引用mut_ref
,即可变对象instance
。
必须指出的是,方法参数也必须是可变引用,与调用方法时的对象类型是完全一致的,不然就会出现程序编译错误。下面程序就是说明这个问题。
# #![allow(unused_variables)] #fn main() { // File: ./examples/mut_fn/base_string.rs // #[cfg(feature = "err_05")] // error[E0596]: cannot borrow `*input` as mutable, as it is behind a `&` reference // FIXED // fn one(input: &mut String) { //OK fn one(input: &String) { input.push_str(", world"); println!("input = {:p}", input); } // fn two(input: &mut String) { //OK fn two(input: &String) { input.push('!'); println!("input = {:p}", input); } let mut instance = String::new(); println!("instance = {:p}", &instance); instance.push_str("Hello"); let mut_ref = &mut instance; println!("mut_ref = {:p}", mut_ref); one(mut_ref); two(mut_ref); println!("instance = {:p}", &instance); println!("{}", instance); // println!("instance = {:p}", &instance); //println!("{}", instance); #}
可变类型String
对象的生命周期
下面程序实例是将可变类型String
对象直接作为方法参数使用,就是方法直接借用可变类型String
对象,而不是其引用。下面实例可以正常运行,但是,它有什么问题呢?
# #![allow(unused_variables)] #fn main() { // File: ./examples/mut_fn/base_string.rs // #[cfg(feature = "okay")] // FIXED fn one(mut input: String) { input.push_str(", world"); println!("input = {:p}", &input); println!("input = {}", input); } let mut instance = String::new(); println!("instance = {:p}", &instance); instance.push_str("Hello"); one(instance); #}
上面程序的运行结果如下。当可变对象instance
作为参数传递到方法以后,该对象就被该方法消费掉了。从它们的内存地址信息可以看到,它们是不一样的。
───────┬──────────────────────────────────────────────────────
│ STDIN
───────┼──────────────────────────────────────────────────────
1 │ instance = 0x7fff53e5cf50
2 │ input = 0x7fff53e5cfc0
3 │ input = Hello, world
───────┴──────────────────────────────────────────────────────
主程序可变对象instance
是不是真的被方法消费掉了呢?通过编译下面程序,编译器会告诉我们答案。下面出现编译错误说明了可变对象instance
不转移了,就是被方法消费掉了。
# #![allow(unused_variables)] #fn main() { // File: ./examples/mut_fn/base_string.rs // #[cfg(feature = "err_04")] // error[E0382]: borrow of moved value: `instance` // FIXED fn one(mut input: String) { input.push_str(", world"); println!("input = {:p}", &input); } let mut instance = String::new(); println!("instance = {:p}", &instance); instance.push_str("Hello"); one(instance); println!("{}", instance); #}
引用类型str
对象的生命周期
上面说明了类型String
对象的可变引用,不仅可以被借用,而且还可以被修改。但是引用类型str
对象只能被借用,而不能被修改。因为它不能被修改,所以它返回时还是它本身。
引用类型str
对象作为方法参数,主要目的是传递其内容。
# #![allow(unused_variables)] #fn main() { // File: ./examples/mut_fn/base_str.rs // #[cfg(feature = "okey")] // FIXED fn one(input: &str) { println!("one() input = {:p}", input); let ret_input = input.to_ascii_uppercase(); println!("one() ret_input = {:p}", &ret_input); println!("one() ret_input = {}", ret_input); println!("one() input = {:p}", input); println!("one() input = {}", input); } let mut instance = String::new(); instance.push_str("Hello"); println!("after instance change = {:p}", &instance); println!(); let immut_ref :&str = &instance; println!("before call one instance = {:p}", immut_ref); one(immut_ref); println!("after call one instance = {:p}", immut_ref); println!("instance = {}", instance); #}
上面程序运行结果可以看到,尽管在方法里参数input
调用了方法to_ascii_uppercase()
,但是,不仅参数input
的内存地址始终与主程序输入对象的内存地址是完全一样的,而且其内容到最后还是原来的内容。这种调用了方法to_ascii_uppercase()
并不是修改其内容,而是创建一个新对象,在新对象里,可以看到其内容发生了变化。
───────┬──────────────────────────────────────────────────────
│ STDIN
───────┼──────────────────────────────────────────────────────
1 │ after instance change = 0x7fff5c99ce48
2 │
3 │ before call one instance = 0x7fff5c99ce48
4 │ one() input = 0x7f94a9c00330
5 │ one() ret_input = 0x7fff5c99cc70
6 │ one() ret_input = HELLO
7 │ one() input = 0x7f94a9c00330
8 │ one() input = Hello
9 │ after call one instance = 0x7fff5c99ce48
10 │ instance = Hello
───────┴──────────────────────────────────────────────────────
引用类型str
对象的生命周期符
通过下面实例,将引出生命周期符的概念。下面程序编译会出现错误:"缺少生命周期符"。
# #![allow(unused_variables)] #fn main() { // File: ./examples/mut_fn/base_str.rs // #[cfg(feature = "err_07")] // error[E0106]: missing lifetime specifier // FIXED fn one(x: &str, y: &str) -> &str { x } let first_str = "Hello"; let second_str = "World"; let result = one(first_str, second_str); println!("result = {}", result); #}
使用函数生命周期符,一般情况下,是因为函数返回对象存在引用对象。上面程序怎么样解决呢?下面程序将上面程序问题得到解决。之所以上面程序不能编译成功,是因为该方法存在两个引用,它们都有自己的生命周期,而结果只有一个生命周期,编译器不知道应该使用哪一个生命周期。生命周期符是统一这两个引用的生命周期。
# #![allow(unused_variables)] #fn main() { // File: ./examples/mut_fn/base_str.rs // #[cfg(feature = "ok")] // FIXED fn one<'de>(x: &'de str, y: &'de str) -> &'de str { println!("x = {:p}", x); println!("y = {:p}", y); if x.is_empty() { x } else { y } } let first_str = ""; let second_str = "World"; println!("first_str = {:p}", first_str); println!("second_str = {:p}", second_str); let result = one(first_str, second_str); println!(); println!("first_str = {:p}", first_str); println!("second_str = {:p}", second_str); println!("result = {:p}", result); println!("result = {}", result); #}
在上面程序里,生命周期符'de
是由两部分:前面单引号'
是必须的;而后面与对象命名法则(snake_case)是一样的。一般使用'a
。
题外话
类型String
与类型str
的区别
字符串类型对象是可变的。而类型str
,通常以&str
表示对象是不可更改的,它只是对字符串的内容储存。
下面程序通过两个方法make_ascii_uppercase()
与to_ascii_uppercase()
说明,两种类型对象使用时的区别。方法make_ascii_uppercase()
是直接修改其对象内容,得到其结果;而方法to_ascii_uppercase()
并不是直接修改本身引用对象,而是创建了一个新类型String
对象作为返回结果。
fn main () { let mut instance = String::from("Hello"); println!("instance = {:p}", &instance); instance.make_ascii_uppercase(); println!("{}", instance); println!("instance = {:p}", &instance); println!(); let instance = "Hello"; println!("instance = {:p}", instance); instance.to_ascii_uppercase(); println!("instance = {}", instance); println!(); let instance = "Hello"; println!("instance = {:p}", instance); let ret_instance = instance.to_ascii_uppercase(); println!("instance = {}", instance); println!("ret_instance = {:p}", &ret_instance); println!("ret_instance = {}", ret_instance); }