Lezione 3 di 7 · 19 min di lettura
Squadre, archivi e classifiche
Array e liste di struct. Inizializzarli, modificare un elemento, la trappola della copia nel foreach, cercare, e ordinare secondo un criterio tuo passando una funzione a sort.
Tanti giocatori
Una struct da sola è utile; una collezione di struct è il cuore di quasi ogni programma: gli studenti di una classe, i prodotti di un negozio, i nemici di un livello. E non serve niente di nuovo: array, slice e liste funzionano con le struct esattamente come con gli int.
struct Player
{
String name;
int score;
}Player[*] team = {
{ "Ada", 72 },
{ "Grace", 42 },
{ "Linus", 20 },
};
io::printn(team[1].name);
team[2].score += 5;GraceL’inizializzazione è un array di inizializzazioni: un paio di graffe esterne per l’array, e dentro un paio per ogni giocatore. Per raggiungere un campo si combinano indice e punto: team[2].score è “il punteggio del giocatore di indice 2”.
Dettagli nerd Come sta un array di struct in memoria?
Esattamente come un array di numeri: gli elementi uno dopo l’altro, senza spazi in mezzo. Ogni Player occupa 24 byte (16 della String e 4 dell’int, più 4 di imbottitura per l’allineamento, come hai visto nel riquadro della prima lezione), quindi team è un blocco di 72 byte e team[2] comincia al byte 48. Il calcolo dell’indirizzo è sempre lo stesso: inizio + indice × dimensione dell’elemento.
Stampare una collezione di struct
Con gli int bastava io::printn(team). Con le struct, sorpresa:
io::printn(team);[<struct type:5a5a40449bb8, addr:7ffc87b30cb0>, <struct type:5a5a40449bb8, addr:7ffc87b30cc8>, <struct type:5a5a40449bb8, addr:7ffc87b30ce0>]printn sa stampare una struct da sola, ma dentro un array o una lista si arrende e mostra solo tipo e indirizzo di ogni elemento. Poco male: è l’occasione perfetta per un metodo.
fn void Player.print(self) => io::printfn("%-8s %3d", self.name, self.score);foreach (p : team) p.print();Ada 72
Grace 42
Linus 25Così decidi tu anche il formato: nome allineato a sinistra su 8 caratteri, punti a destra su 3.
La copia nel foreach, di nuovo
Nel modulo 2 lo avevamo visto con gli int: la variabile del foreach è una copia. Con le struct la trappola è ancora più facile da far scattare:
foreach (p : team) p.score = 0;
io::printn(team[0].score);72Hai azzerato tre copie. Per modificare gli elementi veri serve la &, come nel modulo 2:
foreach (&p : team) p.score += 1; // one bonus point each
foreach (p : team) p.print();Ada 73
Grace 43
Linus 26C’è una bella differenza rispetto agli int, però: con le struct non serve l’asterisco. Con un int scrivevi *v += 1; con una struct basta p.score += 1, perché il punto sa arrivare al campo anche attraverso un indirizzo (lo stesso motivo per cui in un metodo con &self scrivi self.width).
Quiz
Dopo foreach (p : team) p.score *= 2; seguito da foreach (&p : team) p.score += 1;, se team[0].score valeva 10, quanto vale ora?
Liste di struct
Una List di struct si riempie con push, passando la struct tra graffe (per posizione o per nome):
List{Player} players;
players.push({ "Linus", 20 });
players.push({ "Zoe", 90 });
players.push({ "Ada", 72 });
players.push({ .name = "Barbara", .score = 55 });
players[0].score += 5;players[0].score += 5 modifica davvero l’elemento della lista: l’indice di una lista, come quello di un array, porta all’elemento vero. Funziona anche con i metodi &self: players[0].add(5) cambierebbe il giocatore nella lista. Solo la variabile del foreach è una copia.
Cercare
“Dov’è il giocatore che si chiama così?” è una domanda che farai spessissimo. Una funzione che riceve una slice di Player e restituisce l’indice, oppure -1 se non lo trova:
fn int find(Player[] players, String name)
{
foreach (i, p : players)
{
if (p.name == name) return (int)i;
}
return -1;
}io::printn(find(&team, "Grace"));
io::printn(find(&team, "Margaret"));1
-1Grazie alla slice, la stessa find funziona su un array (&team) e su una lista (players.array_view()). Il -1 per dire “non c’è” è una convenzione vecchia quanto il C; C3 ha un modo più sicuro per dire “nessun risultato”, gli optional, e ci arriveremo nel prossimo modulo.
Ordinare con un criterio tuo
Nel modulo 3 sort::quicksort ordinava numeri e stringhe da solo. Ma come si ordinano dei giocatori? Per nome? Per punteggio? Dal più alto o dal più basso? Deve dirlo tu, scrivendo una funzione di confronto: riceve due elementi a e b e restituisce un numero negativo se a va prima di b, positivo se va dopo, zero se per te sono equivalenti. È la stessa convenzione di compare_to sulle stringhe.
fn int by_score_desc(Player a, Player b) => b.score - a.score;
fn int by_name(Player a, Player b) => a.name.compare_to(b.name);Poi la passi a quicksort come secondo argomento, con una & davanti al nome:
sort::quicksort(&players, &by_score_desc);
foreach (p : players) p.print();
sort::quicksort(&players, &by_name);
foreach (p : players) p.print();Zoe 90
Ada 72
Barbara 55
Linus 25
Ada 72
Barbara 55
Linus 25
Zoe 90b.score - a.score è negativo quando a ha più punti di b: allora a va prima, e l’ordine è decrescente. Per l’ordine crescente basta invertire: a.score - b.score.
Passare una funzione a un’altra funzione è un’idea nuova e potente: quicksort sa come ordinare (l’algoritmo), e tu gli dici cosa significa “viene prima”. &by_name è l’indirizzo della funzione, un puntatore a funzione.
Esercizio · sul tuo computer
Il podio
Riprendi la classifica della sfida del modulo 3, stavolta con le struct. Scrivi podium.c3 con:
struct Matchconplayer(String) epoints(int): una partita;struct Playerconname(String),totalegames(int);- un metodo
Player.add(points)che aggiunge i punti al totale e conta una partita in più (quale forma diselfserve?); - la funzione
finddi questa lezione (adattata ai campi diPlayer); - una funzione di confronto per ordinare dal totale più alto al più basso.
I dati sono già pronti:
Match[*] matches = {
{ "Ada", 42 },
{ "Grace", 17 },
{ "Linus", 8 },
{ "Ada", 30 },
{ "Barbara", 55 },
{ "Grace", 25 },
{ "Linus", 12 },
{ "Linus", 31 },
};Per ogni partita, cerca il giocatore nella List{Player}: se non c’è, aggiungilo con totale e partite a zero. Poi chiama add su di lui, nella lista. Alla fine ordina e stampa la classifica numerata come nell’output atteso (nome su 8 caratteri a sinistra, totale su 3).
Mostra una soluzione (prima prova da solo!)
import std::io;
import std::collections::list;
import std::sort;
struct Match
{
String player;
int points;
}
struct Player
{
String name;
int total;
int games;
}
fn void Player.add(&self, int points)
{
self.total += points;
self.games++;
}
fn int find(Player[] players, String name)
{
foreach (i, p : players)
{
if (p.name == name) return (int)i;
}
return -1;
}
fn int by_total_desc(Player a, Player b) => b.total - a.total;
fn void main()
{
Match[*] matches = {
{ "Ada", 42 },
{ "Grace", 17 },
{ "Linus", 8 },
{ "Ada", 30 },
{ "Barbara", 55 },
{ "Grace", 25 },
{ "Linus", 12 },
{ "Linus", 31 },
};
List{Player} players;
foreach (m : matches)
{
int index = find(players.array_view(), m.player);
if (index == -1)
{
players.push({ .name = m.player });
index = (int)players.len() - 1;
}
players[index].add(m.points);
}
sort::quicksort(&players, &by_total_desc);
foreach (i, p : players)
{
io::printfn("%d. %-8s %3d points, games: %d", i + 1, p.name, p.total, p.games);
}
}Confronta con la sfida del modulo 3: niente più liste parallele da tenere allineate, un solo push per giocatore, e il codice dice quello che fa (players[index].add(m.points)). Il nuovo giocatore entra con { .name = m.player }: totale e partite nascono a zero da soli. E il suo indice è l’ultimo, len() - 1.
Linus ha giocato peggio in ogni singola partita, ma avendone giocate tre supera Grace. Se vuoi una classifica più giusta, prova a ordinare per media punti: attenzione alla divisione intera!
Ricapitolando
- Array, slice e liste di struct funzionano come quelli di numeri:
team[2].score,players.push({ "Ada", 72 }). printnnon stampa bene le collezioni di struct: scrivi un metodoprinte usa un ciclo.- La variabile del
foreachè una copia: per modificare usaforeach (&p : team), e con le struct basta il punto,p.score. team[i]elist[i]portano all’elemento vero:list[i].add(5)modifica la lista.- Per cercare, una funzione su
Player[]che restituisce l’indice (o -1). sort::quicksort(&list, &by_score)ordina con una funzione di confronto: negativo seava prima, positivo se va dopo.
Nella classifica, ogni giocatore è una combinazione di valori qualsiasi. Ma ci sono cose che possono essere solo una tra poche: un seme delle carte, una direzione, un giorno della settimana. Per quelle, gli enum.