Archivi delle etichette: value variable

La varietà di variabili in Go

Scarica l’articolo per la stampa nel formato pdf: golang_tipi_variabili1.pdf

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.

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:

-- in un linguaggio generico...
v = 10
print( v )  --> stampa 10

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:

-- 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.

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.

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 è:

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 è:

package main

import "fmt"

func main() {
    // uno slice:
    v := []int{10}
    q := v
    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).

Variabili valore

Se la variabile è intesa come il contenitore stessso 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.

Schema di funzionamento delle variabili valore

Schema di funzionamento delle variabili valore

…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…

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 da 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 direttamente le informazioni solo che non rappresentano il dato ma solo quello che serve al compilatore per raggiungerlo.

Ecco una rappresentazione schematica dello stesso codice precedente che coinvolge le variabili v e q ma stavolta di classe reference.

Schema di funzionamento delle variabili riferimento

Schema di funzionamento delle variabili riferimento

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:

package main

import "fmt"

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?

Risposta uno

Due variabili riferimento di uno stesso oggetto avranno lo stesso indirizzo di memoria?

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:

package main

import "fmt"

func main(){
    val := 10
    // stampa --> 'val' address: 0x10d50038
    fmt.Println("'val' address:", &val)

    ref := []int{1, 2, 3}
    // 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):

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.

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, e conterrà quindi le stesse informazioni nascoste per l’accesso ai dati, ma un istruzione di modifica all’interno della funzione modificherà l’oggetto, l’unico oggetto, a cui sia l’argomento passato che l’argomento di funzione si riferiscono.

Utilizzando il tipo map[string]int lascio al lettore la scrittura del minicodice di verifica…

Risposta tre

In Go, conviene passare ad una funzione un puntatore a slice o lo slice stesso?

La variabile che contiene uno slice è reference, quindi passare un puntatore a slice è solo più complicato: non ci si guadagna nemmeno con la dimensione in memoria degli argomenti.

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?

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.