La varietà di variabili in Go Roberto Giacomelli Articolo sul blog http://okpanico.wordpress.com e-mail: giaconet dot mailbox at gmail dot com 16 aprile 2013 Sommario Faremo un excursus sul significato delle variabili chiarendone il loro funzionamento nei moderni linguaggi di programmazione, in particolare con riferimento al Go. Ci soffermeremo sul concetto di variabile valore e su quello di variabile riferimento. Per la comprensione e l’esecuzione degli esperimenti proposti, è necessario solo un po’ di concentrazione e la lettura dei post sul linguaggio Go apparsi su questo stesso blog. Indice 1 1 Variabili 1 2 Dal C, al C++, a Java 2 3 Tipi di variabili 2 4 Variabili valore 4.1 ...e le strutture? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3 3 5 Variabili riferimento 3 6 Il terzo tipo: i puntatori 3 7 Risposta uno 4 8 Risposta due 5 9 Risposta tre 5 10 Una domanda per il lettore curioso 5 11 Conclusione 6 12 Licenza ed informazioni varie 12.1 Distribuzione/Citazioni . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 12.2 Colophon . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6 6 6 Variabili Tutti o quasi tutti i linguaggi di programmazione comprendono tra i concetti di base quello di variabile. Il nome stesso ci dice che è un qualcosa che può cambiare durante l’esecuzione del programma e quel qualcosa è il dato che la variabile rappresenta. Per fare un esempio di codice in un linguaggio generico, possiamo in modo molto naturale assegnare il valore numerico 10 alla variabile v e sottrarre poi 5: 1 2 3 -- in un linguaggio generico... v = 10 print( v ) --> stampa 10 1 4 5 6 v = v - 5 print( v ) --> stampa 5 Se provo a creare una seconda variabile q con il valore della prima mi aspetto che riceva il valore che v conteneva al momento dell’assegnazione. Ma se poi la prima variabile cambia che ne è della seconda? Il codice seguente chiarisce il dubbio sul comportamento delle variabili: 1 2 3 4 5 6 7 8 9 10 -- sempre scrivendo il codice -- in un linguaggio generico v = 10 q = v print(q) --> stampa 10 -- poi v cambia... v = v - 5 print(q) --> stampa 10 o 5? Ci si può attendere due soli risultati: 1. la seconda variabile non cambia mantenendo il valore 10; 2. la seconda variabile cambia cambiando a sua volta in 5 come la prima. 2 Dal C, al C++, a Java Siamo partiti da un concetto piuttosto semplice: dare un nome al contenitore di un dato che può cambiare durante l’esecuzione del programma — ovvero a run-time — ma ben presto ci siamo resi conto che è necessario scegliere come effettivamente rappresentare le informazioni, e queste scelte influenzeranno nel bene e nel male le applicazioni. Ogni linguaggio dunque affronta problemi di progettazione complessi, con esigenze spesso opposte, come ben testimonia la loro evoluzione storica, per esempio dal C, al C++, a Java. Ad ogni generazione l’ingegneria del software mette a frutto anni di lavoro e l’esperienza di milioni di righe di codice in un nuovo linguaggio. 3 Tipi di variabili Eravamo rimasti al dubbio se nel nostro ipotetico linguaggio sia opportuno o meno agganciare il valore della seconda variabile a quello della variabile da cui era stata costruita. Verifichiamo subito qual è stata la scelta dei progettisti del Go :-) Per una variabile di tipo intero la verifica è: 1 2 3 4 5 6 7 8 9 10 11 12 13 package main import "fmt" func main() { v := 10 q := v // modo compatto di scrivere v = v - 5 v -= 5 // stampa 10 o stampa 5? fmt.Println(q) } invece per una variabile di tipo []int, ovvero uno slice la verifica è: 1 2 3 4 5 6 7 8 package main import "fmt" func main() { // uno slice: v := []int{10} q := v 2 Figura 1: Schema di funzionamento delle variabili valore v v = 10 q = v v = v - 5 9 10 11 12 10 v q 10 10 v q 5 10 v[0] -= 5 // stampa [10] o stampa [5]? fmt.Println(q) } Otteniamo tutti e due i comportamenti! Verificate per esercizio a quale dei due esempi corrispondono i casi (non vi farà male e vi ruberà solo un minuto se utilizzate Go Playground). 4 Variabili valore Se la variabile è intesa come il contenitore in cui si trova il dato, creandone una per mezzo di un’assegnazione di un’altra variabile verrà creato semplicemente un nuovo contenitore con il dato in quel momento contenuto in quest’ultima. Stiamo parlando delle variabili valore che forniscono l’accesso diretto al dato. Quello che abbiamo definito contenitore è in sostanza il segmento di memoria che contiene la rappresentazione binaria del dato. Nella figura di seguito è rappresenta la dinamica della creazione e della modifica delle variabili valore v e q del codice di esempio. 4.1 ...e le strutture? Questo ve lo posso dire: le strutture in Go sono memorizzate in variabili valore, lascio a voi scrivere per utile esercizio il breve codice che lo verifica... 5 Variabili riferimento Se la variabile è intesa come il nome del contenitore in cui si trova il dato, allora creandone una per mezzo di un’assegnazione di un’altra variabile, verrà copiato il nome del contenitore nella nuova. Si tratta delle variabili riferimento che forniscono un accesso indiretto al dato tramite informazioni riguardanti il contenitore. Anche le variabili riferimento sono di tipo valore nel senso che dopotutto contengono informazioni che non rappresentano direttamente il dato ma solo quello che al compilatore serve per raggiungerlo. Ecco una rappresentazione schematica dello stesso codice precedente che coinvolge le variabili v e q ma stavolta di classe reference. 6 Il terzo tipo: i puntatori Non dovrebbe esistere un terzo tipo di variabili perché all’inizio del post abbiamo riscontrato che ci possono essere solo due situazioni che poi abbiamo associato alle variabili valore ed alle variabili riferimento. In Go sono disponibili, come in C ed in C++, i puntatori ed in effetti non costituiscono un terzo tipo di variabile perché (almeno in Go) sono contenuti in normali variabili valore. Per convincerci eseguiamo il solito programma di prova: 1 2 3 4 package main import "fmt" 3 Figura 2: Schema di funzionamento delle variabili valore v v = 10 q = v 10 v q 10 v q v = v - 5 5 6 7 8 9 10 11 12 13 14 5 func main() { n, m := 10, 5 // tipi int p1 := &n // tipo *int p2 := p1 p1 = &m fmt.Println(*p1) // stampa 5 fmt.Println(*p2) // stampa 10 } I puntatori quindi sono variabili valore che contengono l’indirizzo grezzo di memoria dove si trova un dato. Dal punto di vista del linguaggio possiamo considerare i puntatori come delle variabili riferimento primitive. Con i puntatori infatti, gestiamo esplicitamente indirizzi di memoria ottenendoli con l’operatore & come abbiamo appena visto nel listato precedente, ed indichiamo esplicitamente il valore puntato deferenziando il puntatore con l’operatore *. Se affermiamo questo, possiamo anche dire in modo speculare che le variabili riferimento sono un tipo evoluto di puntatore perché nel codice le scriviamo come normali variabili. È il compilatore che svolge il lavoro primitivo per noi elaborando nel modo opportuno le informazioni effettive celate nella variabile riferimento. Possiamo a questo punto rispondere a queste domande: 1. due variabili riferimento di uno stesso oggetto avranno lo stesso indirizzo di memoria? 2. sapendo che in Go alle funzioni vengono passate le copie degli argomenti, cosa accade se modifichiamo una variabile riferimento all’interno di una funzione? 3. in Go, conviene passare ad una funzione un puntatore a slice o lo slice stesso? 7 Risposta uno Quale indirizzo di memoria avranno due variabili riferimento di uno stesso oggetto? Se una variabile riferimento è veramente una variabile valore che contiene un riferimento ad un dato in memoria, ne segue che avrà un proprio indirizzo di memoria diverso da quello di qualsiasi altra variabile dello stesso tipo anche se si riferisce allo stesso oggetto. Verifichiamo per prima cosa se possiamo conoscere facilmente l’indirizzo di memoria di una variabile valore e di una variabile riferimento con questo minicodice: 1 2 3 4 5 6 7 8 9 10 package main import "fmt" func main(){ val := 10 // stampa --> ’val’ address: 0x10d50038 fmt.Println("’val’ address:", &val) ref := []int{1, 2, 3} 4 11 12 13 // stampa --> ’ref’ address: &[1 2 3] fmt.Println("’ref’ address:", &ref) } L’operatore & restituisce il puntatore con l’indirizzo di memoria di una variabile qualsiasi, ma la funzione fmt.Println() esegue giustamente una stampa ad alto livello del puntatore alla variabile riferimento rispettandone la natura. Anziché far decidere alla funzione tuttofare fmt.Println() il formato di stampa possiamo farlo esplicitamente usando il segnaposto %p (consulta la documentazione del pacchetto fmt): 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 package main import "fmt" func main() { // una slice, due variabili reference s1 := []int{1, 2, 3, 4 ,5} s2 := s1 fmt.Printf("address di s1: %p\n", &s1) fmt.Printf("address di s2: %p\n", &s2) // stampano --> // address di s1: 0x10d6f100 // address di s2: 0x10d6f0f0 // verifichiamo che le due variabili // si riferiscono allo stesso slice s1[0]++ s1[1]++ s1[2]-s2[2]-s2[3]++ s2[4]++ fmt.Println(s1,s2) // stampa --> [2 3 1 5 6] [2 3 1 5 6] } La risposta iniziale è corretta: due variabili riferimento non sono puntatori non contenendo un riferimento diretto alla memoria ma contengono dati nascosti al programmatore ciascuna in un’area di memoria propria. 8 Risposta due Sapendo che in Go alle funzioni vengono passate le copie degli argomenti, cosa accade se modifichiamo una variabile riferimento all’interno di una funzione? Dunque, se l’argomento viene copiato all’interno della funzione la variabile riferimento sarà si una copia dell’originale, conterrà quindi le stesse informazioni nascosto per l’accesso ai dati e quindi la modifica all’interno della funzione modificherà l’oggetto, l’unico oggetto, a cui argomento passato e argomento di funzione si riferiscono. Utilizzando il tipo map[string]int lascio al lettore la scrittura del minicodice di verifica. . . 9 Risposta tre In Go, conviene passare ad una funzione un puntatore a slice o lo slice stesso La variabile che contiene uno slice è di tipo reference, quindi passare un puntatore a slice è solo più complicato e non ci si guadagna nemmeno con la dimensione in memoria degli argomenti. 10 Una domanda per il lettore curioso In Go le funzioni sono tipi di prima classe (come in Lua, copioni!). Questo significa che possiamo passare una funzione come argomento di un’altra e creare funzioni anonime. La domanda è questa: Le variabili che contengono una funzione in Go sono di tipo valore o di tipo riferimento? 5 11 Conclusione Abbiamo studiato insieme le due classi di variabili in linguaggio Go, variabili valore e variabili riferimento, lasciando al lettore un po’ di utile lavoro da fare su alcuni esercizi. Come regola generale i moderni linguaggi di programmazione fanno si che ai tipi di dato semplici come i numeri ed i booleani sia associato il tipo di variabile valore, mentre ai dati complessi come gli oggetti sia associato il tipo di variabile riferimento. Un saluto. R. 12 Licenza ed informazioni varie Questo articolo come tutto il materiale didattico/divulgativo del blog http://okpanico.wordpress.com è rilasciato sotto licenza Creative Commons “Attribuzione-Non commerciale-Non opere derivate” 2.5 Italia, il cui testo integrale con valore legale è consultabile a questo indirizzo. Ciò significa che: 1. Bisogna sempre attribuire la paternità del materiale a http://okpanico.wordpress.com; 2. Non si può usare il materiale per fini commerciali; 3. Non si può alterare o trasformare i contenuti, ne’ usarne stralci per creare altre opere. Se esplicitamente indicato nei commenti iniziali, il codice relativo a programmi software è rilasciato nella specifica licenza. 12.1 Distribuzione/Citazioni Ogni volta che usi o distribuisci parti ridistribuibili quest’opera devi farlo secondo i termini con cui esse sono state rilasciate e avendo cura di comunicare tali termini con chiarezza. Ricorda di inserire sempre un hyperlink alla risorsa che ridistribuisci o citi. Il modo migliore per dimostrarmi il vostro apprezzamento è semplicemente quello di linkare direttamente le pagine del blog, senza copiare gli articoli in altri siti, oltre naturalmente a lasciare un commento. Ci sono però casi in cui vorreste poter estrapolare alcune parole dai miei articoli per incuriosire i vostri lettori. In quel caso la prassi convenuta e che, grazie a tutti i blogger seri, viene coscienziosamente rispettata, è questa: • Creare un “blockquote”, ossia un campo in cui è inserita una citazione; • Inserire nel blockquote solo il primo periodo (le prime poche frasi) di un articolo, diciamo fino ad arrivare al link “Leggi il resto. . . ”; • Linkare la rimanente parte dell’articolo all’originale su http://okpanico.wordpress.com. Grazie per la collaborazione. 12.2 Colophon Questo documento è stato composto con LATEX attraverso uno script in Lua chiamato wp2pdf che elabora il file originale html del post pubblicato sul blog in WordPress. Per saperne di più contattatemi via posta elettronica all’indirizzo nel titolo del documento, o lasciate un commento sul blog. 6