Teil 3: Methoden, Optionen, Verweise und Kopfknoten

Die letzten 2 Tage und auch heute habe ich mich mit verschiedenen Themen auseinandergesetzt. Da ich irgendwie einem Thema ins andere gerutscht bin, hat es wenig Sinn gemacht einzelne Beiträge zu schreiben. Ich musste erst einmal den Kopfknoten lösen, der mit den verschiedenen Themen einher ging 😇.

Ich habe mehrere Beispiele zusammengestellt, die ganz gut das wiederspiegeln, was ich in den letzten Tagen gelernt habe. Aber fangen wir einfach mal an.

Rust, Referenzierung und Ownership

Ich will jetzt hier nicht zu weit ins Detail gehen. Ich denke, dass das Rust Buch das alles viel besser im Detail erklären kann als ich. Nur soviel: Was man aus Sprachen wie Go oder C++ als Pointer kennt funktioniert in Rust anders. Rust sorgt durch sein Owenership System dafür, dass jeder Wert nur einen einzigen Besitzer hat. Werte können aber dennoch refrenziert werden. Dazu werden sie “ausgeliehen” und anschließend an den Besitzer zurückgegeben.

Fangen wir einfach mal mit einem Beispiel an. Den kompletten Code findet ihr hier:

Lifetimes

Ich habe in meinem Beispiel eine Datenstruktur für eine Person erstellt. Diese Person soll einen Namen und eine Liste Freunden haben. Den Namen habe ich als &str string slice definiert, was eine Referenz auf den Speicherort ist. Wer sich für Details zuma Strings interessiert sollte diesen Artikel lesen. Hier wird genau erklärt die Strings gespeichert und referenziert werden und was die UNterschiede sind.

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

Hier liefert uns der Compiler einen Fehler:

 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 möchte eine Lifetime Spezifizierung haben. Das leigt daran, dass Rust nicht weiß wie lange die &str Verweise gültig sein sollen. Standardmäßig wird in Rust nach verlassen einen Blocks “{}” der Speicher freigegeben und damit würden diese Verweise ungültig. Über die Lifetimes teilen wir dem Rust Compiler mit wie Lange die Verweise gültig sein sollen:

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

Wir deklarieren hier eine Lifetime a für unsere Datenstruktur. Diese weisen wir auch dem &str Verweis des Namens und den Elementen der Freundesliste zu. Dadurch sagen wir dem Compiler, dass die Felder so lange existieren sollen, wie die Person existiert.

Bei der Implementierung der Methoden fordert der Compiler ebenfalls wieder Lifetimes:

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

Hier gibt es nur zwei einfache Methoden. new erstellt eine neue Person und ist genau genommen keine Methode. Die zweite Methode fügt Freunde zur Freundesliste hinzu.

match und if let

Das Strukturfeld friends ist ein Feld vom Typ Option, d.h. das Feld kann entweder eine Liste mit Freunden enthalten oder nichts (None). Wenn man den Inhalt des Feldes nutzen möchte muss man meistens unterscheiden ob das Feld belegt ist oder nicht. Dabei hilf 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 => {},
    }
}

Für den Fall, dass friends Daten enthält wird Peter has friends ausgegeben, andernfalls passiert gar nichts.

Das Gleiche erreicht man kompakter indem man den match Block durch if let ersetzt:

1
2
3
if let Some(_peter_has_friends) = peter.friends {
        println!("{} has friends!", peter.name);
    }

Beide Varianten führen zum gleichen Ergebnis. In diesem Fall sind aber keine Freunde vorhanden. Ist bei einer Person das Optionsfeld belegt erfolgt auch eine Ausgabe:

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

Auch hier ist die Verwendung if let kompakter, wobei ich persönlich finde, dass das der match Block klarer zu verstehen gibt, was hier getan wird.

Referenzierung und Veränderung Daten

Nehmen wir mal an wir möchten in dem oben stehenden Beispiel einen Freund ändern. In diesem Fall muss die Freundesliste mutable / änderbar sein:

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

Durch die Verwendung der Methode as_mut() wird unsere Freundesliste an had_friends als mutable übergeben und wir können das 2. Element ändern. Wenn wir das so machen tritt aber ein anderes Problem auf. has_friends wird neuer Eigentümer der Freundesliste. D.h., wenn wir auf max.friends zugreifen wollen meldet der Compiler einen Fehler:

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

Fehlermeldung:

 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 {
   |                 ^^^

Der Compiler sagt uns, dass der Inhalt max_friends zu has_friends verschoben wurde. Außerdem gibt der Compiler an, dass der Copy Trait vom Typ Vector nicht implementiert wird. Gleichzeitig schlägt uns der Compiler einen Teil der Lösung vor. Wir müssen max.friends an has_friends ausleihen.

0%