常见的锈蚀寿命误区

2020-05-23 15:14:02

我曾经在某种程度上持有所有这些误解,今天我看到许多初学者都在与这些误解作斗争。我的一些术语可能不是标准的,所以这里有一张我使用的速记短语的表格,以及我想要表达的意思。

简而言之:变量的生存期是编译器可以静态验证它所指向的数据在其当前内存地址有效的时间长度。我现在将用接下来的6000个单词更详细地介绍人们通常会感到困惑的地方。

这种误解更多的是关于泛型,而不是生存期,但是泛型和生存期在铁锈中紧密交织在一起,所以谈到其中一个而不谈到另一个是不可能的。不管怎么说:

当我第一次开始学习Rust时,我知道I32、&;I32和&;mut I32是不同的类型。我还理解一些泛型类型变量T表示包含所有可能类型的集合。然而,尽管我把这两件事分开理解,我还是不能把它们放在一起理解。在我的新手Rust头脑中,我认为泛型是这样工作的:

%t包含所有拥有的类型。&;T包含所有不变的借用类型。&;mut T包含所有可变借用类型。T、&;T和&;mut T是不相交的有限集。漂亮,简单,干净,轻松,直观,完全错误。下面是泛型在Rust中的实际工作方式:

T、&;T和&;mut T都是无限集合,因为可以借用ad-infinitum类型。T是&;T和&;mut的超集。&;T和&;mut T是不相交的集合。这里有几个例子来验证这些概念:

&;T{}的特征特征{}实施<;T&>;特征(&;t;T&>;特征)&;T;T&>;特征(&;t;T&>;特征)编译错误(&;mut;T&>;T&>;特征)(&;mut T{})//编译错误。

错误[E0119]。mut_`:-->;src/lib.rs:7:1|3|为T{}实施<;T>;特征|-第一个实现在此.。7|实施&;mut T{}的T>;特征|^`冲突的`&;mut_`实现

编译器不允许我们定义&;T和&;mut T的特征实现,因为这将与已经包括所有&;T和&;mut T的特征实现冲突。以下程序按预期进行编译,因为&;T和&;mut T是不相交的:

特征特征{}实施&;T&>;特征{}//编译&;mut T{}//实施<;T&>;特征。

大多数Rust初学者都是在如下代码示例中第一次了解静态生存期的:

他们被告知,字符串文字被硬编码到编译后的二进制文件中,并且在运行时加载到只读存储器中,因此它是不可变的,并且对整个程序有效,这就是它是静态的。围绕使用static关键字定义静态变量的规则进一步强化了这些概念。

静态字节:[U8;3]=[1,2,3];静态mut_bytes:[U8;3]=[1,2,3];fn main(){mut_bytes[0]=99;//编译错误,变异静态不安全{mut_bytes[0]=99;assert_eq!(99,mut_bytes[0]);}}。

静态生存期可能是以静态变量的默认生存期命名的,对吗?因此,静态生存期必须遵循所有相同的规则是有道理的,对吗?

是的,但是具有静态生存期的类型与以静态生存期为界限的类型是不同的。后者可以在运行时动态分配,可以安全自由地变异,可以删除,并且可以存活任意持续时间。

静态T是对某些T的不可变引用,这些T可以安全地无限期地保持很长时间,包括直到程序结束。只有当T本身是不可变的,并且在创建引用后不会移动时,这才是可能的。t不需要在编译时创建。有可能在运行时生成随机的动态分配的数据,并返回对它的静态引用,代价是内存泄漏,例如

使用rand;//在运行时生成随机静态字符串引用fn rand_str_Generator()->;&;';静态字符串{let rand_string=rand::Random::<;U64>;()。to_string();Box::Leak(rand_string。into_boxed_str())}。

T:';Static是一些T,它可以安全地无限期地保持很长时间,包括直到程序结束。T:';static包括所有&;&;static T,但它也包括所有拥有的类型,如String、Vec等。某些数据的所有者可以保证,只要所有者持有数据,数据就永远不会失效,因此所有者可以安全地无限期地持有数据,包括直到程序结束。T:';静态应理解为";T由";静态生存期限定,而不是";T具有";静态生存期。帮助说明这些概念的程序:

使用rand;fn drop_static<;T:';static>;(t:t){std::mem::drop(T);}fn main(){let mut string:vec<;string>;=vec::new();for_in 0.。10{if rand::Random(){//所有字符串都是随机生成的//并在运行时动态分配,让string=rand::Random::<;U64>;()。to_string();字符串。Push(String);}//字符串是拥有的类型,因此它们由字符串中mut字符串的静态约束{//所有字符串都是可变字符串。PUSH_STR(";a突变";);//所有字符串都是可丢弃的DROP_STATIC(字符串);//编译}//程序结束前所有字符串都已失效println!(";我是程序的结尾";);}。

如果T:静态,则T可以是具有静态生存期的借用类型或拥有类型。

&;A T需要并暗示T:';a,因为如果T本身对';a无效,则对生命周期&a的引用不能对';a有效。例如,Rust编译器永远不允许构造类型&;static Ref<;';a,因为如果引用仅对';a有效,我们就不能使';a成为静态类型。

//仅采用';a,t:';a,T:';a>;(t:&;&39;a){}//以';fn t_bound<;';a,T:';a>;(t:t){}//所拥有的包含引用结构的类型为边界的ref类型(';a_ref<;';a,T:';a>;(t:t){}//包含引用结构的fn t_bound<;(t:t){}//拥有的类型包含引用结构。a>;(&;#39;a T);fn main(){let string=string::from(";string";);t_Bound(&;string);//编译t_Bound(Ref(&;string));//编译t_Bound(&;Ref(&;String));//编译t_ref(&;string);//编译t_ref(Ref(&;String));//编译t_ref(&;string);//编译t_ref(&;string。//编译错误,预期的ref,找到struct t_ref(&;Ref(&;string));//编译//字符串变量由';a t_bound(String);//编译}。

多亏了Rust的生存期省略规则,这个令人欣慰的误解得以保留,它允许您省略函数中的生存期注释,因为Rust借入检查器将按照以下规则推断它们:

如果存在多个输入生存期,但其中之一是&;self或& mut self,则self的生存期将应用于所有输出引用

//elided FN打印(s:&;str);//扩展FN打印(s:&;&39;a>;(s:&;';a str);//elid FN trim(s:&;str)->;&;str;//扩展FN trim<;&39;a>;(s:&;&39;a str)->;&。//非法,无法确定输出寿命,没有输入fn get_str()->;&;str;//显式选项包括fn get_str;()->;&;&39;a str;//毫无意义的泛型,因为';a必须等于';static fn get_str()->;&;&39;static str。//更好、更明确//非法,无法确定输出寿命,多个输入fn重叠(s:&;str,t:&;str)->;&;str;//显式(但仍部分省略)选项包括fn重叠<;&39;a>;(s:&;&39;a str,t:&;str)->;&;a str;/。t超出fn重叠<;a>;(s:&;str,t:&;&39;a str)->;&;a str;//输出不能超过fn重叠<;&39;a>;(s:&;a str,t:&;a str)->;&。//输出可以超过s&;t FN重叠(s:&;str,t:&;str)->;&;&39;静态str;//输出可以超过s&;t FN重叠<;&39;a>;(s:&;str,t:&;str)-&>&;&;a str;//输入和amp;之间没有关系。';a,';b&>(s:&;a str,t:&;&39;b str)->;&;&;';a,';b&>(s:&;&39;a str,t:&;&39;b str)->;&;';b。fn重叠<;a>;(s:&;a str,t:&;a str)->;&;a str;fn重叠<;a,';b>;(s:&;&39;a str,t:&;&39;b str)->;&;&39;';a,';b,';c&>;(s:&;&39;a str,t:&;';b str)->;&;';c str;//省略FN比较(&;self,s:&;str)->;&;&;str;//扩展的fn比较<;a,&39;b>。';a Self,&;&b str)->;&;a str;

Rust程序有可能在技术上是可编译的,但在语义上仍然是错误的。以此为例:

struct ByteIter<;';a&>;{剩余部分:&;&39;a[U8]}实施<;';a;ByteIter<;{fn Next(&;mut self)->;option<;&;U8&>;{如果自我保留。is_Empty(){None}Else{let byte=&;self.reminder[0];self.reminder=&;self.remindance[1..];Some(Byte)}fn main(){let mut bytes=ByteIter{Remainder:B";1";};assert_eq!(Some(&;b';1';),bytes。next());assert_eq!(无,字节。Next());}。

ByteIter是迭代字节切片的迭代器。为简洁起见,我们将跳过迭代器特征实现。它似乎工作得很好,但是如果我们想一次检查几个字节怎么办?

fn main(){设mut bytes=ByteIter{剩余部分:B";1123";};设byte_1=字节。Next();设byte_2=字节。Next();如果byte_1==byte_2{//做点什么}}。

错误[E0499]:不能一次将`bytes`作为可变变量多次借用-->;src/main.rs:20:18|19|let byte_1=bytes。Next();|-第一个可变借入出现在这里20|let byte_2=bytes。Next();|^第二次可变借入出现在此处21|If BYTE_1==BYTE_2{|-这里稍后使用的第一次借入。

我想我们可以复制每个字节。当我们正在处理字节时,复制是可以的,但是如果我们将ByteIter转换为可以迭代任何[T]的泛型切片迭代器,那么我们可能希望在将来将其用于可能非常昂贵或不可能复制/克隆的类型。哦,好吧,我想我们对此无能为力,代码会编译,所以生存期注释一定是正确的,对吗?

不,当前的生存期注释实际上是错误的来源!它特别难被发现,因为错误的生命周期注释被省略了。为了更清楚地了解问题,让我们延长已取消的生命周期:

struct ByteIter<;';a&>;{剩余部分:&;&39;a&>}实施<;';a&>;字节项<;{FN Next<;';b&>(&;#39;b mut Self)-&>选项<;&;b U8&>t;{FN Next<;(&;#39;b MUT Self)-&>选项<;&;b U8&>t;{FN Next<;(&;#39;b MUT Self)-&>选项<;&;b U8&>。IS_EMPTY(){NONE}ELSE{let byte=&;self.reminder[0];self.reminder=&;self.reminder[1..];ome(Byte)}。

那一点帮助都没有。我还是很困惑。这里有一个只有铁锈专家才知道的热点提示:给你的终身注释起描述性的名字。让我们重试:

结构字节项目';剩余部分&>;{剩余部分:&;#39;剩余部分[U8]}实施<;';剩余部分;字节项<;';剩余部分&>{FN NEXT<;';MUT_SELF&>(&;&39;MUT_Self MUT SELF)-&>选项<;&;&39;IS_EMPTY(){NONE}ELSE{let byte=&;self.reminder[0];self.reminder=&;self.reminder[1..];ome(Byte)}。

每个返回的字节都带有';mut_self注释,但是这些字节显然来自';RELENDER!让我们把它修好。

结构字节项<;';剩余&>;{剩余部分:&;';剩余部分[U8]}实施<;';剩余部分<;';剩余部分&>{fn Next(&;mut self)-&>选项<;&;&;';剩余部分U8&>{如果自我保留。is_Empty(){None}Else{let byte=&;self.reemainth[0];self.reminder=&;self.remindance[1..];Some(Byte)}fn main(){let mut bytes=ByteIter{RELEMENT:B";1123";};let byte_1=bytes。Next();设byte_2=字节。Next();std::mem::drop(Bytes);//我们现在甚至可以删除迭代器!如果BYTE_1==BYTE_2{//编译//执行某些操作}}。

现在我们回过头来看我们程序的前一个版本,它显然是错误的,那么为什么Rust要编译它呢?答案很简单:它是内存安全的。

Rust借入检查器只关心程序中的生存期注释,只要它可以使用它们来静态验证程序的内存安全性。即使生命周期注释有语义错误,Rust也会愉快地编译程序,其结果是程序变得不必要的限制。

这里是一个与前一个示例相反的快速示例:在这个实例中,Ruust的生存期省略规则碰巧在语义上是正确的,但是我们无意中用我们自己不必要的显式生存期注释编写了一个非常有限制性的方法。在这种情况下,Ruust的生存期省略规则在语义上是正确的,但是我们无意中用自己不必要的显式生存期注释编写了一个非常严格的方法。

#[Derive(Debug)]struct NumRef<;';a>;(&;#39;a I32);Iml<;';a;NumRef<;';a&>{//my struct是';a上的泛型结构,这意味着我也需要用';a注释//我的self参数,对吗?(回答:不,不正确)fn ome_method(&;';a mut self){}}fn main(){let mut num_ref=NumRef(&;5);num_ref。SomeMethod();//可变地借用num_ref作为其剩余生命周期的num_ref。Some_Method();//编译错误println!(";{:?}";,num_ref);//也编译错误}。

如果我们在';a上有一些结构泛型,我们几乎永远不会想要编写一个带有mut自身接收器的方法。我们正在向Rust传达的是,此方法将在结构的整个生命周期内可变地借用结构。实际上,这意味着Rust的借入检查器在结构变为永久可变借入从而不可用之前,最多只允许调用一次某些方法。这方面的用例极其罕见,但是上面的代码对于困惑的初学者来说非常容易编写,并且可以编译。修复方法是不添加不必要的显式生存期注释,并让Rust的生存期省略规则处理:

#[派生(调试)]struct NumRef<;';a>;(&;#39;a I32);实施&39;a;NumRef<;';a&>a on mut self FN SOME_METHOD(&;MUT Self){}//以上行对FN SOME_METHOD_Desugared<。b mut self){}}fn main(){let mut num_ref=NumRef(&;5);num_ref.。某些方法();num_ref。Some_Method();//编译println!(";{:?}";,num_ref);//编译}。

前面我们讨论了Rust的函数生命周期省略规则。Ruust还具有特征对象的生存期省略规则,其中包括:

如果将特征对象用作泛型类型的类型参数,则从包含类型推断其生存期界限。如果从包含类型有多个界限,则必须指定显式界限。

如果上述条件不适用,则如果特性定义为具有单个生命周期界限,则使用该界限。

如果特征没有生存期界限,则其生存期在表达式中推断,并且在表达式之外是静态的。

所有这一切听起来超级复杂,但可以简单地概括为特征对象的生命周期界限是从上下文中推断出来的。在看了几个例子之后,我们会发现生命周期界限的推论非常直观,所以我们不需要记住形式规则:

使用std::cell::ref;特征特征{}//省略类型T1=Box<;dyn特征>;;//展开,Box<;T>;对T没有生存期限制,因此推断为';静态类型T2=Box<;dyn特征+';静态>;;//省略实施动态特征{}//扩展实施动态特征+';静态{}//省略类型T3。动态特征;//扩展的T需要T:';a,因此推断为';a类型T4>;=&;#39;a(动态特征;a);//省略类型T5<;&39;a&>;=Ref<;a,Dyn特征&>;//扩展,参考。需要T:';a,因此推断为';a类型T6<;';a&>;=Ref<;';a,dyn特征;特性GenericTrait<;';a&>;:';a{}//省略类型T7<;';a&>;=Box<;a&>;=Box<;a>;=Box<;a&>;';a&>=Box<;dyn GenericTrait<;';a>;dyn GenericTrait<;';a>;

实现特征的具体类型可以有引用,因此它们也有生存期界限,因此它们对应的特征对象也有生存期界限。此外,您还可以直接为引用实现特征,这显然是有生命周期限制的:

特征{}struct Struct{}struct Ref&>;(&;&39;T&>;(&;;a T);对结构{}实施特征对&;Struct{}//对引用类型实施特征直接对引用类型实施特征;对类型实施特征(&;A,T&>;{}//对引用类型实施特征),T&>;(&;A,T&>;(&;a);(&;a);直接在引用类型上实施特征(&;Struct{})//实施特征(&;Struct{}//Iml Struct{}//Iml Characteristic)。

无论如何,这是值得复习的,因为当初学者将函数从使用特征对象重构到泛型(反之亦然)时,这常常会让他们感到困惑。以此程序为例:

使用std::fmt::display;fn dynamic_thread_print(t:box<;dyn display+send>;){std::thread::spawn(Move||{println!(";{}";,t);})。Join();}fn static_thread_print<;T:display+send>;(t:t){std::thread::spawn(Move||{println!(";{}";,t);})。Join();}。

错误[E0310]:参数类型`T`可能存在时间不够长-->;src/lib.rs:10:5|9|fn static_thread_print<;T:display+send>;(t:t){|--help:考虑添加明确的生存期限制.:`t:';static+`10|std::Thread::SPAWN(移动||{|^}类型`[Close@src/lib.rs:10:gt;src/lib.rs:10:5|10|std::Thread::Spawn(移动||{|^。

好的,很好,编译器告诉我们如何解决这个问题,所以让我们来解决这个问题。

使用std::fmt::display;fn dynamic_thread_print(t:box<;dyn display+send>;){std::thread::spawn(Move||{println!(";{}";,t);})。Join();}fn static_thread_print<;T:display+send+';static>;(t:t){std::thread::spawn(Move||{println!(";{}";,t);})。Join();}

它现在可以编译,但是这两个函数看起来彼此相邻不方便,为什么第二个函数需要一个。

..