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

2023-09-13 17:22:18

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<RefCell<>>或Arc<Mutex<>>,如果后面有遇到,再来和大家分享。

更多推荐

Golang中的GMP调度模型

GMP调度模型Golang调度器的由来单进程时代不需要调度器1.单一的执行流程,计算机只能一个任务一个任务处理。2.进程阻塞所带来的CPU时间浪费。后来操作系统就具有了最早的并发能力:多进程并发,当一个进程阻塞的时候,切换到另外等待执行的进程,这样就能尽量把CPU利用起来,CPU就不浪费了多进程/线程时代有了调度器需求

C++ PrimerPlus 复习 第七章 函数——C++的编程模块(上)

第一章命令编译链接文件make文件第二章进入c++第三章处理数据第四章复合类型(上)第四章复合类型(下)第五章循环和关系表达式第六章分支语句和逻辑运算符第七章函数——C++的编程模块(上)本章重要点注意函数指针,const指针参数。其他的其实都简简单单第七章函数——C++的编程模块(上)函数基本知识;函数原型(函数声明

【数据结构】AVL树的删除(解析有点东西哦)

文章目录前言一、普通二叉搜索树的删除1.删除结点的左右结点都不为空2.删除结点的左结点为空,右节点不为空3.删除结点的右结点为空,左节点不为空4.删除结点的左右结点都不为空二、AVL树的删除1.删除结点,整棵树的高度不变化1.1parent的平衡因子在删除结点之前为01.1.1删除结点为parent的左节点1.1.2删

就只说 3 个 Java 面试题

在面试时,即使是经验丰富的开发人员,也可能会发现这是一些很棘手的问题:1、Java中“transient”关键字的用途是什么?如何才能实现这一目标?在Java中,“transient”关键字用于指示类的特定字段不应包含在对象的序列化形式中。这意味着当对象被序列化时,其状态将转换为可以写入文件或通过网络发送的字节序列。通

Mybatis学习笔记10 高级映射及延迟加载

Mybatis学习笔记9动态SQL_biubiubiu0706的博客-CSDN博客无论简单映射(前面所学的单表和对象之间的映射关系)还是高级映射说到底都是java对象和数据库表记录之间的映射关系准备数据库表:一个班级对应多个学生.班级表:t_class学生表:s_stu(自增)新建模块项目整体结构pom.xml<?xm

深度学习——卷积神经网络

卷积神经网络1计算机视觉(ComputerVision)2边缘检测示例(EdgeDetectionExample)3更多边缘检测内容(MoreEdgeDetectionExample)4Padding5卷积步长(StridedConvolutions)6三维卷积(ConvolutionsOverVolumes)7单层卷

汽油辛烷值的测定 马达法

声明本文是学习GB-T503-2016汽油辛烷值的测定马达法.而整理的学习笔记,分享出来希望更多人受益,如果存在侵权请及时联系我们8试剂和标准物8.1气缸夹套冷却液若实验室所处海拔的水沸点为100℃±1.5℃(212F±3F),应使用水作为气缸夹套冷却液。当实验室海拔高度不确定时,应使用添加商用乙二醇防冻剂的水溶液,加

面向Java开发者的ChatGPT提示词工程(8)

GPT是一种强大的自然语言处理技术,能够对文本进行深入分析,实现多种任务,如提取标签、识别实体、理解情感等。在传统的机器学习工作流程中,若要分析一段文本的情感,首先需要收集带有标签的数据集,然后训练模型,接着探索如何在云端部署模型并进行推断。虽然这种方法可能取得不错的效果,但其工作流程较为繁琐。此外,对于每个任务(如情

ChatGPT扇动翅膀后带来的蝴蝶效应

对于蝴蝶效应最常见的阐述是:“一只南美洲亚马逊河流域热带雨林中的蝴蝶,偶尔扇动几下翅膀,可以在两周以后引起美国得克萨斯州的一场龙卷风。”简介肯尼亚essay正文论文代写之都为什么是肯尼亚?蝴蝶效应简介在印象中贫穷且落后的东非国家肯尼亚,几乎承包了全球的英文essay代写业务。肯尼亚肯尼亚共和国(TheRepublico

面向Java开发者的ChatGPT提示词工程(6)

在使用GPT构建应用程序时,我们通常不会直接使用第一次写的提示词,而是通过不断迭代来改进它们,以找到最适合我们想要实现的任务的提示词。虽然第一次写的提示词可能会有一定的可用性,但最重要的是找到适合你的应用程序的提示词的过程,而不是第一个提示是否有效。因此,我们需要不断地尝试和改进,才能找到最佳的提示词。使用GPT构建应

MySQL基础—从零开始学习MySQL

01.MySQL课程介绍_哔哩哔哩_bilibili1、MySQL安装以管理员身份运行cmdnetstartmysql80netstopmysql80客户端连接1).方式一:使用MySQL提供的客户端命令行工具2).方式二:使用系统自带的命令行工具执行指令mysql[-h127.0.0.1][-P3306]-uroot

热文推荐