[2023.09.13]: Rust Lang,避不开的所有权问题

Rust的所有权问题,在我学Rust的时候就跳过了,因为我知道这玩意儿没有场景就不好理解。没想到场景很快就出现了。
在开发Yew应用组件的时候,涉及到了事件,闭包,自然就引出了所有权问题。
话不多说,下面让我们直接进入代码场景,去体验并了解Rust的所有权机制吧。

下面这段代码是能够正常工作的。这段代码的逻辑意图也很简单,这是一个函数式的编辑组件,在这个组件中,有一个保存按钮。当用户点击保存按钮时,触发handle_save事件,在handle_save事件中,获取input和textarea的值,并将其通过在Props上定义的on_save Callback,传递给外部组件。

#[function_component(Editor1)]
pub fn editor1(props: &Props) -> Html {
    let title_ref = use_node_ref();
    let content_ref = use_node_ref();
    let Props { on_save, .. } = props;

    let handle_save = {
        let title_ref = title_ref.clone();
        let content_ref = content_ref.clone();
        let on_save = on_save.clone();
        Callback::from(move |_| {
            let title: String = if let Some(title) = title_ref.cast::<HtmlInputElement>() {
                title.value()
            } else {
                "none".to_string()
            };
            let content: String = if let Some(content) = content_ref.cast::<HtmlTextAreaElement>() {
                content.value()
            } else {
                "none".to_string()
            };
            on_save.emit(EditorData { title, content })
        })
    };
    html! {
        <div class="editor">
            <div class="title">
                <input type="text" ref={title_ref}/>
                <Button text="保存" onclick={handle_save}/>
            </div>
            <div class="content">
                 <textarea value={props.data.content.clone()} ref={content_ref}/>
            </div>
        </div>

    }
}

Rust的所有权机制最开始虽然不太好理解,但是它的编译器能够在编译时把代码的问题找出来。所以,我们也没有必要抱怨。在Rust中,只要是编译通过的代码都是好代码,哈哈哈。
我们都知道,在不讲所有权机制的编程语言中,变量的使用都比较随意,它们都会被GC或者其它内存管理机制照料。因此,我的第一版代码写出来是这个样子。

#[function_component(Editor1)]
pub fn editor1(props: &Props) -> Html {
    let title_ref = use_node_ref();
    let content_ref = use_node_ref();

    let handle_save = Callback::from(move |_| {
            let title: String = if let Some(title) = title_ref.cast::<HtmlInputElement>() {
                title.value()
            } else {
                "none".to_string()
            };
        });

    ...
}

在这段代码中,直接在闭包中使用了title_ref变量,虽然已经很有Rust的语言特色了,但是还是错了,编译器给出错误提示如下

error[E0382]: use of moved value: `title_ref`
  --> src/components/editor1.rs:45:41
   |
31 |     let title_ref = use_node_ref();
   |         --------- move occurs because `title_ref` has type `yew::NodeRef`, which does not implement the `Copy` trait
...
34 |     let handle_save = Callback::from(move |_| {
   |                                      -------- value moved into closure here
35 |         let title: String = if let Some(title) = title_ref.cast::<HtmlInputElement>() {
   |                                                  --------- variable moved due to use in closure
...
45 |                 <input type="text" ref={title_ref}/>
   |                                         ^^^^^^^^^ value used here after move

For more information about this error, try `rustc --explain E0382`.

Rust的编译器给我们做了详细的解释,特别的,运行rustc --explain E0382还能看到关于所有权的更加详细的解释。
让我来逐一解读编译器提示及报错,以便让我们更好的了解Rust的所有权机制。

第一个提示

move occurs because `title_ref` has type `yew::NodeRef`, which does not implement the `Copy` trait

title_ref是一个类型为yew::NodeRef且没有实现Copy trait的变量。这里面“没有实现Copy trait”很关键。在我们之前的经历中,遇到Copy trait相关的问题,只需要调用它的.clone()方法就可以解决。如果它没有clone方法,就在它的类型定义上加上#[derive(Clone)]

第二个提示

value moved into closure here

因为有move关键字,因此在闭包中,所有的变量的所有权都会被移动,包括title_ref。问题来了,如果我们把move关键字移除掉,又会是什么提示呢?
让我们开一个小差,把代码修改成下面这个样子。

    let handle_save = Callback::from(|_| {
        let title: String = if let Some(title) = title_ref.cast::<HtmlInputElement>() {
            title.value()
        } else {
            "none".to_string()
        };
    });

编译器的提示和报错如下

error[E0373]: closure may outlive the current function, but it borrows `title_ref`, which is owned by the current function
  --> src/components/editor1.rs:34:38
   |
34 |     let handle_save = Callback::from(|_| {
   |                                      ^^^ may outlive borrowed value `title_ref`
35 |         let title: String = if let Some(title) = title_ref.cast::<HtmlInputElement>() {
   |                                                  --------- `title_ref` is borrowed here
   |
note: function requires argument type to outlive `'static`
  --> src/components/editor1.rs:34:23
   |
34 |       let handle_save = Callback::from(|_| {
   |  _______________________^
35 | |         let title: String = if let Some(title) = title_ref.cast::<HtmlInputElement>() {
36 | |             title.value()
37 | |         } else {
38 | |             "none".to_string()
39 | |         };
40 | |     });
   | |______^
help: to force the closure to take ownership of `title_ref` (and any other referenced variables), use the `move` keyword
   |
34 |     let handle_save = Callback::from(move |_| {
   |                                      ++++

这个提示告诉我们,闭包的生命周期可能超出了当前函数的生命周期,但是闭包中借用了title_ref变量。也就是所可能当前函数销毁了,但是闭包还存在。这个时候在闭包中借用title_ref肯定是不合乎逻辑的,因此编译器立即拒绝了这段代码,最后给出了添加move关键字的建议。所以,我们又回到之前的那个讨论。

第三个提示

variable moved due to use in closure

这个提示告诉我们,变量title_ref的所有权被移动是因为这一行代码上使用了该变量。不得不说,这个提示很精确。

第四个报错

value used here after move

这是一个报错信息,在终端上显示的是红色,但是在这里没有把红色显示出来。这个信息告诉我们,title_ref的所有权已经被移动了,不能再使用title_ref了。
这个报错对于从其它开发语言转过来的同学,可能有点匪夷所思。就好像我名下的财产,弄来弄去,最后变成不是我了,我找谁说理去哇。
但这就是Rust的“财产”管理规则。还好它的编译器比较讲道理,把错误(第四)以及这个错误是怎么形成的(第一到第三)都给你说清楚了。
因此,要在闭包中使用title_ref,我们得clone一下。
需要注意的是,如果这个对象已经实现了Copy trait,编译器会自动生成调用copy的代码,就没有必要显示的调用clone()了。但我们的大部分struct只能通过#[derive(Clone)]来实现Clone trait。而通过#[derive(Copy)]来获取Copy trait,大部分情况,由于里面的字段,例如String类型不支持Copy trait,以失败告终。
因此,我们把代码修改成下面这样,就不会报错了。

#[function_component(Editor1)]
pub fn editor1(props: &Props) -> Html {
    let title_ref = use_node_ref();
    let content_ref = use_node_ref();

    let title_ref1 = title_ref.clone();
    let handle_save = Callback::from(move |_| {
        let title: String = if let Some(title) = title_ref1.cast::<HtmlInputElement>() {
            title.value()
        } else {
            "none".to_string()
        };
    });
    ...
    }
}

然后,我参考了一下Yew官方的一些例子代码,他们通过block的方式,优雅的处理了这个所有权转移问题(在我看来,至少变量名称干扰少了,但是这种写法,在其它语言中不推荐哇,因为这个是一个赤裸裸的name shadow哇)

    let handle_save = {
        let title_ref = title_ref.clone();
        Callback::from(move |_| {
            let title: String = if let Some(title) = title_ref.cast::<HtmlInputElement>() {
                title.value()
            } else {
                "none".to_string()
            };
        })
    };

好了,关于Rust语言的所有权机制,我暂时就讲到这里,这只是它的冰山一角,希望给大家讲清楚了。听说还有更高级的用法,关于Rc>或Arc>,如果后面有遇到,再来和大家分享。

你可能感兴趣的:(Rust,开发每日一篇,rust,算法,开发语言)