• [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>
    
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37

    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()
                };
            });
    
        ...
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    在这段代码中,直接在闭包中使用了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`.
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

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

    第一个提示

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

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

    第二个提示

    value moved into closure here
    
    • 1

    因为有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()
            };
        });
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    编译器的提示和报错如下

    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 |_| {
       |                                      ++++
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25

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

    第三个提示

    variable moved due to use in closure
    
    • 1

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

    第四个报错

    value used here after move
    
    • 1

    这是一个报错信息,在终端上显示的是红色,但是在这里没有把红色显示出来。这个信息告诉我们,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()
            };
        });
        ...
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    然后,我参考了一下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()
                };
            })
        };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

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

  • 相关阅读:
    迅为IMX6Q开发板QT系统移植tinyplay
    Python中两种网络编程方式:Socket和HTTP协议
    Maven3.6的下载和安装
    人人都会Kubernetes(二):使用KRM实现快速部署服务,并且通过域名发布
    LeetCode 每日一题 2022/11/7-2022/11/13
    uniapp 跳转返回携带参数(超好用)
    【分割链表】
    C-数据结构-平衡二叉树
    静态代理和动态代理的区别是什么
    机器学习(四十五):使用Flask构建机器学习web
  • 原文地址:https://blog.csdn.net/firefox1/article/details/132854876