Rust - Struct設計注意總結

188 阅读3分钟

如果要我说新手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, 就没问题了, 我是觉得这是保持代码不变性的好利器。