This repository has been archived on 2024-11-25. You can view files and clone it, but cannot push or open issues or pull requests.
m3tam3re.com/content/posts/rust-3.en.md
2023-10-12 14:01:05 +02:00

216 lines
7.6 KiB
Markdown
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

---
title: "Part 3: Methods, Options, References and Head Nodes"
date: 2020-09-22
draft: false
series: ["Starting with rust"]
tags: ["rust","coding"]
---
For the last 2 days and today I have dealt with various topics.
Since I somehow slipped one topic into another, it makes little sense
made to write individual posts. I had to tie the head knot first
solve that went along with the various topics 😇.
I've put together several examples that reflect quite well that
what I've learned in the past few days. But let's just start.
## Rust, referencing and ownership
I don't want to go into too much detail here. I think the [Rust book](https://doc.rust-lang.org/book/ch04-00-understanding-ownership.html) can explain all of this in much better detail than I can. Just this much: What you know as a pointer from languages like Go or C ++ works differently in Rust. With its Owenership System, Rust ensures that every asset has only one owner. However, values can still be referenced. To do this, they are "borrowed" and then returned to the owner.
Let's just start with an example. You can find the complete code [here](https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=d5e36c9e573201e0843b0e947b033ab9):
### Lifetimes
In my example I created a data structure for a person. This person should have a name and a list of friends. I defined the name as & str string slice, which is a reference to the storage location. If you are interested in details about strings, you should read [this article](https://blog.thoughtram.io/string-vs-str-in-rust/). This explains exactly which strings are stored and referenced and what the differences are.
```rust
#[derive(Debug)]
struct Person {
name: &str,
friends: Option<Vec<&str>>,
}
```
Here the compiler gives us an error:
```rust
error[E0106]: missing lifetime specifier
--> src/main.rs:3:11
|
3 | name: &str,
| ^ expected named lifetime parameter
|
help: consider introducing a named lifetime parameter
|
2 | struct Person<'a> {
3 | name: &'a str,
|
error[E0106]: missing lifetime specifier
--> src/main.rs:4:25
|
4 | friends: Option<Vec<&str>>,
| ^ expected named lifetime parameter
|
help: consider introducing a named lifetime parameter
|
2 | struct Person<'a> {
3 | name: &str,
4 | friends: Option<Vec<&'a str>>,
|
```
Rust wants a lifetime specification. This is because Rust doesn't know how long the & str references should be valid. By default, the memory is released in Rust after leaving a block "{}" and this would invalidate these references. We use the lifetimes to tell the Rust compiler how long the references should be valid:
```rust
#[derive(Debug)]
struct Person<'a> {
name: &'a str,
friends: Option<Vec<&'a str>>,
}
```
We declare a lifetime a for our data structure here. We also assign these to the & str reference of the name and the elements of the friends list. By doing this, we are telling the compiler that the fields should exist as long as the person exists.
When implementing the methods, the compiler also requests lifetimes again:
```rust
impl<'a> Person<'a> {
fn new(name: &'a str, friends: Vec<&'a str>) -> Self {
let mut fl: Option<Vec<&str>> = None;
if friends.len() > 0 {
fl = Some(friends);
}
Self {
name,
friends: fl
}
}
fn add_friends(&mut self, friend: &'a str) {
let fl = self.friends.as_mut(); // This needs to be borrowed as mutable!
match fl {
Some(f) => f.push(friend),
None => self.friends = Some(vec![friend]),
};
println!("Friend '{}' added.", friend);
}
}
```
There are only two simple methods here. new creates a new person and is not strictly a method. The second method adds friends to the friends list.
### Match and if let
The friends structure field is an option field, i.e. the field can either contain a list of friends or nothing (None). If you want to use the content of the field, you usually have to distinguish whether the field is occupied or not. Help match:
```rust
fn main() {
// Peter has no friends
let peter = Person {
name: "Peter",
friends: None,
};
println!("{:?}", peter);
let peter_has_friends = &peter.friends; //this needs to be borrowed because peter.friends is needed later on
match peter_has_friends {
Some(_) => println!("{} has friends.", peter.name),
None => {},
}
}
```
In the event that friends contains data, Peter has friends is output, otherwise nothing happens.
The same thing can be achieved more compactly by replacing the match block with if let:
```rust
if let Some(_peter_has_friends) = peter.friends {
println!("{} has friends!", peter.name);
}
```
Both variants lead to the same result. In this case, however, there are no friends. If the option field is occupied for a person, there is also an output:
```rust
fn main() {
// Mary has friends
let mary = Person {
name: "Mary",
friends: Some(vec!["Paul", "Jerry"]),
};
println!("{:?}", mary);
let mary_has_friends = &mary.friends; //this needs to be borrowed because mary.friends is needed later on
match mary_has_friends {
Some(_) => println!("{} has friends.", mary.name),
None => {},
}
// Instead of the match block "if let" is a shorter alternative
if let Some(_mary_has_friends) = &mary.friends {
println!("{} has friends!", mary.name);
}
}
```
Here, too, the use of if let is more compact, although I personally find that the match block gives a clearer understanding of what is being done here.
## Referencing and changing data
Let's say we want to change a friend in the example above. In this case the friends list must be mutable / changeable:
```rust
...
// Let'try max
let mut max = Person::new("Max", vec![]);
max.add_friends("Bobby");
max.add_friends("Batman");
println!("{:?}", max);
if let Some(has_friends) = max.friends.as_mut() {
has_friends[1] = "Superman";
println!("{} has friends: {:?}", max.name, has_friends);
}
// This could also be written like this:
if let Some(mut has_friends) = max.friends {
has_friends[1] = "Batgirl";
println!("{} has friends: {:?}", max.name, has_friends);
}
...
```
By using the as_mut () method, our friend list is passed to had_friends as mutable and we can change the 2nd element.
But if we do that, another problem arises. has_friends becomes the new owner of the friends list. That means, if we want to access max.friends, the compiler reports an error:
```rust
...
println!("{} has friends: {:?}", max.name, max.friends);
```
Error message:
```rust
error[E0382]: borrow of moved value: `max.friends`
--> src/main.rs:77:48
|
73 | if let Some(mut has_friends) = max.friends {
| --------------- value moved here
...
77 | println!("{} has friends: {:?}", max.name, max.friends);
| ^^^^^^^^^^^ value borrowed here after move
|
= note: move occurs because value has type `std::vec::Vec<&str>`, which does not implement the `Copy` trait
help: borrow this field in the pattern to avoid moving `max.friends.0`
|
73 | if let Some(ref mut has_friends) = max.friends {
| ^^^
```
The compiler tells us that the max_friends content has been moved to has_friends. The compiler also states that the Vector copy trait will not be implemented.
At the same time, the compiler suggests part of the solution. We have to lend max.friends to has_friends.