同步操作将从 deepinwiki/wiki 强制同步,此操作会覆盖自 Fork 仓库以来所做的任何修改,且无法恢复!!!
确定后同步将在后台操作,完成时将刷新页面,请耐心等待。
rust 是一门系统级别的底层语言,它的竞争对手是 c / c++ 。
它比 c 优秀的地方是支持更强大的语法特性。但是又避免以性能为代价。(当然 c 看上去比较简单)。
它比 c++ 优秀的地方,在于它把精力花在现在行业比较注重的领域,那就是内存安全性。而c++ 历史包袱太重,各种领域都想涉及,庞然大物。这也是 linux 内核作者抨击 c++ 的地方。
由此可见,rust 的核心创新,就是内存管理。其他特性不过是甜头而已。
rust 的对象有且只有一个拥有者,其他使用者只能通过引用临时借用。拥有者决定了对象的生存和消亡。
怎么做到?
fn drop(_:T){
// 什么代码也没有
}
drop(A); // A 移动进去,却没有返回,所以 A 销毁了
在所有权移动语义下,要销毁一个对象变得很容易,也很自然。如果一个对象不被谁特意去持有,它自动就消亡。在 rust 中,消亡是主旋律,不断持有才是精心设计(比如函数要返回所有内部的变量,谁那么得闲?)。
因此, rust 虽然没有内置垃圾回收,但是它是一个拥有高效智能内存管理的编程语言。
类似的概念,比如 c 里面的指针, c++ 里面的引用,或者 java 里面的托管对象。它的含义就是一个指向实际地址的指针。因为有这个指针,就能对实际内存地址里面的数据间接操作。
c / c++ 这种看上去很合理的指针,但是被 rust 设计者们认为是存在风险的。多个访客对同一块内存地址的读写操作,是存在安全风险的。
应该对此作严格的限制。而建立这种严格限制的语言设施,就是“借用”。
rust 认为,数据安全的界限是:
不安全怎么办?传统的技能是:
这些人工的维护代码,对于传统编程语言的工程师来说,是极具挑战性的。
rust 也无法解决所有内存问题。因为所谓的安全不安全,由业务决定。老板就是要两个随机写的访客,就是玩,那老板也是对的!
但是借用就限定了在它认为安全的界限内,它能从语言层面给予支持,保证你不会出错。
这带来的好处,就是大部分非主观性的内存错误都不会发生。除非你跨越了 rust 设立的安全界限。
首先, rust 定义了所有权的概念:
一、一个对象只有一个拥有者(对应了第一条规矩)
虽然移动不等于复制,但是跨函数移动必然是复制。我们知道数据完整的复制,是代价比较高的。有一些语言到这一步就止步了,虽然保证了安全性,但是效率也失去了。作为一个系统语言,是不能对效率妥协的。
所以:
二、可以出借所有权,使用完毕归还
多个访客可以,但是只能读,不能修改,修改就会出现数据同步问题。甚至一个读,另一个写都是不允许的。这也许是很严格的要求,但是它是很好的界限,因为只要有写,另一个读的访客究竟该如何合理的处理数据?这就是具体业务逻辑才能说清楚的问题。rust 语言作者是永远无法确认哪一种默认逻辑才是合理的。
以上就是借用的设计逻辑。
但是具体实施到代码中去的时候,还有更多需要考虑的点。那就是怎么确定只读借用有几个?怎么确定读写借用有几个?
借了啥时候还?这些都是具体的问题。
另一个点是,出借方销毁了数据,那么借用不就没有意义了?这就涉及到生命周期的管理。
总体而言, rust 的借用相对指针受限制的点:
指针不管你(数据)有没有效,读写也相对随意,多个写也无所谓。
和变量的作用范围类似,借用的范围要稍微缩小一些。
一个变量在一个层级里面定义,它会从定义的开始到离开层级都保持。
而借用的范围是定义到最后使用的范围。如果这个范围内,出现不兼容的借用(比如读写借用),那就会编译报错。
当跨层级时,比如函数调用,进入内层事情会变得更复杂一些。
函数会有两种情况:
同步函数结束后,借用无疑会归还。但是异步函数呢?它在后面的代码中可能还是占用着借用的。这就不能假定它会归还了。然后同步函数内调用异步函数又如何?是不是应该假定所有函数都视为异步函数?
rust 没有这么简单化的处理,因为这样处理将会大大缩小借用的价值,rust 根据具体问题具体解决,但是也进一步提高它的复杂性:
归根结底,这是一系列生命周期问题。下面展开生命周期的概念:
这就保证了 a 永远有效(A 还活着)。这种约束是双向的,即要求 a 满足,也要求 A 满足。A 不能在存在 a 的时候移动自己,释放自己。a 不能跨越 'a 的限制。
所谓 'a 的限制是:
具体例子:
{
let b:&i32; // 'b
{
let A = 1; // 'c
let a = &A; // 'a; 'a < 'c, ok
b = a; // 'b > 'a, not ok
// A 的作用范围到这里
}
// b 这里还有效呢
println!(b);
}
别问为啥用 'a 这种怪异的表示方法,rust 就是用这个标识生命周期的。当然名字可以修改。
不过这种一眼就能判断出生命周期的场合,rust 是不要求你写生命周期标注的,生命周期标注在一些不那么明确的场合,如函数调用,结构内部。
fn max<'c,'a:'c,'b:'c>(x:&'a i32, y:&'b i32)->&'c i32{
if *x > *y{
x
}else{
y
}
}
如何阅读以上代码:
// 用法
let A = 1;
let B = 2;
let C = max(&A,&B);
标注是不是很麻烦?但是如果不标注,却返回一个借用,它究竟借用了谁?这种借用是不是安全的?比如:
这都是有可能的。但是标注了,编译器就能用这些信息来检查调用函数时实际的参数和返回值。
我们习惯了没有生命周期的指针,一时半刻可能还不能习惯借用是依赖生命周期这一事实。你可以把生命周期理解为借用的一部分(哪怕它有时候不标注出来)。
但是你细想一下,以上代码如果用指针来实现,是不是漏洞百出?这就是指针不安全的地方。
在 rust 中,生命周期和泛型参数是类似的技术,也就是静态生成(不同)类型的技术。
struct B<'a,T:'a>(&'a T);
视角:
因此可以认为借用有很多种形态(类引用):
可以说相当的繁复,梳理一下有三种:
这里侧重补充泛型 T:'a 为什么是必要的。因为 rust 的生命周期安全检查不能让长的指针指向短的指针。假设 T 是 &'b X, 且 'a > 'b, 则 &'a T 是错误的,因为结果是 &'a &'b X:即一个双重指针,且是一个长指针指向短生命周期的指针。
而 T:'a 的作用结果是 <'b:'a>&'a &'b X 是合理的。因此,泛型参数往往需要用生命周期来约束,毕竟 T 可以是任何类型。
特殊的生命周期:
注意: T:'static 约束的意义,它几乎就是表明 T 不是引用(指向静态常量范围太窄,意义不大)。
所谓子类型,类似面向对象中的基类出现的位置可以用子类代替。
rust 只有生命周期支持这种关系。
如 'a:'b(即 'a > 'b):
这里 ‘a 取代 ’b 是什么含义? 意思是一个生命周期长的对象 'a,你在更短的生命周期范围内('b)使用它。明显是安全的!
生命周期只是类型的一部分,它不能独立的表示类型,所以要和 T 相关:
Box<T:'b>
Cell<T>
先来理解为什么会有逆变。注意,fn(&'a i32)->&'b i32
这里是指函数指针,而不是函数。
首先要理解函数指针的作用,它是一个接口规范,数据从它流到实际的函数。因此流动是逆反的:
比较绕,需要慢慢理解。这里'static 是静态生命周期,是 rust 最大(长)的生命周期。
let f = |f:fn(&'static i32)->&i32|f(&1);
fn f1(_:&i32)->&'static i32{
&2
}
// 传入参数 'a --> 'static
// 返回 'static --> 'a
f(f1);
不变的情况是内部可变类型。还有可变双重指针,内层借用也是不可变的。如果短的内容可能存储到长的变量中,是不安全的,所以只能是不变。
fn f1<T:Copy>(a:& mut T, b:& mut T){
*a = *b; // 如果可以,a 将持有 b,而b 是 f 的内部变量
}
fn f(a:&mut &'static i32){
let b = 1;
f1(a, &mut &b); // &mut &'static -X-> &mut &'a
}
let mut a = &1;
f(&mut a);
// b 已经析构, a 持有无效的指针
使用生命周期会在三个地方:
标注的目的:
标注的意义:
简单来说,就是'a 只是占位符,你只需要将实际的对象放上去替换,根据替换后的实际对象的生命周期分析。f(a,b) 实际返回是a,还是b,编译器原本是不知道的,标注后,编译器就可以假定 a 或者 b了,这就是标注的作用。
如果有约束'a:'b, 它是要求实际的参数 a > b么,并不要求。如果 'a:'b ,且实际生命周期 a < b, 那么它只需要假定返回是 a 即可。标注的运算结果是让编译器选择返回的假定对象,而不是对参数a,b 的生命周期做任何约束,哪怕 a < b 一样可以传入参数。
fn f1(a:&u8, b:&u8)->&u8; // 默认 'a 'b 'c ,无关联,但返回值必须关联参数,所以报错
fn f2<'a>(a:&'a u8, b:&u8)->&'a u8; // 标注 'a, a 和 返回值相同
fn f3<'a,'a:'b>(a:&'a u8, b:&'b u8)->&'b u8;// 标注 'a > 'b, 返回值可能是 a 也可能是 b
struct A(&u8, &u8); // 结构没有默认值,报错
struct B<'a>(&'a u8, &'a u8); // 关联
struct C<'a,'b>(&'a u8, &'b u8); // 独立
impl<'a> B<'a>{
fn f1(a:&u8, b:&u8)->&u8{} // 实现函数和结构没有必然联系,可以视为独立的函数看待
fn f2(&'a self)->&'a u8{self.0} // 使用了结构的标注'a。实际对象可以来自 self (结构对象),也可以来自标注'a 的参数
fn f3(&self)->&u8{}
fn f3_5<'b,'a:'b>(&'b Self<'a>)->&'b u8{} // 上一个写法等价于这个。也就是和f2 有微妙的不同。
fn f4<'b>(a:&'b u8, b:&u8)->&'b u8{a} // 可以有自己独立的标注
}
trait TA{}
trait Into_Dyn<'a,T> // 默认 T:'static ,也就是和当前对象无关
where T:'a { // 修改 T:'a,也就是和当前对象相关
fn into(&'a self)->&'a T; // 返回的 T 和 self 有关,从而避免必须返回'static T
}
impl TA for B<'_>{}
impl<'a> Into_Dyn<'a, dyn TA> for B<'a>{
fn into(&self)->&dyn TA{
self
}
}
// 或者在使用时(而非定义时)用 dyn T + 'a 的方式也能改变默认的 'static
// 如 impl<'a> Into_Dyn<dyn TA + 'a> for B<'a>
// fn into(&self)->&(dyn TA + 'a){...}
// 这样只需要定义 trait Into_Dyn<T>,不需要格外定义 'a 参数了
trait Into //类型内部(非函数部分)出现的引用默认和Self 有关
where
for<'a> &Self:Into_Dyn<'a, &'a dyn TA>{} // 高阶 for<'a> 和 Self无关,和描述对象 Into_Dyn 有关
// 相关实现
impl<'a,'b> Into for C<'a,'b>{}
impl<'a> Into_Dyn<'a, &'a dyn TA> for C<'a,'_>{
fn into(&self)->&dyn TA{
self
}
}
impl TA for C<'_, '_>{}
fn f4<'a>()->&'a u8{painc!()} // 和输入参数无关的标注,可以转换为任意生命周期
借用规则内的是安全代码,但规则外也是必须要涉猎的,如果你的代码足够底层,你的需求足够奇葩。
这就是为啥 rust 提供非安全代码的缘故。
安全边界:
* ptr
(原生指针,非安全)rust 的非安全代码,要么是被封装好了,要么使用 unsafe 给界定。非安全代码的安全职责在程序员身上。
解决的问题:
安全边界内还是会出现的问题:
不要混淆原生的引用和智能指针,智能指针不是一个引用,它只是模拟引用的操作。你可以把理解为一个能够绑定(转移和持有)所有权的包装对象,虽然它的操作很多是类似指针操作,但是它的根本性质,是类似普通变量的。
DST(dynamic size type) 动态尺寸类型包含: 特征(trait)和切片(slice)。和 c++ 设计不同的地方,动态尺寸类型的尺寸信息是指针的一部分,即类似:
胖指针:
而 C++ 的设计是:
rust 这种设计的好处就是虚表指针不需要占用对象的大小,要用到这种运行时多态(特征指针)时才生成对应的虚表指针。
不足的地方是胖指针的设计比较呆板,它只能有一个虚表指针(或大小),即胖指针本身占用的空间是固定的,否则静态代码无法生成。如果要添加点什么其他信息呢?这就不是 rust 内置的胖指针设计能够满足的。
Sized 特征约束:
特殊尺寸的类型:
当使用指针操作的时候,这些特殊性是需要考虑的。
对齐: #[repr(C)]
按 C 语言对齐。
let mut a = String::from("aaa");
(*a).push('w') --> String::push(&mut *a, 'w')
,所以并不会导致移动引用对象的所有权相关 api:
api | & T --> U | T --> & U | T --> &mut U |
---|---|---|---|
new | new(&T)->U | ||
Clone | clone(&T)->U | ||
ToOwned | to_owned(&T)->&U | ||
Deref | Deref(&T)->&U | Deref(&mut T)->&mut U | |
DerefMut | Deref(&mut T)->&mut U | ||
AsRef | AsRef(&T)->&U | ||
AsMut | AsMut(&mut T)->&mut U | ||
Borrow | Borrow(&T)->&U | ||
BorrowMut | BorrowMut(&mut T)->&mut U | ||
Cow | to_mut(&mut T)->&mut U |
Box<T>
: 最基础的智能指针,在堆上分配对象,对对象拥有所有权类型转换种类:
强制转换(coercion)这个术语有点怪,因为它实际上类似其他语言中的自动转换或者隐式转换:
Deref<T>
--> &TDerefMut<T>
-->&mut T使用 as 的转换(显式转换 Explicit coercions):
* T --> * U
: U 不是动态类型(即不是胖指针)* dyn T -x-> * dyn U
: 动态类型指针之间不能直接转换,会判断虚表不一致&[T;n] --> *const T
函数的类型关系有点复杂:
let f3:&dyn Fn() = &bar
; 这是特征对象,函数实现了这个特征,函数指针也实现了这个根据规则,只要将引用转换成对应的指针格式,然后指针可以无限制的转换成任意指针 *T, 在 unsafe 上下文利用反引用得到任意非动态大小类型 T,或者直接转成整数。
万能转换公式:
unsafe{ &*t }
,注意指针 --> 引用的转换是不存在的,但可以通过反引用和取引用运算来得到(哪怕中间过程无效也没关系,比如对特征对象反引用是无效的,但 &*
可以)let a:&dyn Fn()->i32 = &||3;
let a:*const dyn Fn()->i32 = a;
let a:fn()->i32 = unsafe { *(a as *const fn()->i32) };
a.type_id(); //因为 fn() : Any
let a:&dyn Any = &a;
a.type_id();
// 注意这段代码没有实际意义,因为 any 不解析特征对象
隐式和显式转换都是安全的(注意:转换到指针是安全的,访问指针指向对象即反引用操作才是不安全的)。
非安全转换:
mem::transmute::<T,U>(T)->U
Box<T>
Rc<T>
Arc<T>
Pin<T>
UnsafeCell<T>
PhantomDate<T>
安全边界一节已经提到大概不安全的操作,明显有很多算是比较日常的需求,按借用规则也是做不到的。如果什么都要由程序员来进入非安全边界去实现,这样这门语言本身就变得不那么可靠了。
要避免程序员出现错误,最好的方法就是提供更优秀的基础类库,比如数据结构之类的。而 rust 因为安全边界比较严格,所以它要提供一些基础措施来解决一些日常开发需求。
rust 的方案就是: 内部可变性。
解决:
方案:
Rc<RefCell<i32>>
Arc<Mutex<i32>>
这里面是个组合:
借用的局限性:
RC 智能指针模拟了借用的共享特性,如: RC<T>
,T 是其指向(也是其管理)的对象,Rc::clone(&self) 将返回新的指针,不过都是指向同一个对象 T。但是要注意 &self,是只读借用,那么理论上它是没有修改自己。如果只是复制一个新的指针,a 指针和 b 指针怎么协调它背后共同管理的 T 对象呢? a, b 怎么知道啥时候该释放 T 呢?答案:
Rc 是一个内部可变类型。所以 &self 并不阻止它内部发生了修改。
Rc 的原理是用引用计数来统计 T 的引用个数,到 0 的时候就释放 T。
同样的, RefCell 也是内部可变,它模拟了借用规则,返回 Ref 智能指针,和 RefMut 智能指针。在其内部维护借用规则的动态实施。
这一顿操作下来有什么意义?
fn f1(a:&'static i32, b:&mut i32){
*b = 1;
}
fn f2(a:Rc<RefCell<i32>>, b:Rc<RefCell<i32>>){
*b.borrow_mut() = 1;
}
fn f(){
let mut a = 0;
f1 (&a, &mut a); // 做不到
let a = Rc::<RefCell<i32>>::new(RefCell::<i32>::new(0));
f2 (a.clone(), a.clone()); // 轻松秒杀
println!("{}", a.borrow());
}
也许以上代码让你一头雾水,但是它要表达的意思就是,接口可以更灵活。
将本来在编译器检查的问题,推迟到运行期。这样的好处,在于读写的操作,写写的操作,不会被编译器就地否决,而变为了可能,从而将编译器问题变为业务逻辑问题。
虽然同步代码也许意义不大,但是对异步代码来说,这是很有必要的。因为异步代码的接口就是非常严格。
分析一下 f1 接口,借用规则下,是不能同时存在两个访客,a,b 拥有对同一个对象的读写权的,那么 f2 是否违反了借用规则?其实并不是,它只是将检测放到了函数内部,在函数内部你可以先写,也可以先读,但是读和写是有顺序的,写之前关闭读,读之前关闭写。这样来实现读写的目的。
从函数整体来看他是实现了两个访客同时拥有读写接口,但从内部逻辑来看它还是遵从借用规则。写写逻辑也是同样的道理。
能做到这点的根本原因是什么?就是 a,b 都是一个内部可写类型,内部封装了不安全代码,它能在需要的时候改写只读的对象,但又在外部保持安全的外观和 rust 的其他安全代码兼容。
代价是什么?
所以,除非你真的需要同时传递“读”和“写”两种借用出去,否则应该调整逻辑,自己安排读写顺序和逻辑,才能有效利用编译器的排错。
当然,在复杂的异步环境下,这个可能是个奢望。
特征:
哪些能跨线程传递?
// & mut
impl<'_, T> Send for &'_ mut T
where
T: Send + ?Sized
// &
impl<'_, T> Send for &'_ T
where
T: Sync + ?Sized
哪些能跨线程共享?
编译器会在符合安全条件下自动实现这两个特征。
那究竟怎样的条件才是符合? rust 安全界限内的产品,都是符合的。它实际上是借用规则的翻版。
可见同步规则都没有违反借用规则。也就是借用规则和同步规则是类似的。那为什么还需要定义同步规则呢(Send / Sync)?
这是因为我们之前引入了内部可变性类型,这些类型中,有一部分是不支持同步的。他们在穿越线程时,会有问题。比如: Rc, RefCell。另外,其他不安全组件,如原生指针,或者封装了不安全代码却没有对跨线程优化的组件,也是不支持跨线程的。
也就是如果经过对跨线程精心优化的不安全组件,它大可以通过标记 Send、Sync 来表明支持同步。这就是 rust 类库的做法,也是为什么需要标记的原因。
问题二:为什么需要封装成智能指针,而不用原生的借用?
这就要从生命周期去理解,线程什么时候消亡,这个是很难确定的,也很难去安排不同线程的读写顺序,就算安排好了,颗粒度也是比较粗糙的,并发性能会降低。也就是在多线程环境下,往往都是要求你同时拥有读写线程的,不能说等到读线程完毕,再来启动写线程。因此静态的借用规则就完全没有用了,只能依赖智能指针在运行时动态执行借用规则。
不过好消息是:借用规则永远值得信赖!只要借用规则执行到位,不管是静态还是动态,它实际都是安全的。这并不影响 rust 安全语言的承诺。
如果成员指向自身,这类结构称为自引用结构。这类结构一般都是个麻烦,但是现实中却有用。rust 中的生成器和异步函数(async) 会产生一个自引用的内部结构。
此类结构在交换和移动对象指针的时候,会产生 bug,原本是引用自身,生命周期和自己是相同的,但是如果指向外部,那么生命周期就不相同了。也就是,编译器对其生命周期的假设就没有原本的对自引用结构的假设那么强。而生成器和异步函数的实现很依赖对自引用结构生命周期的假设。
为了解决这个问题,Pin 能够固定指针,让上下文不能安全的访问 &mut T 指针,从而避免无意间的改变自引用结构。
对于非自引用结构,自动实现了 UnPin 特征,所以 Pin 对其无效(因为操作指针是安全的,所以禁止操作没有任何意义)。对于特定的自引用结构,用户可以实现 !UnPin 特征,从而让 Pin 机制得以生效。
对于栈对象,Pin 固定是非安全操作(unsafe),因为固定后的对象会随着栈释放而丢失,所以它不是一种强烈的保证,可能隐含 bug。对于堆对象,Pin 固定是安全操作。
此处可能存在不合适展示的内容,页面不予展示。您可通过相关编辑功能自查并修改。
如您确认内容无涉及 不当用语 / 纯广告导流 / 暴力 / 低俗色情 / 侵权 / 盗版 / 虚假 / 无价值内容或违法国家有关法律法规的内容,可点击提交进行申诉,我们将尽快为您处理。