Enums e Pattern Matching: Il Sistema dei Tipi Pokemon¶
Livello: 🟢 Principiante
Concetti trattati: Enums, Pattern Matching, Type Safety, Exhaustiveness Tempo di lettura: ~15 minuti
Il Problema¶
Pokemon ha 18 tipi diversi (Fire, Water, Grass, Electric, ecc.) e ogni tipo ha vantaggi e svantaggi contro altri tipi. Come modelliamo questo sistema in modo type-safe ed elegante?
In molti linguaggi useresti stringhe o costanti numeriche. In Rust, usiamo enums!
Soluzione Naive: Stringhe ❌¶
Vediamo perché le stringhe non sono ideali:
struct Pokemon {
name: String,
pokemon_type: String, // ❌ Problematico!
}
fn create_charmander() -> Pokemon {
Pokemon {
name: String::from("Charmander"),
pokemon_type: String::from("Fire"), // Typo? "fire"? "FIRE"?
}
}
fn is_fire_type(pokemon: &Pokemon) -> bool {
pokemon.pokemon_type == "Fire" // Fragile!
}
Problemi¶
- ❌ Typo: "Frie" invece di "Fire" compila ma è sbagliato
- ❌ Case sensitivity: "fire" vs "Fire"
- ❌ No autocompletamento IDE
- ❌ Impossibile verificare tutti i casi gestiti
- ❌ Performance: confronto stringhe più lento
Soluzione Rust: Enums ✅¶
Rust ci permette di definire un tipo che può essere esattamente uno tra un set di valori:
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PokemonType {
Normal,
Fire,
Water,
Grass,
Electric,
Ice,
Fighting,
Poison,
Ground,
Flying,
Psychic,
Bug,
Rock,
Ghost,
Dragon,
Dark,
Steel,
Fairy,
}
struct Pokemon {
name: String,
pokemon_type: PokemonType, // ✅ Type-safe!
}
Vantaggi Immediati¶
- ✅ Impossibile avere typo: Solo i valori definiti sono validi
- ✅ Autocompletamento: L'IDE suggerisce tutti i tipi
- ✅ Type checking: Il compilatore verifica la correttezza
- ✅ Performance: Enum è un semplice integer in memoria
- ✅ Semantica chiara: Il codice documenta se stesso
Pattern Matching: Calcolo dell'Efficacia¶
Ora arriva la parte interessante: calcolare se una mossa è super efficace, non molto efficace, o immune.
Pattern Matching Base¶
fn calculate_effectiveness(
attacking_type: PokemonType,
defending_type: PokemonType,
) -> f32 {
use PokemonType::*;
match (attacking_type, defending_type) {
// Super efficace (2.0x)
(Fire, Grass) => 2.0,
(Fire, Ice) => 2.0,
(Fire, Bug) => 2.0,
(Fire, Steel) => 2.0,
(Water, Fire) => 2.0,
(Water, Ground) => 2.0,
(Water, Rock) => 2.0,
(Grass, Water) => 2.0,
(Grass, Ground) => 2.0,
(Grass, Rock) => 2.0,
// Resistente (0.5x)
(Fire, Fire) => 0.5,
(Fire, Water) => 0.5,
(Fire, Rock) => 0.5,
(Fire, Dragon) => 0.5,
(Water, Water) => 0.5,
(Water, Grass) => 0.5,
(Water, Dragon) => 0.5,
// Immune (0.0x)
(Normal, Ghost) => 0.0,
(Ghost, Normal) => 0.0,
(Electric, Ground) => 0.0,
(Ground, Flying) => 0.0,
// Normale (1.0x) - tutti gli altri casi
_ => 1.0,
}
}
Cosa Succede Qui?¶
use PokemonType::*- Importa tutte le varianti per scrivereFireinvece diPokemonType::Firematch (attacking_type, defending_type)- Pattern matching su una tupla di due enums(Fire, Grass) => 2.0- Se attacco Fire contro difesa Grass, 2x danni_ => 1.0- Wildcard: tutti gli altri casi sono 1x (efficacia normale)
Exhaustiveness Checking: Il Superpotere del Compilatore¶
Una delle feature più potenti di Rust è che il compilatore verifica che tu gestisca tutti i casi.
Esempio: Aggiungere un Nuovo Tipo¶
Immagina di aggiungere un nuovo tipo Pokemon:
Cosa succede? Il compilatore ti avvisa di ogni funzione che usa pattern matching incompleto:
fn get_type_color(pokemon_type: PokemonType) -> &'static str {
match pokemon_type {
PokemonType::Fire => "red",
PokemonType::Water => "blue",
PokemonType::Grass => "green",
// ... altri tipi ...
}
// ❌ ERROR: non-exhaustive patterns: `Cosmic` not covered
}
Zero Bug Guarantee
Con gli enum, è impossibile dimenticare di gestire un caso. Il compilatore non ti lascia compilare!
Caso Reale da Rustmon¶
Ecco come Rustmon implementa il sistema completo:
// Estratto da rustmon/src/types.rs
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Type {
Normal, Fire, Water, Grass, Electric, Ice,
Fighting, Poison, Ground, Flying, Psychic,
Bug, Rock, Ghost, Dragon, Dark, Steel, Fairy,
}
impl Type {
/// Calcola il moltiplicatore di efficacia di questo tipo
/// quando attacca un tipo difensore
pub fn effectiveness_against(self, defender: Type) -> f32 {
// Tabella completa delle efficacie
match (self, defender) {
// Fire
(Type::Fire, Type::Grass | Type::Ice | Type::Bug | Type::Steel) => 2.0,
(Type::Fire, Type::Fire | Type::Water | Type::Rock | Type::Dragon) => 0.5,
// Water
(Type::Water, Type::Fire | Type::Ground | Type::Rock) => 2.0,
(Type::Water, Type::Water | Type::Grass | Type::Dragon) => 0.5,
// Grass
(Type::Grass, Type::Water | Type::Ground | Type::Rock) => 2.0,
(Type::Grass, Type::Fire | Type::Grass | Type::Poison |
Type::Flying | Type::Bug | Type::Dragon | Type::Steel) => 0.5,
// Electric
(Type::Electric, Type::Water | Type::Flying) => 2.0,
(Type::Electric, Type::Electric | Type::Grass | Type::Dragon) => 0.5,
(Type::Electric, Type::Ground) => 0.0, // Immune!
// ... tutti gli altri 14 tipi ...
_ => 1.0, // Efficacia normale
}
}
}
Pattern Avanzati: Combinare Valori¶
Nota l'uso di | (OR) per combinare pattern:
(Type::Fire, Type::Grass | Type::Ice | Type::Bug | Type::Steel) => 2.0,
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
// Fire è super efficace contro uno qualsiasi di questi
Questo è equivalente a:
(Type::Fire, Type::Grass) => 2.0,
(Type::Fire, Type::Ice) => 2.0,
(Type::Fire, Type::Bug) => 2.0,
(Type::Fire, Type::Steel) => 2.0,
Ma molto più conciso e leggibile!
🔗 Vedi il codice completo su GitHub
Enum con Dati: Mosse Pokemon¶
Gli enum in Rust possono anche contenere dati:
pub enum Move {
Physical {
name: String,
power: u32,
move_type: PokemonType,
},
Special {
name: String,
power: u32,
move_type: PokemonType,
},
Status {
name: String,
effect: StatusEffect,
},
}
enum StatusEffect {
Burn,
Poison,
Paralysis,
Sleep,
}
Pattern Matching con Dati¶
fn calculate_damage(pokemon_move: &Move, attacker: &Pokemon) -> u32 {
match pokemon_move {
Move::Physical { power, move_type, .. } => {
// Usa statistica Attack
(attacker.attack * power) / 50
}
Move::Special { power, move_type, .. } => {
// Usa statistica Special Attack
(attacker.special_attack * power) / 50
}
Move::Status { .. } => {
// Mosse status non fanno danno
0
}
}
}
Destructuring¶
Il .. nel pattern significa "ignora gli altri campi":
Comparazione con Altri Linguaggi¶
Esercizi Pratici¶
Esercizio 1: Weather System¶
Implementa un sistema meteo per Pokemon:
enum Weather {
Sunny,
Rain,
Sandstorm,
Hail,
}
fn weather_boost(weather: Weather, pokemon_type: PokemonType) -> f32 {
// Sunny: Fire type gets 1.5x
// Rain: Water type gets 1.5x
// Sandstorm: Rock type gets 1.5x defense
// Hail: Ice type is immune to damage
todo!()
}
Soluzione
Esercizio 2: Dual Type Pokemon¶
Molti Pokemon hanno due tipi. Implementa:
enum TypeSlot {
Single(PokemonType),
Dual(PokemonType, PokemonType),
}
fn calculate_effectiveness_dual(
attacking_type: PokemonType,
defender_types: TypeSlot,
) -> f32 {
// Hint: con Dual, moltiplica l'efficacia di entrambi i tipi
todo!()
}
Soluzione
fn calculate_effectiveness_dual(
attacking_type: PokemonType,
defender_types: TypeSlot,
) -> f32 {
match defender_types {
TypeSlot::Single(type1) => {
calculate_effectiveness(attacking_type, type1)
}
TypeSlot::Dual(type1, type2) => {
let eff1 = calculate_effectiveness(attacking_type, type1);
let eff2 = calculate_effectiveness(attacking_type, type2);
eff1 * eff2 // Esempio: 2.0 * 0.5 = 1.0
}
}
}
Derive Macros: Funzionalità Gratuite¶
Notato il #[derive(...)] sopra l'enum?
Queste derive macros generano automaticamente implementazioni:
| Derive | Funzionalità |
|---|---|
Debug |
Permette println!("{:?}", pokemon_type) |
Clone |
Permette .clone() per duplicare |
Copy |
Permette copie implicite (enum è piccolo) |
PartialEq |
Permette == e != |
Eq |
Garantisce equivalenza totale |
Hash |
Permette uso in HashMap/HashSet |
Copy vs Clone
Copy è permesso solo per tipi piccoli che possono essere copiati velocemente (come gli enum semplici).
Clone è più generale ma richiede chiamata esplicita .clone().
Punti Chiave da Ricordare 💡¶
Takeaways
- Enums: Tipo che può essere esattamente una variante tra un set
- Type Safety: Impossibile creare valori invalidi
- Pattern Matching: Gestisci tutti i casi in modo esplicito
- Exhaustiveness: Il compilatore verifica che tu gestisca tutti i casi
- Enums con Dati: Ogni variante può contenere dati diversi
- Performance: Enums sono efficienti come integer
Quando Usare Enums¶
✅ Usa enums quando:
- Hai un set finito di possibilità (tipi, stati, comandi)
- Vuoi type safety invece di stringhe/numeri magici
- Vuoi che il compilatore ti aiuti a non dimenticare casi
- Vuoi pattern matching espressivo
❌ Non usare enums quando:
- Il set di valori è dinamico o illimitato
- I valori arrivano da input esterno (usa parsing con validation)
Prossimi Passi¶
Continua il tuo viaggio:
- ← Ownership in Pratica - Gestione memoria sicura
- Traits → - Comportamenti comuni e polimorfismo
Approfondimenti¶
Hai domande? Apri una discussion su GitHub!