Rust生命周期之个人理解
在Rust里,类型分为基础类型名(想不到更好的描述),类型泛型名,和生命周期(可以显示或自动推断一个生命周期标志)组成,其中生命周期[标志]也可以不算类型组成的一部分,因为它具有动态性是可变的;
生命周期标志在Rust编译器足够智能后单线程下有部分其实是可以不要的【对于结构体属性如果是引用的rust强制要求提供生命周期标志】(多线程是需要的,多线程的作用域rust没法比较,人都没法完全比较对;单线程下生命周期函数'static也是蛮有用的可以限制调用者必须传什么参数,比如用了format!这样的宏,外部的参数如果是非static的是不能作为formatter字符串的),除非我们希望提供足够详细的生命周期报错信息(最简单的例子就是longest(a, b)->c的例子,其实本质上没有生命周期标志rust编译器也是能检测到相关调用这类函数代码是否有问题,但是目前rust要求必须声明这种函数时就要指定生命周期标志);
我们通过代码来理解声明周期到底是什么:
1.
let mut list: Vec<&str> = Vec::new(); let str1 = "ssf".to_string(); list.push(str1.as_str()); let str2: String = "ccc".to_string(); list.push(str2.as_str()); println!("{:?}", list);
上面的代码我们可以看到list的元素类型是&str,且两个元素str1和str2的生命周期都是一样的(被编译器自动推断),且元素生命周期要大于等于list的生命周期(从使用的时候开始算,因此println!时list和str1和str2的生命周期一致);
因此println!(...)不会报错(或说不会造成前者错误【后面的代码示例可以看到是什么意思】);
2.
let mut list: Vec<&str> = Vec::new(); list.push("aaa"); { let str2: String = "ccc".to_string(); list.push(str2.as_str()); // flag0 } println!("{:?}", list); // flag1
这里则会报错,因为str2的生命周期比list要小,如果我们把flag1注释掉不会报错(以前估计是会报错的,后来智能化了一点),因为rust编译器能够检测到后续没有再用list了所以不会造成内存安全问题(比如list.get(1)会引用已经释放的内存,不过rust编译器没有那么智能所以发现是用到了list就认为可能产生内存安全从而报错【因此哪怕后续我们只用list.get(0)也会报错】,不过报错的地方是flag0,这也是上面说的不会造成前者错误的解释)
3.
let mut list: Vec<&str> = Vec::new(); list.push("aaa"); let str2: String = String::new(); list.push(str2.as_str()); println!("{:?}", list);
这个也不会报错,明显第一个元素和第二个元素的生命周期是不一致的,一个是static,一个是暂且叫'a,所以之前自己在知乎看的一篇文章说的是有问题的;这里是只要元素的生命周期大于等于list即可;
4.
#[derive(Debug)] struct Kkk<'a, 'b> {
// 这里不是说pro1是'a的,它只是一个引用/借用/指针(引用作为结构属性和部分特殊的函数【比如longest这类】必须有生命周期标志)
// 这个是说pro1这个引用指向的空间是一个String对象空间,然后这个对象空间的生命周期是'a pro1: &'a String, pro2: &'b i32, } ------------------------ { let sse = 3; let mut k = Kkk {pro1: &"sss".to_string(), pro2: &sse}; println!("{:?}", k); { let ssk = 4; k.pro2 = &ssk; println!("{:?}", k); } //println!("{:?}", k); // flag1 //println!("{:?}", k.pro1); // flag2 }
接下来看这个不会报错,由这个例子可以看出结构体里不要求所有的属性生命周期是一致的,也不要求属性的生命周期标志只能对应一个生命周期(比如pro2的先后两次赋值的生命周期显然是不同的,但是都用的'b标志。。【这就是一开始说的生命周期作为类型一部分是可变的意思】)
如果我们只把flag1取消注释则会报错,因为此时pro2的生命周期要小于k,所以会产生内存安全问题(引用已经释放的属性pro2,对应ssk变量);
但是如果我们只取消flag2的注释也是会报错的,尽管我们只引用了pro1,而pro1的生命周期是大于等于k的,这个是因为rust编译器不够智能导致的(以后可能会改善);就像我们闭包里如果用了一个全局变量则必须加锁才行,哪怕我们的程序是单线程的,因为rust编译器不够智能,它不知道我们会不会把这个闭包用在多线程里,因此干脆就直接提示可能存在问题需要加锁(虽然是可以更智能,不过代价很大,可能会造成编译速度下降和编译器占用内存啥的都升高)
【非智能指针对象,即栈上的对象,不存在RAII销毁对象的说法,在C++里相当于不是通过new创建的,自然不需要delete【类似比如int i = 3;我们是不需要说delete i的,int也可以是struct类型前提是栈上的对象(字面量赋值后的变量不是static的)】,只是作用域过后不可见,之后栈帧弹出后释放变量值(栈上对象值)】
接下来是多线程下的生命周期问题,目前有这些需要特殊关注的点:
1.我在A线程创建k对象,然后k的两个属性也都是在A线程创建的,所以在A线程创建这些变量的方法或作用域块结束后就会释放这些对象及属性,
因此开启一个线程B的方法里如果捕获了在线程A创建的变量时需要用到move【标准库里的是必须move除非捕获的“变量”是'static的,但是如果我们明确知道开启的线程B一定比A先销毁,我们可以用第三方库实现不move只是借用一下和只read,貌似crossbeam可以】,将这些变量及属性的所有权move到线程B里(对应的变量值(栈上的)会复制一份在closure的栈上创建一个同名变量【这个复制和Copy trait不是一个概念,就是连续内存空间的复制】),这样当A线程创建这些对象的作用域结束后就不会去释放这些对象或属性了(前提是栈变量是智能指针类型)【Box创建的对象不会释放,但是栈变量还是会销毁的】,只不过在创建线程move之后A线程是没法再使用这些变量了);
不过这造成了一个问题,就是当我们在A线程开启这样的线程后,在接下来的A线程代码里就无法引用这些变量了,这个和Java之类的语言很不一样;
如果我们想要在A线程后续继续用move的变量该怎么办呢,目前我知道的就是要么在A线程用完相关变量之后再创建B线程move这些变量(废话),要么就是把这些变量创建为全局变量,然后用的时候都要加锁。。;
好像之前还听过可以在A线程里将move的变量再move回来,不知道是不是有这个功能(听起来像Go的channel功能,好像rust里有个mpsc channel就类似这个功能,不过channel对象应该是要求全局的【或说是创建在堆上】)
,这个功能如果用类似channel的方式做应该是这个样子,A线程在创建线程move了k对象后,然后继续执行其他代码,然后在需要再次用到k的时候通过全局的channel的引用来接收B线程发过来的k对象,B线程在做完一些逻辑后也是通过堆 channel的引用来send处理后的k对象,在A线程receive k对象的过程里是会阻塞A线程的直到接收到了消息(即k对象【或包含其的对象】)然后继续A线程的处理逻辑;
Rust的channel是一个函数创建一个receiver和一个sender,在堆空间里;因此A和B线程分别用其中一个即可,都是直接move过去;
Rust应该还有可以在多个线程共用的数据类型,即不是那种必须move到B然后A就不能用了的(不过这种不是安全的操作),比如上面的receiver我们如果希望A和B线程都能receiver时就要这个功能(go里没记错的话好像是send一条数据后会“随机”一个协程通过同一个channel接收到数据【但应该是先循环探测到数据的那个协程收到数据】)
注意,Rust里let p = Point { x: 0.0, y: 0.0 }是创建在栈上(所以p所在作用域结束后只是后面代码无法访问,但是没有delete,而是等对应栈帧弹出后释放栈帧上所有对象)
而let p = Box::new(Point { x: 0.0, y: 0.0 })则是创建在堆上,因此创建p对象的作用域结束后会自动delete p引用的内存空间,而p变量变成不可访问,但是p也是占用一个指针地址的需要栈帧结束后弹出;
因此对于需要move到其它线程的对象,最好创建在堆上开销小一点;
再来看这个代码:
static mut KKK: String = String::new(); fn main() { unsafe { KKK = "ss".to_string(); test(&KKK); /*let kkk = "ss".to_string(); test(&kkk);*/ } } const fn confn() -> i32 { 8 } fn test(a: &'static String) { println!("{}, {}", a, confn()) }
上面的代码不报错,由此可知static生命周期标志(非static生命周期标志必须在结构体名或方法名上先声明才能在参数或属性上用,static不需要)并不是说必须要字面量,也不是说必须是常量,可变的static变量也是可以的,因为它在栈上且不会被delete掉;
像Box::new(..)的结构体虽然也在栈上,但是它指向的对象是可能被自动delete掉的,因此不符合static生命周期的定义。
再看这个代码:
fn main() { test2("aaa") } const fn confn() -> i32 { 8 } fn test2(a: &'static str) { // 是指a这个 str类型的借用(指针)指向的str类型空间是static的【不能被RAII释放的堆空间,无法被解引用,能被解引用的指针(借用)都是指向的可以被RAII的空间】 println!("{}, {}", a, confn()) }
这里也不报错,因为"aaa"是作为程序元数据的一部分的在加载程序后(加载程序指令,而"aaa"直接作为了指令的一部分而不需要创建栈空间或者堆空间),因此符合static
再看这个:
fn main() { test(); } struct Kkk { pro1: i32 } fn test() {
// 这里可以打印{:p}来输出k指向的地址,然后多次调用test()可以发现都是指向的一个地址,说明确实是静态局部变量; let k = &Kkk{pro1: 8}; // 这里如果换成&mut Kkk{pro1:8};则报错了,因为这种生成的是局部变量,而非mut的生成的是static的局部绑定变量,所以没有内存泄漏,因为下次调用这个方法用的k指向的对象还是上一次调用这个方法产生的 test2(k); /*let k2 = Kkk{pro1: 8}; test2(&k2);*/ } fn test2(a: &'static Kkk) { println!("{}", a.pro1) }
这里也不报错,但是注释的那部分是会报错的,这里感觉和Go很像不&则是分配在栈上,而&则是分配在堆上(但是注意不是说分配在堆上就符合static);
所以这里要消除一个错误的认知,即let kk = &Kkk{pro1:8};并不是生成这样的代码(至少不是唯一的方式,可能会根据上下文产生不同的情况):
let tmp1 = Kkk {pro1:8}; let kk = &tmp1;
/*
好吧,这里的生成代码的方式和是否有mut有关,let k = &Kkk{pro1: 8};这种方式生成的是static tmp1 = Kkk {pro1:8};【tmp1只读,而且也没有产生内存泄漏,这个就和C++的方法里的static变量是一样的】
而如果是let k = &mut Kkk{pro1:8};这种方式则是生成的是let mut tmp1 = Kkk {pro1:8};
*/
不过话说回来,上上的代码示例里的情况应该会产生内存泄漏把?k指向的地址是static的,因此根据之前的理解应该是不能被RAII 自动delete释放的,但是test里执行完后k指向的堆空间应该是没法再访问到的。
let bbb = "sfjlk"; // 这个字符串字面量类型是&str,即这个字符串实际上是保存在不能被RAII释放的堆空间里的(应该有类似享元模式的str优化)
// ,然后获取这个str的地址(对该str对象借用)赋值给bbb,因此不能用*"sss",因为static 对象无法被move【可以move的都是能被释放的】
注意上面的代码bbb是static的,通过这个代码测试出的:【没问题,字符串的字面量确实是static的,因为它本身就是&str,但是数值的字面量不是static,因为数值是i32,所以let bbb = "kkk";bbb是static,而let bbb = 3;bbb不是static,但是let bbb = &3;则bbb是static】
【还有一点很重要:(a: &'static str),这里是说a是一个static的str的引用,而不是说a这个引用是static的,这一点要明确,理解了这个后用人的思维看这个就很好理解了,bbb虽然是栈上的变量,但是它是一个static str的引用因此符合这个条件】
fn test3(a: &'static str) { println!("{}$$$", a); } fn main() { let bbb = "sfjkl";
// 这里可以写成&bbb或&&bbb都是一样的,因为会被自动解引用为bbb,但是closure捕获的变量是不会自动解引用的,因为本身就没有需要转换为什么类型 test3(bbb); }
/*
而且这里将bbb换成:
let uuks = String::new();
let bbu = uuks.as_str();
里的bbu则编译不通过提示bbu存活时间不够长,说明不是编译器“智能”的发现bbb虽然非static但是确实要比test3更长存活所以也通过了,而是bbb就是static的;【宏参数则要求必须是字面量,static的变量都不行,宏比较特殊就不考虑那么多了】
*/
但是这个代码又不能运行成功不知道为什么【经过大佬提点,closure如果没有move则这里其实是捕获的bbb的借用,即这里的bbb的类型实际上是&&'static,因此它类似let b = 3;我们捕获b一样(只不过3的类型换成指针类型),
或者这么说,bbb指向的字符串确实不会在调用者方法结束后RAII掉,但是bbb这个指针变量是会的,而不move则这里捕获的是指针变量的引用,因此存活期可能不够长】:
【这里再换个概念对个人理解更好,就是rust closure不能理解为捕获一个变量,而应该理解为在closure里产生一个地址空间的引用/指针(某种意义上也叫变量的引用),如果这个地址空间是static的则可以不用move,否则必须move(这个时候就不要非得用生命周期标志来理解了
,按人的思维理解反而更好,即f: 'static是指f里捕获的变量【捕获的对象空间(i32的也是一种对象)】必须是static的【不被释放的堆空间】,然后closure会自动获取其(这个空间)借用)
,因此这里没有move时bbb对象所在的空间是栈空间,是会消亡的,因此不是static,一下子就理解了,如果非得生搬硬套生命周期标志就很蛋疼(也可以理解,就要把它展开为let bbb_tmp = &bbb)】
let bbb = "sfjlk"; // 换成这个可以:static bbb: &str = "sfjlk";,这个时候调用者函数结束也不会释放bbb变量【但是如果换成static mut则又不行,因为这里可能存在并发问题(可以用unsafe解决,不过最好加锁)】 thread::spawn(|| { println!("{}####", bbb); }).join();
// 这里如果用了move个人认为是编译器产生了魔法,即在closure最上面(即println上面)隐式的添加一个代码let bbb = 外部bbb的值【一个地址】;,这个是在运行到这里时执行的,编译期间应该没法知道bbb的值是多少吧
for v in 1..=2 { test8(); } } fn test8() { static mut m: i32 = 0; unsafe { println!("{}", m); m += 1; } }
这里输出两次分别是0和1可以知道static局部变量的用处函数结束后不会释放m变量,因此m变量其实是在堆里的一个i32变量,只不过可见性是test8函数里,因此这里其实捕获的不是局部变量而是全局变量了【和C++一样】
按理这个f确实捕获的是static的变量才对啊,为什么不允许呢【经过大佬提点,闭包捕获的变量如果没有move,其实是捕获的是这个变量的借用,因此捕获的变量的类型其实是&&'static,即bbb是&'bbb,但是这里又弄了它的借用因此不是'static的了】
这部分代码换一下就可以了,但是还是没有解释上面的为什么不行:
fn main() { static bbb: &str = "sfjlk"; thread::spawn(|| { println!("{}####", bbb); }).join(); }
还有就是类型的static和对象/变量的static不是一个概念,看代码:
struct TestStruct(i32); trait Hello: Sized { fn say_hello(&self) { // 默认trait实现 println!("hello"); } } impl Hello for TestStruct{} fn test1<T: Hello + 'static + Sized>(t: &T) { t.say_hello(); } fn test2() { let x = TestStruct(666); test1(&x); } fn main(){ test2(); }
上面的代码通过,可能有人会觉得疑惑,这里不是要求T是static的吗,但是x明显是局部变量啊;注意这里的static是针对T类型的,即T类型必须是static的类型声明(前提是它必须声明生命周期标志,那么编译器判定发现生命周期标志不等于static则不符合上面的约束);
(注意impl trait是可以写在局部的,类似use一样可以局部use)
【注意,在方法里声明的类型也是static的,只不过是作用域可见性变小了,类似函数里static变量一样】
看这段代码:
struct TestStruct<'a>(&'a i32); trait Hello: Sized { fn say_hello(&self) { println!("hello"); } } impl<'a> Hello for TestStruct<'a>{} fn test1<T: Hello + 'static + Sized>(t: &T) { t.say_hello(); } fn test2() { let y = 666; let x = TestStruct(&y); // 这种不行,【因为y不是static(注意不是说字面量就是static的,所以之前的那个字面量写到程序元数据里所以是static的理解是错误的,static就是因为指向的空间是不会被RAII的堆空间)】 test1(&x); } fn main(){ test2(); }
这个就会报错了,因为这里经过编译分析,发现TestStruct<'a>不等于TestStruct<'static>(当然不存在这种写法,static不需要先声明才能在属性上使用),因此这里判定T不符合T: 'static的条件;
我这边再换成这种写法:
struct TestStruct<'a>(&'a i32); trait Hello: Sized { fn say_hello(&self) { println!("hello"); } } impl<'a> Hello for TestStruct<'a>{} fn test1<T: Hello + 'static + Sized>(t: &T) { t.say_hello(); } fn test2() { //let y = 666; let x = TestStruct(&666); // 改动点 test1(&x); } fn main(){ test2(); }
这种写法又可以了(因为&666实际上是生成了一个static的局部变量,类似static tmp1: i32 = 666;)
结构体的属性如果是引用类型则rust要求必须写生命周期标志(因为引用/借用类型的属性,它本身没有这个属性指向的对象的所有权,所以它是可能用着用着就被其他地方给释放掉了,因此必须用生命周期标志);
posted on 2020-07-02 16:12 Silentdoer 阅读(1013) 评论(0) 编辑 收藏 举报