Vorige week dronk ik mijn eerste kopje Rust. Ik heb concepten geleerd die vreemd zijn in de talen die ik ken: eigendom, lenen, en levens. Deze week wil ik de tweede beker drinken en zien waar het me toe leidt.
Zelfverwijzende typen
Het is tijd om nog een oefening uit het eerste examen te implementeren:
Gegeven een verzameling van
Super
, retourneer de subcollectie van degenen die een sidekick hebben.
We moeten het model wijzigen om a . toe te voegen sidekick
attribuut van type Super
.
De implementatie lijkt eenvoudig genoeg.
pub struct Super<'a> {
pub super_name: &'a str,
pub real_name: &'a str,
pub power: u16,
pub sidekick: Option<Super<'a>>,
}
Helaas compileert de bovenstaande code niet:
error[E0072]: recursive type `Super` has infinite size
--> src/model.rs:2:1
|
2 | pub struct Super<'a> {
| ^^^^^^^^^^^^^^^^^^^^ recursive type has infinite size
...
6 | pub sidekick: Option<Super<'a>>,
| ----------------- recursive without indirection
|
help: insert some indirection (e.g., a `Box`, `Rc`, or `&`) to make `Super` representable
|
6 | pub sidekick: Box<Option<Super<'a>>>,
|
Het lijkt erop dat een type in Rust niet naar zichzelf kan verwijzen. Laten we de suggestie van de compiler volgen. Een “eenvoudige” oplossing is om een referentie te gebruiken in plaats van een type.
pub struct Super<'a> {
pub super_name: &'a str,
pub real_name: &'a str,
pub power: u16,
pub sidekick: &'a Option<Super<'a>>,
}
Terwijl het type compileert, doen de tests dat niet:
error[E0515]: cannot return value referencing temporary value
--> src/tests/samples.rs:5:5
|
5 | / Super {
6 | | super_name: "Batman",
7 | | real_name: "Bruce Wayne",
8 | | power: 50,
9 | | sidekick: &Some(robin()),
| | ------------- temporary value created here
10 | | }
| |_____^ returns a value referencing data owned by the current function
Terug naar af. De compiler hintte ook op het gebruik van Box
.
Alle waarden in Rust worden standaard toegewezen aan de stapel. Waarden kunnen in een kader worden geplaatst (toegewezen op de heap) door a . te maken
Box<T>
. Een doos is een slimme verwijzing naar een heap toegewezen waarde van het typeT
. Wanneer een doos buiten bereik raakt, wordt zijn destructor genoemd, wordt het innerlijke object vernietigd en wordt het geheugen op de hoop bevrijd.
Hier is de nieuwe code die gebruikt Box
:
pub struct Super<'a> {
pub super_name: &'a str,
pub real_name: &'a str,
pub power: u16,
pub sidekick: Box<Option<Super<'a>>>,
}
Hiermee wordt alles gecompileerd, inclusief de testvoorbeelden!
pub(in crate::tests) fn batman<'a>() -> Super<'a> {
Super {
super_name: "Batman",
real_name: "Bruce Wayne",
power: 50,
sidekick: Box::from(Some(robin())), // 1
}
}
- Gebruik de om een variabele te “boxen”
Box.from()
functie.
Ten slotte is de oplossing eenvoudig:
pub mod j {
use crate::model::Super;
pub fn find_supers_with_sidekicks<'a>(supers: &'a Vec<Super<'a>>) -> Vec<&Super<'a>> {
supers
.iter() // 1
.filter(|&s| s.sidekick.is_some()) // 2
.collect() // 3
}
}
- Herhaal de items van de vector.
- Houd alleen degenen die een sidekick hebben.
- Verzamel de resterende items.
Het testen van de oplossing brengt geen nieuwe inzichten in de taal.
Eigenschappen, eigenschappen, eigenschappen overal!
De volgende oefening luidt als volgt:
Groepeer de sidekicks van
Super
in twee groepen:
- Een bevat de sidekicks van helden,
Super
` met deAlignment.GOOD
; de andere die van schurken, metAlignment.EVIL`
.- De `Kaart` bevat twee sleutels,
Alignment.GOOD
enAlignment.EVIL
.- De waarden zijn respectievelijk de set van helden’ sidekicks en de set van schurken’.
- Nee
null
waarden worden geaccepteerd in de ingestelde waarden.- De afwezigheid van een sidekick voor a
Super
zou geen uitzondering moeten maken looptijd.
Alignment
lijkt me een goede kandidaat voor een enum
type. Gelukkig biedt Rust inderdaad opsommingen. We moeten het model dienovereenkomstig bijwerken:
Het vertaalt zich in het volgende:
pub struct Super<'a> {
pub super_name: &'a str,
pub real_name: &'a str,
pub power: u16,
pub sidekick: Box<Option<Super<'a>>>,
pub alignment: Alignment,
}
pub enum Alignment {
Good, Neutral, Evil
}
De logica zelf is een vouwen. Het goede nieuws is dat Iter
verschaft een fold
functie. Laten we beginnen met de volgende code:
pub fn group_sidekicks_by_alignment<'a>(supers: &'a Vec<Super<'a>>) -> HashMap<Alignment, Vec<&'a Super<'a>>> {
let mut map = HashMap::new(); // 1
map.insert(Good, Vec::new()); // 2
map.insert(Evil, Vec::new()); // 2
supers
.iter()
.filter(|&s| s.sidekick.is_some()) // 3
.fold(map, |mut map, s| { // 4
let value = map.entry(s.alignment).or_default(); // 5
value.push(&s.sidekick.unwrap()); // 6
map // 7
})
}
- Maak de hash-kaart van het resultaat.
- Stel de sleutels en standaardwaarden in.
- Uitfilteren
Super
zonder hulpje. - Vouwen!
- Verkrijg de vector die overeenkomt met de uitlijning van de sidekick.
- Voeg de sidekick toe aan de vorige vector.
- Vergeet niet de kaart terug te sturen.
Zoals verwacht mislukt het:
error[E0277]: the trait bound `model::Alignment: Eq` is not satisfied
--> src/solutions.rs:29:17
|
29 | map.insert(Good, Vec::new());
| ^^^^^^ the trait `Eq` is not implemented for `model::Alignment`
De sleutels van een HashMap
vergeleken hoeven te worden. Uit de documentatie:
Het is vereist dat de sleutels de implementeren
Eq
enHash
eigenschappen, hoewel dit vaak kan worden bereikt door gebruik te maken van#[derive(PartialEq, Eq, Hash)]
. Als u deze zelf implementeert, is het van belang dat de volgende eigenschap geldt:
k1 == k2 -> hash(k1) == hash(k2)
Met andere woorden, als twee sleutels gelijk zijn, moeten hun hashes gelijk zijn.
Hoewel dat logisch is, zou ik verwachten dat opsommingen de to Eq
en Hash
eigenschappen standaard. Dat is niet het geval. Laten we ze expliciet toevoegen.
#[derive(Debug, PartialEq, Eq, Hash)]
pub enum Alignment {
Good, Evil,
}
De volgende compileerfout is de volgende:
error[E0507]: cannot move out of `s.alignment` which is behind a shared reference
--> src/solutions.rs:35:39
|
35 | let value = map.entry(s.alignment).or_default();
| ^^^^^^^^^^^ move occurs because `s.alignment` has type `model::Alignment`, which does not implement the `Copy` trait
Het is eenvoudig genoeg om de . toe te voegen Copy
eigenschap om Alignment
. Eerlijk gezegd begrijp ik niet waarom Rust het een zet vindt. Je moet ook implementeren Clone
.
Clone
is een supereigenschap vanCopy
, dus alles wat isCopy
moet ook implementerenClone
.
#[derive(Debug, PartialEq, Eq, Hash, Copy, Clone)]
pub enum Alignment {
Good, Evil,
}
Soorten verplaatsen
De volgende fout is:
error[E0507]: cannot move out of `*s.sidekick` which is behind a shared reference
--> src/solutions.rs:36:29
|
36 | value.push(&s.sidekick.unwrap());
| ^^^^^^^^^^
| |
| move occurs because `*s.sidekick` has type `Option<Super<'_>>`, which does not implement the `Copy` trait
| help: consider borrowing the `Option`'s content: `s.sidekick.as_ref()`
Deze fout is wat moeilijker direct op te lossen. Laten we eens kijken naar de betrokken typen:
Variabele | Type |
---|---|
s |
&Super |
sidekick |
Box<Option<Super>> |
value |
Vec<&Super> |
Gezien het bovenstaande moeten we:
- Pak de
Super
. - Pak de
sidekick
attribuut verpakt in eenOption
, die zelf is verpakt in eenBox
. Herinnering: de vorige stap heeft alle eruit gefilterdSuper
wie heeft er geen sidekick. - zet een referentie naar de sidekick in de
value
.
Hier is een (de?) oplossing:
value.push((*s.sidekick).as_ref().unwrap());
Laten we ontleden in stappen om beter te begrijpen wat er gebeurt:
Stap | Uitdrukking | Type |
---|---|---|
1 | s.sidekick |
Box<Option<Super>> |
2 | *s.sidekick |
Option<Super> |
3 | (*s.sidekick).as_ref() |
Option<&Super> |
4 | (*s.sidekick).as_ref().unwrap() |
&Super |
Nitpick: maak een kaart op een functionele manier
Op dit punt wordt alles gecompileerd (en uitgevoerd). Toch heb ik een hekel aan de manier waarop ik de HashMap
. Het gaat om veranderlijkheid:
let mut map = HashMap::new();
map.insert(Good, Vec::new());
map.insert(Evil, Vec::new());
Ik ben voorstander van een meer functionele onveranderlijke manier. Hier is het:
let map = [Good, Evil]
.iter()
.cloned()
.map(|alignment| (alignment, Vec::new()))
.collect();
Er is waarschijnlijk een manier om beide te “zippen” Alignment
kaart en de Super
vector, maar ik moet toegeven dat ik zippen minder leesbaar vind.
Conclusie
Deze week dronk ik de tweede kop Rust, en ik overleefde. Deze stap voelde minder vreemd aan dan de week ervoor: een goed teken. Blijf op de hoogte voor andere berichten over mijn Rust-leerreis!
De volledige broncode voor dit bericht is te vinden op Github.
Om verder te gaan:
Oorspronkelijk gepubliceerd op Een Java Geek op 6 junidit, 2021.
CreditSource link