织梦更换网站模板,巩义移动网站建设,简易博客网站模板下载,宜春建设局官方网站先序文章请看#xff1a; 面向C程序员的Rust教程#xff08;一#xff09;
所有权与移动语义
要说Rust语言跟其他语言最大的区别#xff0c;那笔者觉得非数这个所有权和移动语义莫属。
深浅复制
对于绝大多数语言来说#xff0c;变量/对象之间的赋值通常都是复制语义。…先序文章请看 面向C程序员的Rust教程一
所有权与移动语义
要说Rust语言跟其他语言最大的区别那笔者觉得非数这个所有权和移动语义莫属。
深浅复制
对于绝大多数语言来说变量/对象之间的赋值通常都是复制语义。例如C中
void Demo() {Obj o1; // 对象1auto o2 o1; // 复制语义o2是o1的复制
}只不过深复制还是浅复制需要进一步研究。C中由于完全支持栈上部署自定义类型以及自定义的拷贝构造/赋值函数程序员需要自行判断内部指针/引用关系决定使用深复制或是浅复制。
一些语言是把「结构体」和「类」做区分结构体仅用于做数据聚合部署在栈上而类则添加更多OO特性部署在堆上然后栈上给一个指针。比如说Swift和C#就是如此。那么这种情况下栈上部署的类型复制就为深复制而堆上部署的类型复制就为浅复制。
还有一些语言索性不允许自定义类型在栈上部署比如java、OC那么这种情况下也就是限定了默认的复制均为浅复制例如下面OC的例子
void Demo() {Object *o1 [[Object alloc] init];Object *o2 o1; // 由于栈上只有指针因此复制一定是浅复制
}总之统一的原则都是「栈上做深复制」所以如果栈上是完整数据那么就是深复制如果栈上只有指针/引用那么就是浅复制。
rust移动语义
但Rust非常特殊他根本不在这里纠结深复制还是浅复制的问题而Rust默认为「移动语义」而非「复制语义」。当然这只针对自定义类型来说对于整数、浮点数这些它仍然是简单的值复制。我们来看一个例子
fn main() {let mut a 5;let b a;a 10;println!({},{}, a, b); // 10,5
}这种基本类型看上去无可厚非但如果换成自定义类型结果可能大大超出预期
struct Test {a: i32,b: i32
}fn main() {let mut t Test{a: 1, b: 2};let t2 t;t.a 8; // ERROR
}我们会发现在尝试更改t.a的时候编译报错了报错信息如下
意思就是说我们尝试去操作了一个已经被移动的变量t。换句话说let t2 t;这一行语句隐含了「移动语义」。
由于Test是自定义类型因此它会被部署在堆上main函数栈中的t则是它的一个指针。之后我们把t赋值给t2的时候相当于把「对象的所有权」「转移」给了t2也就是说赋值之后t2成为了指向原始对象的指针同时t不可以再被使用。
如果和C做对比大致上可以等价于下面的代码
struct Test {int32_t a;int32_t b;
};int main() {Test *t new Test(1, 2); // 自定义类型部署在堆上auto t2 t; // 所有权转交t nullptr; // 原始指针废弃return 0;
}当然事实上还是有一些区别的比如说C中这里的t仍然可以复用而rust中它就是完全不可再用的状态除非定义重影这个语法后续章节详细讨论。
对于一些C程序员来说可能会把rust的这种「移动语义」与C中的「移动语义」混淆甚至可能认为「rust的赋值相当于自带std::move」但其实并非如此一来std::move是为了触发移动构造/赋值函数从而触发浅复制而rust的赋值中根本没有任何复制的语义而是「所有权转交」二来std::move并不能使原本的指针失效但rust中的赋值是可以的这一点希望读者一定要区分。
如果一定要与C的语法做对比rust的行为倒是更加符合std::unique_ptr的行为unique_ptr不可复制只可移动移动时转交对象所有权原本的指针清空
void Demo() {auto t std::make_uniqueTest(1, 2); // 对象部署在堆中栈上用指针指向auto t2 std::move(t); // 赋值时做所有权转交// 这时t已经被清空了不再指向原始对象t-a 8; // ERROR
}当然rust的机制更先进一些一个是它不用套壳不需要理解所谓智能指针和std::move的概念二来如果对已经释放的指针做操作报错是在编译阶段而如果是C的unique_ptr例如上面例程报错则是在运行阶段而且报的是解空指针错误。
Rust的一个世界观
相信很多读者会对rust的所有权转交这一机制非常不适应甚至非常不解。那么这里我们就不得不讨论一下Rust的一个重点世界观就是手Rust希望「尽可能在编译阶段发现和避免更多的潜在问题」。也就是说Rust它不希望程序问题留给运行期而是在编译期就把可能会出现的一些错误都发现或者干脆避免掉。
因此每当我们发现一些Rust奇怪的限制或机制的时候都应当思考这样限制所希望避免的问题。下面用C来举几个例子读者可以体会一下传统的复制语义在这里会出现的问题
示例1
void f1(Obj obj) {// 使用obj做一些事情
}void Demo() {Obj pre_obj;f1(pre_obj); // 构造pre_obj只是为了传给f1// 后面也不会使用pre_obj
}上面这种场景下我们在Demo中构造pre_obj只是为了传给f1使用但如果f1使用了复制语义那么就会平白多一次无意义的复制如果Obj类型比较大或者是拷贝构造比较复杂那么这里的效率就会很低。
示例2
void f1(Obj obj) { // 右值引用类型希望强制获取所有权// 使用obj做一些事情
}void Demo() {Obj pre_obj;f1(std::move(pre_obj));// 照理说后边不可以再使用pre_obj但这是软约束pre_obj.set_xxx(yyy); // OK不会报错
}上面这个例子中尽管我们用了右值引用「企图」让外界传参时把obj的「所有权」交给函数内部但在C中这种移动语义是一种软约束如果不小心在外界操作了pre_obj仍然是合法的。
示例3
class Test {public:Test(int a): pa_(new int(a)) {}~Test() {delete pa_;}private:int *pa_;
};void Demo() {Test t1(1);Test t2 t1;
} // 析构时出现重复delete问题上面这个例子中我们实现Test类虽然遵从了构造时new析构时delete的原则但却没有考虑到复制语义的问题由于t2是t1的一个浅复制因此在函数结束时t1和t2都会对同一片堆空间进行delete。
Rust的世界观中为了避免这些乱七八糟的内存分配和释放问题干脆直接在语义上杜绝了这种影响。首先自定义类型只能部署在堆空间就不存在浅复制的问题其次栈上的变量同时只能有一个持有对象也不会存在重复释放的问题最后由于栈变量和堆对象是1对1的关系那么他们的生命周期可以做强绑定也就是说当栈变量释放时所持有的堆空间就进行析构。
struct Point {x: f32,y: f32
}fn Demo() {let p1 Point{x: 0.5, y: 1.2}; // p1持有对象let p2 p1; // p2持有对象p1不再可用
} // p2生命周期结束对象同时释放上例中由于Point对象只能被一个变量持有当p1交接给p2后p1就跟这个对象没关系了。后面当p2结束时自然也不会有其他变量持有这个对象当然可以放心把它释放。
所以看出来了吗Rust为什么不需要垃圾回收机制也不需要什么引用计数器就能做到避免内存泄漏或者重复释放答案很简单因为它根本不允许多重引用。
借用
上一节我们讲解了Rust中自定义类型的所有权问题相信大家应该能够意识到这种语言特性在很多场景下是很不方便的。
举例来说在一个程序流程中我需要先检验一下输入的参数是否合法然后再对数据做一些处理。比如说
struct Data {dt1: i32,dt2: u32
}fn check_args(dt: Data)-bool {// 判断dt1和dt2要非0dt.dt1 ! 0 dt.dt2 ! 0
}fn main() {let mut dt Data{dt1: 1, dt2: 3};// 先检查数据if !check_args(dt) {// 一些处理} else {// 后续逻辑dt.dt1 5; // ERROR}
}如果按照上面这种写法在检查完参数以后这个dt的所有权就转交了然后在check_args函数结束后就被释放了这显然是不符合预期的。同时编译也会报错。
但仔细分析这种场景这里有一个非常重要的特点就是说在check_args中dt相当于只读不会对其做任何更改。那么也就是说check_args的调用不会改变dt的值而且因为只是做检查因此原本的dt后续还需要使用的。
那么这种场景下并不应当「转交所有权」而是应当「借用」一下dt。所谓「借用」形象来说就相当于借别人东西你只是在借用的过程中可以使用而已但东西还是人家的用完了要还回去并且你使用的过程中不能损坏。
C解决这个问题的办法是常引用做参数这样一来不用复制二来内部不可改变。
// 用常引用解决问题
bool check_args(const Data dt) {return dt.dt1 ! 0 dt.dt2 ! 0;
}int main() {Data dt {1, 3};if (!check_args(dt)) {// ... } else {// ...dt.dt1 5;}return 0;
}无独有偶Rust中解决这个问题的办法也是利用引用而且是不可变引用。
fn check_args(dt: Data)-bool {// 判断dt1和dt2要非0dt.dt1 ! 0 dt.dt2 ! 0
}fn main() {let mut dt Data{dt1: 1, dt2: 3};// 先检查数据if !check_args(dt) { // 注意传参时要显式取引用// 一些处理} else {// 后续逻辑dt.dt1 5; // OK}
}前面章节我们已经初步介绍过引用他有点像C中引用和指针的结合体所以这里用作引用传参时也一定要注意要显式用表示取引用这一点与C不同。
【未完更新中……】