Part 3: Methods, Options, References and Head Nodes

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 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:

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. This explains exactly which strings are stored and referenced and what the differences are.

1
2
3
4
5
#[derive(Debug)]
struct Person {
    name: &str,
    friends: Option<Vec<&str>>,
}

Here the compiler gives us an error:

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

1
2
3
4
5
#[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:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
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:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
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:

1
2
3
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:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
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:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
...
// 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:

1
2
...
println!("{} has friends: {:?}", max.name, max.friends);

Error message:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
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.

0%