Lezione 2 di 8 · 18 min di lettura

Slice, finestre su un array

Guardare un pezzo di array senza copiarlo. Il tipo int[], gli intervalli con .. e :, gli indici contati dalla fine con ^, e perché modificare una slice modifica l'array.

Una finestra, non una copia

Hai un array con le temperature di sei ore del giorno e ti interessano solo le prime tre, quelle del mattino. Potresti copiarle in un nuovo array int[3]. Ma spesso non vuoi una copia: vuoi solo guardare una parte dell’array che hai già.

È quello che fa una slice (in inglese, “fetta”): una finestra aperta su un pezzo di array. Il suo tipo si scrive come quello di un array, ma senza numero tra le quadre:

c3
int[6] temps = { 12, 15, 19, 22, 18, 14 };
int[] all = &temps;     // a slice looking at the whole array
io::printn(all);
io::printn(all.len);
output
[12, 15, 19, 22, 18, 14]
6

int[] si legge “una slice di int”: non dice quanti sono, perché una slice può guardare qualsiasi numero di elementi. Lo sa lei, e te lo dice con .len. La & davanti a temps significa “l’array vero e proprio, non una sua copia”: è il segno che la slice non si porta dietro i valori, ma indica dove stanno. Si usa e si scorre esattamente come un array: all[0], all.len, foreach (t : all).

Dettagli nerd Cosa c'è dentro una slice?

Una slice è piccolissima: sono due numeri. Il primo è l’indirizzo in memoria del suo primo elemento, cioè il numero della casella di memoria dove comincia (la RAM è una fila enorme di byte numerati, e l’indirizzo è semplicemente il numero di uno di quei byte). Il secondo è la lunghezza. In tutto 16 byte su un computer a 64 bit, che la slice guardi 3 elementi o 3 milioni.

Un indirizzo di memoria si chiama anche puntatore, perché “punta” a un dato. I puntatori avranno un modulo tutto loro; per ora ti basta sapere che una slice è un puntatore con la sua lunghezza sempre accanto. In C, la lunghezza la devi portare in giro tu, a parte, e se ti sbagli nessuno te lo dice.

Tagliare: gli intervalli

Il bello delle slice è che puoi aprire la finestra su un pezzo qualsiasi. Ci sono due modi di scrivere l’intervallo tra le quadre:

  • inizio..fine: dall’indice inizio all’indice fine, entrambi inclusi;
  • inizio:quanti: dall’indice inizio, prendi quanti elementi.
c3
io::printn(temps[1..3]);   // indexes 1, 2 and 3
io::printn(temps[1:3]);    // three elements starting at index 1
output
[15, 19, 22]
[15, 19, 22]

Stesso risultato, due modi di pensare: con .. dici dove finisce, con : quanto è lunga. Usa quello che rende più chiaro il tuo codice.

Puoi anche omettere un estremo: senza inizio si parte da 0, senza fine si arriva in fondo.

c3
io::printn(temps[..2]);    // from the start up to index 2
io::printn(temps[3..]);    // from index 3 to the end
io::printn(temps[..]);     // everything
output
[12, 15, 19]
[22, 18, 14]
[12, 15, 19, 22, 18, 14]

temps[..] è l’intero array come slice: equivale a &temps.

Contare dalla fine: ^

Spesso interessano gli ultimi elementi. Invece di calcolare temps.len - 2, puoi scrivere ^2: il simbolo ^ significa “contando dalla fine”, e ^n vale len - n.

c3
io::printn(temps[^2..]);   // the last two
io::printn(temps[^1]);     // the last one
output
[18, 14]
14

^1 è l’ultimo elemento, ^2 il penultimo, e così via. Funziona anche come indice semplice, fuori da un intervallo.

Quiz

Con int[6] temps = { 12, 15, 19, 22, 18, 14 };, cosa contiene temps[2:3]?

Guardare è toccare

Una slice non ha valori suoi: guarda quelli dell’array. Quindi se scrivi attraverso la slice, modifichi l’array:

c3
int[] morning = temps[0:3];
morning[0] = -5;
io::printn(temps);
output
[-5, 15, 19, 22, 18, 14]

È la differenza fondamentale con l’array della lezione precedente: int[3] b = a; fa una copia, int[] s = a[0:3]; apre una finestra. Stessa sintassi per leggere e scrivere, effetti molto diversi.

Puoi anche tagliare una slice, e ottenere una finestra più piccola sullo stesso array:

c3
int[] middle = temps[1..4];   // 15, 19, 22, 18
int[] inner = middle[1..2];   // 19, 22: indexes are relative to middle!
inner[0] = 0;
io::printn(temps);
output
[-5, 15, 0, 22, 18, 14]

Attenzione agli indici: middle[1..2] conta dall’inizio di middle, non dell’array. Ma la memoria sotto è sempre quella di temps.

Riempire e copiare a pezzi

Assegnando un singolo valore a una slice, lo scrivi in tutte le sue caselle:

c3
temps[0..2] = 20;
io::printn(temps);
output
[20, 20, 20, 22, 18, 14]

E assegnando una slice a un’altra della stessa lunghezza, copi gli elementi da una all’altra:

c3
int[3] backup = { 7, 8, 9 };
temps[3..] = backup[..];
io::printn(temps);
output
[20, 20, 20, 7, 8, 9]

Le due slice devono avere la stessa lunghezza: qui entrambe ne hanno 3.

Infine, == tra due slice confronta il contenuto, come per gli array:

c3
int[*] x = { 1, 2, 3 };
int[*] y = { 9, 2, 3 };
io::printn(x[1..] == y[1..]);   // [2, 3] == [2, 3]
output
true

Fuori dalla finestra

Una slice non può uscire dall’array che guarda. Se l’intervallo sfora, il programma si ferma, come con gli indici:

window.c3
import std::io;

fn void main()
{
	int[6] temps = { 12, 15, 19, 22, 18, 14 };
	int start = 4;
	io::printn(temps[start:3]);
}
output
ERROR: 'End index out of bounds (end index of 6 exceeds size of 6)'
  in window.main (window.c3:7) [window] [inline]

Tre elementi a partire dall’indice 4 sarebbero gli indici 4, 5 e 6, ma l’ultimo indice valido è 5. Il messaggio parla di “end index of 6”: l’ultimo indice che hai chiesto.

Esercizio · sul tuo computer

Il contapassi

Scrivi steps.c3 con i passi di una settimana, da lunedì a domenica:

c3
int[*] week = { 8200, 10400, 6300, 12000, 9100, 15500, 4800 };

Senza copiare nulla, crea tre slice e stampale:

  1. workdays: da lunedì a venerdì, usando un intervallo senza inizio;
  2. weekend: gli ultimi due giorni, usando ^;
  3. midweek: da martedì a giovedì, usando la forma inizio:quanti.

Poi:

  1. stampa Last day: e l’ultimo elemento di week, usando ^;
  2. azzera la domenica scrivendo attraverso weekend (non attraverso week!) e stampa After reset: con l’array intero;
  3. con una sola assegnazione, metti 10000 da martedì a giovedì e stampa Flattened: con l’array intero.
Mostra una soluzione (prima prova da solo!)
steps.c3
import std::io;

fn void main()
{
	int[*] week = { 8200, 10400, 6300, 12000, 9100, 15500, 4800 };

	int[] workdays = week[..4];
	int[] weekend = week[^2..];
	int[] midweek = week[1:3];
	io::printfn("Workdays: %s", workdays);
	io::printfn("Weekend: %s", weekend);
	io::printfn("Midweek: %s", midweek);
	io::printfn("Last day: %d", week[^1]);

	weekend[1] = 0;
	io::printfn("After reset: %s", week);

	week[1..3] = 10000;
	io::printfn("Flattened: %s", week);
}

Il punto 5 è quello che conta: weekend[1] è la seconda casella della finestra, cioè la domenica di week. Hai cambiato l’array senza mai nominarlo. Se weekend fosse stato un int[2] (una copia), week sarebbe rimasto com’era.

Ricapitolando

  • Una slice int[] è una finestra su un array: non ha valori suoi, e ha sempre la sua .len.
  • &arr e arr[..] danno l’array intero come slice.
  • a[1..3] va dall’indice 1 al 3 inclusi; a[1:3] prende 3 elementi dall’indice 1. Gli estremi si possono omettere.
  • ^n conta dalla fine: a[^1] è l’ultimo elemento, a[^2..] gli ultimi due.
  • Scrivere in una slice modifica l’array sotto. a[0..2] = 0; riempie, a[3..] = b[..]; copia.
  • == confronta il contenuto; uscire dai limiti ferma il programma.

Ora che sai cos’è una slice, puoi risolvere il problema lasciato in sospeso: scrivere funzioni che accettano array di qualsiasi lunghezza.