Struct
Una struct raggruppa sotto un unico nome dei valori correlati, come in C. In Cromo può avere anche un costruttore, dei metodi e un distruttore, come una classe del C++, ma non c'è ereditarietà: ciò che tipi diversi hanno in comune si esprime con le interfacce, nel capitolo 9.
#Definire una struct
struct Rectangle {
f64 width
f64 height
}
i32 main() {
Rectangle r
r.width = 3.0
r.height = 2.0
println("{} x {}", r.width, r.height)
ret 0
}3 x 2Rectangle r dichiara un rettangolo con il suo valore predefinito: ogni campo ha il valore predefinito del suo tipo, qui 0. Un campo può dichiarare un proprio valore iniziale:
struct Settings {
string host = "localhost"
i32 port = 8080
list<string> paths = ["/", "/health"]
bool verbose
}
i32 main() {
Settings s
println("{}:{} {} {}", s.host, s.port, s.paths.len(), s.verbose)
ret 0
}localhost:8080 2 falseIl valore iniziale può essere un'espressione qualsiasi, e viene calcolato ogni volta che si crea un Settings predefinito.
#Costruttori
Un costruttore si scrive come in C++, con il nome della struct e senza tipo. Assegna i campi, che si usano con il loro nome: non esiste this.
struct Rectangle {
f64 width
f64 height
Rectangle(f64 w, f64 h) {
width = w
height = h
}
}
i32 main() {
Rectangle r = Rectangle(3.0, 2.0)
println("{} x {}", r.width, r.height)
ret 0
}3 x 2Una struct ha al massimo un costruttore. Un parametro non può avere il nome di un campo, così un nome dentro la struct indica sempre una cosa sola: width = w, mai width = width.
Quando un costruttore non riesce a fare il suo lavoro, per esempio perché un file non si apre, lancia un'eccezione: l'oggetto allora non esiste, e i campi già costruiti vengono distrutti.
Una struct senza costruttore si può comunque creare come valore, con Rectangle(): è il suo valore predefinito. Una variabile dichiarata senza valore riceve sempre il valore predefinito, anche quando la struct ha un costruttore: il costruttore viene eseguito solo quando lo si chiama.
#Metodi
Le funzioni scritte dentro la struct sono i suoi metodi. Vedono i campi per nome e possono chiamare gli altri metodi per nome:
struct Rectangle {
f64 width
f64 height
Rectangle(f64 w, f64 h) {
width = w
height = h
}
f64 area() const {
ret width * height
}
bool isSquare() const {
ret width == height
}
void scale(f64 factor) {
width *= factor
height *= factor
}
bool contains(Rectangle other) const {
ret width >= other.width && height >= other.height
}
}
i32 main() {
Rectangle a = Rectangle(3.0, 2.0)
Rectangle b = Rectangle(1.0, 1.0)
println("area {}, square {}", a.area(), b.isSquare())
b.scale(4.0)
println("a contains b: {}", a.contains(b))
ret 0
}area 6, square true
a contains b: false#Metodi const
const dopo i parametri, come in C++, dice che il metodo non modifica l'oggetto. Solo i metodi const si possono chiamare su un valore che non può essere modificato: un parametro in lettura, una variabile const, l'elemento di un for su una lista.
struct Counter {
i64 value
void increment() {
value++
}
}
void show(Counter c) {
c.increment()
}
i32 main() {
Counter c
show(c)
ret 0
}error: cannot call 'increment', which is not constSenza puntatori, const è profondo: un metodo const non può modificare nemmeno le stringhe e le liste contenute nell'oggetto.
#Distruttori
Il distruttore, ~Name(), viene eseguito quando l'oggetto finisce: alla fine del suo blocco, quando viene rimosso da una lista, quando finisce la struct che lo contiene. Rilascia ciò che l'oggetto detiene. Dopo il distruttore, i campi vengono distrutti da soli, a partire dall'ultimo dichiarato, quindi un distruttore si occupa solo di ciò che i campi non sanno rilasciare da soli: un handle di sistema, un file temporaneo, un messaggio da stampare.
struct Timer {
string label
Timer(move string name) {
label = move name
println("{}: start", label)
}
~Timer() {
println("{}: end", label)
}
}
void work() {
Timer t = Timer("work")
println("working")
}
i32 main() {
work()
Timer outer = Timer("main")
ret 0
}work: start
working
work: end
main: start
main: endUn distruttore non si chiama mai a mano, e non può lasciar uscire un'eccezione: il compilatore lo controlla.
Una struct con un distruttore non è un tipo semplice: viene spostata, e copiata solo se scrive un proprio metodo copy().
#Stampare una struct
{:?} stampa qualsiasi valore in una forma pensata per il debug, con i nomi dei campi. Per stampare una struct con {}, dalle un metodo string text() const:
struct Point {
i64 x
i64 y
string text() const {
ret format("({}, {})", x, y)
}
}
i32 main() {
Point p
p.x = 1
p.y = 2
println("{} {:?}", p, p)
ret 0
}(1, 2) Point { x: 1, y: 2 }format funziona come println, ma restituisce il testo invece di stamparlo.
#Struct dentro altre struct
Una struct può contenere altre struct, liste di struct, mappe: tutto ciò che contiene viene distrutto insieme a lei. Non può contenere direttamente se stessa, perché avrebbe una dimensione infinita, ma può farlo attraverso una lista:
struct Folder {
string name
list<Folder> children
i64 count() const {
i64 total = 1
for (Folder child in children) {
total += child.count()
}
ret total
}
}
i32 main() {
Folder root
root.name = "/"
Folder docs
docs.name = "docs"
docs.children.add(Folder())
root.children.add(move docs)
root.children.add(Folder())
println("{} folders", root.count())
ret 0
}4 folders#Riepilogo
- Una struct ha dei campi, al massimo un costruttore, dei metodi e un distruttore; al suo interno, i campi si usano per nome.
- Ogni campo ha un valore predefinito, oppure il valore iniziale che dichiara.
- I metodi
constnon modificano l'oggetto, e sono gli unici che si possono chiamare su un valore in sola lettura. - Il distruttore rilascia ciò che l'oggetto detiene, in automatico, al momento giusto.
text()permette di stampare una struct con{};{:?}stampa qualsiasi valore per il debug.