如果要我说新手rust入门的第一个坑, 我会说是struct和所有权的问题。当在方法上定义self是否具有可变性时, 它是针对struct中所有属性, 要不全部不变, 要不全部都不变, 这导致不能直接照搬以前在有gc语言的oop写法, 否则一定会遇到不少关于所有权报错, 说你方法, 用了不变的, 但又用了可变。我写了一个月左右rust, 这问题让我抓狂了不少, 网上也搜过解决方案, 但依然不能让我满意。这里我分享一下我对struct定义需要注意的总结。我不敢说自己一定是对的, 最起码是能解决我遇到的问题。
1. 不要随便定义 Option属性
当你在struct中定义option, 表示strcut的属性间一定是有生命周期不一致的问题, 这意味着接下来你得要处理一堆空指针问题, 要去写不少match
, if let
去解决。所以当你定义option属性, 最好认真思考这属性的生命週期有多长, 如果是很短, 只是某几个方法用到, 还不如不要放在属性, 拆出来使用还更好, 如果是好多地方用到, 那不妨想一下能否在struct生成实例时定义好。
2. 不要有嵌套struct
我们在一般oop语言里, 经常会有嵌套属性出现, 这在一般oop语言是没什么问题的, 但在rust里, 就很容易导致可变性污染情况出现。所谓可变性污染指的是, 方法里调用的某个方法会改变属性, 导致调用该方法的方法也得变成可变。这我觉得算是rust的一个坑, 它竟然不能指定某个属性是可变, 而是要整个struct设定, 无语啊。我自己想到的解决方案是尽可能减少嵌套属性, 或用RefCell
来定义嵌套属性, 然后这个struct里所有方法都是self, 尽可能减少可变的区域。举一个例子:
定义两个struct:
#[derive(Debug)]
struct Foo {
arr: Vec<i32>,
}
struct Bar {
can_assign: bool
}
这里我随便定义两个struct, 但要点是struct不能直接有嵌套的struct, 如果我们希望它们有依赖交互, 可以另外定义一个struct, 然后用RefCell
来封装它们:
struct Both {
foo: RefCell<Foo>,
bar: RefCell<Bar>
}
这样就能做到内部不变性, 不会因为某个方法可变, 导致所有都要变。把方法补上吧:
impl Foo {
fn new() -> Self {
Self { arr: Vec::new() }
}
fn push_arr(&mut self, num: i32) {
self.arr.push(num);
}
}
impl Bar {
fn new() -> Self {
Self { can_assign: false }
}
fn get_assign(&self) -> bool {
self.can_assign
}
fn set_assign(&mut self, can_assign: bool) {
self.can_assign = can_assign;
}
}
impl Both {
fn new() -> Self {
Self {
foo: RefCell::new(Foo::new()),
bar: RefCell::new(Bar::new()),
}
}
fn push_and_print(&self, num: i32) {
if self.bar.borrow().get_assign() {
self.foo.borrow_mut().push_arr(num);
println!("{:?}",self.foo.borrow().arr);
}
}
fn set_assign(&self, can_assign: bool) {
self.bar.borrow_mut().set_assign(can_assign)
}
}
通过refcell
和struct封装, 就能保証Both
所有方法都可以直接用self就可以调用, 如果是涉及改变属值, 就尽可能减少可变区域范围。这里要注意的是, 不要RefCell一把梭, 而是在最大范围内用RefCell封装好, 不然就一堆panic错误没处理了。其实这就类似面向接口编程的思维, 在定义struct时, 先想好它是要处理什么的, strcut与struct之间都是调用方法交互, 不要去理会struct里面的细节。
3. 简单的struct加个#[derive(Clone)]吧
所谓简单的strcut, 指的是没有嵌套的struct。这一点我不知道是对还是错, 只是我觉得加了挺方便, 有时想读取某个属性, 但又想修改它, 会导致有所有权问题, 如果直接clone
, 就没问题了, 我是觉得这是保持代码不变性的好利器。