[2023.09.13]: Rust Lang, the inevitable ownership issue

The ownership issue of Rust was skipped when I was learning Rust because I knew it would be difficult to understand without scenarios. Unexpectedly, the scene appeared soon.
When developing Yew application components, events and closures are involved, which naturally leads to ownership issues.
Without further ado, let’s go directly into the code scene to experience and understand Rust’s ownership mechanism.

The code below works fine. The logical intention of this code is also very simple. This is a functional editing component. In this component, there is a save button. When the user clicks the save button, the handle_save event is triggered. In the handle_save event, the values of the input and textarea are obtained and passed to the external component through the on_save Callback defined on the Props.

#[function_component(Editor1)]
pub fn editor1(props: & amp;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="Save" onclick={<!-- -->handle_save}/>
            </div>
            <div class="content">
                 <textarea value={<!-- -->props.data.content.clone()} ref={<!-- -->content_ref}/>
            </div>
        </div>

    }
}

Although Rust’s ownership mechanism is not easy to understand at first, its compiler can find problems in the code at compile time. So, there is no need for us to complain. In Rust, as long as the code is compiled, it is good code, hahaha.
We all know that in programming languages that do not talk about ownership mechanisms, variables are used more casually, and they will be taken care of by GC or other memory management mechanisms. Therefore, my first version of the code looked like this.

#[function_component(Editor1)]
pub fn editor1(props: & amp;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()
            };
        });

    ...
}

In this code, the title_ref variable is used directly in the closure. Although it is already a Rust language feature, it is still wrong. The compiler gives the following error message:

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`.

The Rust compiler gives us a detailed explanation. In particular, you can see a more detailed explanation of ownership by running rustc --explain E0382.
Let me explain the compiler prompts and errors one by one so that we can better understand Rust’s ownership mechanism.

first tip

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

title_ref is a variable of type yew::NodeRef that does not implement the Copy trait. The key here is that “Copy trait is not implemented”. In our previous experience, when encountering problems related to the Copy trait, we only needed to call its .clone() method to solve it. If it does not have a clone method, add #[derive(Clone)] to its type definition.

second tip

value moved into closure here

Because of the move keyword, the ownership of all variables in the closure will be moved, including title_ref. The question is, if we remove the move keyword, what will be the prompt?
Let’s take a break and modify the code to look like this.

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

The compiler prompts and errors are as follows:

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 |_| {<!-- -->
   | + + + +

This tip tells us that the life cycle of the closure may exceed the life cycle of the current function, but the title_ref variable is borrowed in the closure. That is to say, it is possible that the current function is destroyed, but the closure still exists. It is definitely illogical to borrow title_ref in the closure at this time, so the compiler immediately rejected this code and finally gave the suggestion to add the move keyword. So, we go back to the previous discussion.

third tip

variable moved due to use in closure

This tip tells us that the ownership of the variable title_ref was moved because it was used on this line of code. I have to say, this tip is very accurate.

The fourth error report

value used here after move

This is an error message. It is displayed in red on the terminal, but the red color is not displayed here. This message tells us that the ownership of title_ref has been moved and title_ref can no longer be used.
This error may be a bit surprising to students who have switched from other development languages. It’s like the property in my name has been moved around, and in the end it’s no longer mine. Who am I to talk to for explanation?
But that’s Rust’s “property” management rules. Fortunately, its compiler is quite reasonable and clearly explains the error (fourth) and how the error is caused (first to third).
Therefore, to use title_ref in a closure, we have to clone it.
It should be noted that if this object has implemented the Copy trait, the compiler will automatically generate the code to call copy, and there is no need to explicitly call clone(). But most of our structs can only implement the Clone trait through #[derive(Clone)]. In most cases, obtaining the Copy trait through #[derive(Copy)] will fail because the fields inside, such as the String type, do not support the Copy trait.
Therefore, we modify the code to the following, and no error will be reported.

#[function_component(Editor1)]
pub fn editor1(props: & amp;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()
        };
    });
    ...
    }
}

Then, I referred to some official example codes of Yew. They elegantly handled this ownership transfer problem through blocks (in my opinion, at least the variable names are less disturbing, but this way of writing is not found in other languages. Recommended, because this is a naked name shadowwow)

 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()
            };
        })
    };

Okay, I will stop talking about the ownership mechanism of the Rust language here for now. This is just the tip of the iceberg. I hope I can explain it clearly to everyone. I heard that there are more advanced usages, about Rc> or Arc>. If you encounter them later, I will share them with you.