Rust的所有权问题,在我学Rust的时候就跳过了,因为我知道这玩意儿没有场景就不好理解。没想到场景很快就出现了。
在开发Yew应用组件的时候,涉及到了事件,闭包,自然就引出了所有权问题。
话不多说,下面让我们直接进入代码场景,去体验并了解Rust的所有权机制吧。
下面这段代码是能够正常工作的。这段代码的逻辑意图也很简单,这是一个函数式的编辑组件,在这个组件中,有一个保存按钮。当用户点击保存按钮时,触发handle_save事件,在handle_save事件中,获取input和textarea的值,并将其通过在Props上定义的on_save Callback,传递给外部组件。
rust
#[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或者其它内存管理机制照料。因此,我的第一版代码写出来是这个样子。
rust
#[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的语言特色了,但是还是错了,编译器给出错误提示如下
bash
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的所有权机制。
第一个提示
bash
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)]
。
第二个提示
bash
value moved into closure here
因为有move关键字,因此在闭包中,所有的变量的所有权都会被移动,包括title_ref。问题来了,如果我们把move关键字移除掉,又会是什么提示呢?
让我们开一个小差,把代码修改成下面这个样子。
rust
let handle_save = Callback::from(|_| {
let title: String = if let Some(title) = title_ref.cast::<HtmlInputElement>() {
title.value()
} else {
"none".to_string()
};
});
编译器的提示和报错如下
bash
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关键字的建议。所以,我们又回到之前的那个讨论。
第三个提示
rust
variable moved due to use in closure
这个提示告诉我们,变量title_ref的所有权被移动是因为这一行代码上使用了该变量。不得不说,这个提示很精确。
第四个报错
rust
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,以失败告终。
因此,我们把代码修改成下面这样,就不会报错了。
rust
#[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
哇)
rust
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<>>,如果后面有遇到,再来和大家分享。