5  La variabile

Le variabili sono uno dei concetti fondamentali nella programmazione, essenziali per la manipolazione e la gestione dei dati. Una variabile è un nome simbolico associato a una locazione di memoria che può contenere uno o più valori di un certo tipo di dato. Questo concetto permette agli sviluppatori di astrarre dalla memoria fisica e concentrarsi sulla logica del programma.

La gestione delle variabili varia tra i diversi linguaggi di programmazione, quindi esploreremo le variabili con particolare attenzione a Python, Java, C e C++.

5.0.1 La dichiarazione e la inizializzazione

La dichiarazione di una variabile è il processo mediante il quale si introduce una variabile nel programma, specificandone il nome e, in molti casi, il tipo di dato che essa può contenere. Questo processo informa il compilatore o l’interprete che una certa variabile esiste e può essere utilizzata nel codice. Abbiamo visto che esiste una istruzione specifica in alcuni linguaggi, mentre in altri è implicita nella assegnazione.

L’inizializzazione di una variabile è il processo di assegnazione di un valore iniziale alla variabile. Questo può avvenire contestualmente alla dichiarazione o in un’istruzione separata successiva, quella di assegnamento.

Esempi:

  • In Python, le variabili sono dichiarate automaticamente al momento dell’assegnazione del valore. Non è necessario specificare il tipo di dato, poiché Python è dinamicamente tipizzato, cioè determina durante l’esecuzione il tipo di dato del valore associato alla variabile.

    1x = 10
    
    2x = "Hello"
    1
    Dichiarazione e inizializzazione.
    2
    Cambia il tipo di x dinamicamente a stringa.
  • In Java, le variabili devono essere dichiarate con un tipo di dato esplicito. La dichiarazione può avvenire contestualmente all’inizializzazione o separatamente:

    int x;
    
    x = 10;
    
    int y = 20;
  1. Dichiarazione.
  2. Inizializzazione.
  3. Dichiarazione e inizializzazione.
  • In C, la dichiarazione delle variabili richiede la specifica del tipo di dato. La dichiarazione e l’inizializzazione possono essere separate o combinate:

    int x;
    
    x = 10;
    
    int y = 20;
  1. Dichiarazione.
  2. Inizializzazione.
  3. Dichiarazione e inizializzazione.
  • Il C++ è simile al C, ma con funzionalità aggiuntive come l’inizializzazione a lista:

    1int x;
    
    2x = 10;
    
    3int y = 20;
    
    4int z{30};
    
    5int arr[5] = {1, 2, 3, 4, 5};
    1
    Dichiarazione.
    2
    Inizializzazione.
    3
    Dichiarazione e inizializzazione.
    4
    Dichiarazione e inizializzazione a lista di z con l’intero 30.
    5
    Dichiarazione e inizializzazione a lista di un array con 5 valori predefiniti.

5.0.2 L’ambito

L’ambito di una variabile rappresenta la porzione del codice in cui l’identificatore della variabile è definito e, quindi, può essere utilizzato.

Gli approcci dei diversi linguaggi sono diversi, infatti Java, C e C++ hanno una gestione dell’ambito delle variabili per cui quelle dichiarate all’interno di un blocco sono limitate a quel blocco e non sono visibili al di fuori di esso. In Python, invece, le variabili definite all’interno di un blocco di un’istruzione composta (come un ciclo for o una condizione if) rimangono accessibili anche al di fuori del blocco, purché siano ancora nel medesimo ambito di funzione o modulo e, soprattutto, quel blocco sia stato eseguito.

  • Le variabili con ambito globale sono dichiarate al di fuori di qualsiasi blocco e sono accessibili ovunque nel programma:

    1int globalVar = 10;
    
    2void function() {
    3  printf("%d\n", globalVar);
    }
    1
    Variabile globale.
    2
    Inizio del blocco.
    3
    Accesso alla variabile globale.
  • Le variabili con ambito locale sono dichiarate all’interno di un blocco, come una funzione o un loop, e sono accessibili solo all’interno di quel blocco:

    public class Main {
      public static void main(String[] args) {
        if (true) {
    1      int x = 10;
        }
    
    2    System.out.println(x);
    
        for (int i = 0; i < 10; i++) {
    3      int y = i;
        }
    
    4    System.out.println(y);
      }
    }
    1
    Variabile locale al blocco if.
    2
    Errore: x non è visibile qui!
    3
    Variabile locale al blocco for.
    4
    Errore: y non è visibile qui.
  • In Python, una variabile definita all’interno di un blocco di un’istruzione composta, come all’interno di un ciclo for o di una condizione if, rimane accessibile anche dopo l’esecuzione del blocco:

    for i in range(10):
      loopVar = i  # 
    
    print(loopVar)  # 
    1. Variabile locale al ciclo.
    2. loopVar è ancora accessibile qui.

5.0.3 La visibilità

La visibilità si riferisce alla possibilità che in una regione di codice una certa variabile possa essere vista e utilizzata. Anche se correlata all’ambito, la visibilità può essere influenzata da altri fattori come la modularità e gli spazi di nomi (namespace).

  • Consideriamo un esempio in C++ per illustrare la differenza tra ambito e visibilità:

    #include <iostream>
    
    1int globalVar = 10;
    
    void function() {
    2  int localVar = 5;
    
    3  std::cout << globalVar << std::endl;
    4  std::cout << localVar << std::endl;
    }
    
    int main() {
      function();
    
    5  std::cout << globalVar << std::endl;
    6  std::cout << localVar << std::endl;
    
      return 0;
    }
    1
    Variabile globale (ambito globale).
    2
    Variabile locale (ambito locale alla funzione).
    3
    Visibilità globaleVar all’interno della funzione.
    4
    Visibilità localVar all’interno della funzione.
    5
    Visibilità globalVar all’interno di main.
    6
    Errore: localVar non è visibile qui (ambito locale alla funzione function).
  • In Python, le variabili definite all’interno di una funzione sono locali a quella funzione, ma le variabili definite all’interno di un blocco (come un ciclo for o un if) sono visibili all’interno della funzione o del modulo in cui si trovano:

    1globalVar = 10
    
    def function():
    2  localVar = 5
    
      if True:
    3    blockVar = 20
    
    4  print(localVar)
    5  print(blockVar)
    
    function()
    
    6print(globalVar)
    7print(localVar)
    8print(blockVar)
    1
    Variabile globale.
    2
    Variabile locale.
    3
    Visibile all’interno della funzione.
    4
    Visibile.
    5
    Visibile.
    6
    Visibile.
    7
    Errore: non visibile al di fuori della funzione.
    8
    Errore: non visibile al di fuori della funzione.

5.0.4 La durata di vita degli oggetti

La durata di vita descrive per quanto tempo un oggetto rimane in memoria durante l’esecuzione del programma. Questo è distinto dalla variabile (o puntatore) che fa riferimento all’oggetto.

In alcuni linguaggi di programmazione è presente il garbage collector, cioè un processo avviato dal compilatore o interprete che si occupa di rendere nuovamente disponibili le aree di memoria precedentemente occupate da oggetti non più referenziati da variabili. Questo accade quando l’esecuzione del programma raggiunge regioni di codice dove quelle variabili non sono più visibili. In questo modo, la visibilità è legata alla durata di vita degli oggetti, rendendo la gestione della memoria non più una preoccupazione del programmatore.

Distinguiamo tra variabile automatica, variabile statica e variabile dinamica:

  • Variabile automatica: L’oggetto esiste solo durante l’esecuzione del blocco di codice in cui è stata dichiarata la variabile a cui è associato. Esempio in C:

    void function() {
    1  int autoVar = 10;
    } 
    
    2printf("autoVar cancellata!");
    1
    Dichiarazione di autoVar e creazione in memoria di un oggetto corrispondente all’intero 10.
    2
    Prima di questa istruzione l’oggetto 10 non è più presente in memoria.
  • Variabile statica: La variabile esiste per tutta la durata del programma, ma è accessibile solo all’interno del blocco in cui è dichiarata. Esempio in C:

    void function() {
    1  static int staticVar = 10;
    }
    1
    Variabile statica ottenuta con una parola chiave ad hoc in fase di dichiarazione.
  • Variabile dinamica: Le variabili dinamiche sono utilizzate per riservare memoria che persiste oltre la durata del blocco di codice in cui sono state create. L’oggetto è creato in memoria e deve essere cancellato esplicitamente dall’utente, utilizzando funzioni di gestione della memoria come delete. La variabile che punta all’oggetto è separata dall’oggetto stesso, quindi se la variabile non è più visibile, l’oggetto continuerà a rimanere in memoria e non sarà più eliminabile, causando una perdita di memoria (memory leak). Esempio in C++:

    #include <iostream>
    
    void function() {
    1  int* dynamicVar = new int(10);
    
    2  std::cout << "dynamicVar: " << *dynamicVar << std::endl;
    }
    
    int main() {
    3  function();
    
    4  int* safeDynamicVar = new int(20);
    
    5  std::cout << "safeDynamicVar: " << *safeDynamicVar << std::endl;
    
    6  delete safeDynamicVar;
    
      return 0;
    1
    Allocazione dinamica di un intero all’interno della funzione function().
    2
    Stampa del valore puntato da dynamicVar. Prima della chiusura del blocco non viene deallocata dynamicVar per dimostrare il problema di perdita di memoria
    3
    Chiamata alla funzione function(). Dopo l’uscita dalla funzione, dynamicVar non è più accessibile, causando una perdita di memoria poiché non è stata deallocata.
    4
    Allocazione dinamica di un intero.
    5
    Stampa del valore puntato da safeDynamicVar.
    6
    Deallocazione dinamica dell’intero e ciò mostra il corretto uso di allocazione e deallocazione dinamica.

In Python, la gestione della memoria è automatica grazie al garbage collector. Quando non ci sono più riferimenti di variabili a un oggetto, il garbage collector lo rimuove dalla memoria.

6 Lo spazio di nomi, il modulo e il file

In molti linguaggi di programmazione, la gestione dell’ambito e della visibilità delle variabili e delle funzioni può essere ulteriormente organizzata utilizzando spazio di nomi (namespace), moduli, header e file separati. Questa organizzazione aiuta a evitare conflitti di nome e a mantenere il codice più modulare e manutenibile.

6.0.1 Python

In Python, i moduli sono file che contengono definizioni di variabili, funzioni e classi. I moduli possono essere importati in altri moduli o script per riutilizzare il codice. Quando un modulo viene importato, gli identificatori definiti in quel modulo (come variabili, funzioni e classi) diventano accessibili attraverso il nome del modulo. Sebbene i moduli siano spesso implementati come file separati, è possibile definirli anche all’interno di un file di codice sorgente principale.

Esempio di modulo (mymodule.py):

# mymodule.py
1my_var = 10

def my_function():
2    print("Funzione del modulo")
1
Variabile globale nel modulo.
2
Funzione nel modulo.

Importazione di un modulo in un altro file sorgente (main.py):

# main.py
1import mymodule

2print(mymodule.my_var)
3mymodule.my_function()
1
Importazione del modulo mymodule.
2
Accesso alla variabile my_var dal modulo mymodule.
3
Chiamata della funzione my_function dal modulo mymodule.

6.0.2 Java

In Java, i pacchetti (package) sono utilizzati per organizzare le classi in namespace separati. Ogni classe deve dichiarare il pacchetto di appartenenza.

Esempio di classe in un pacchetto (mypackage/MyClass.java):

// mypackage/MyClass.java
1package mypackage;

public class MyClass {
2    public static int myVar = 10;

    public static void myMethod() {
3        System.out.println("Metodo del pacchetto");
    }
}
1
Dichiarazione del pacchetto mypackage.
2
Variabile globale di classe.
3
Metodo della classe.

Importazione di una classe da un pacchetto in un’altra classe (Main.java):

// Main.java
1import mypackage.MyClass;

public class Main {
    public static void main(String[] args) {
2        System.out.println(MyClass.myVar);
3        MyClass.myMethod();
    }
}
1
Importazione della classe MyClass dal pacchetto mypackage.
2
Accesso alla variabile myVar dalla classe MyClass.
3
Chiamata del metodo myMethod dalla classe MyClass.

6.0.3 C

In C, i file di intestazione (header file) sono utilizzati per dichiarare funzioni e variabili che possono essere utilizzate in più file sorgente, a mo’ di libreria.

Esempio di file di intestazione (mymodule.h):

// mymodule.h
#ifndef MYMODULE_H 
#define MYMODULE_H

1extern int myVar;

2void myFunction();

#endif
1
Dichiarazione della variabile globale myVar.
2
Dichiarazione della funzione myFunction.

Esempio di file sorgente (mymodule.c):

#include "mymodule.h"

1int myVar = 10;

void myFunction() {
2    printf("Funzione del modulo\n");
}
1
Definizione della variabile globale myVar.
2
Definizione della funzione myFunction.

Utilizzo del file di intestazione in un altro file sorgente (main.c):

#include <stdio.h>
1#include "mymodule.h"

int main() {
2    printf("%d\n", myVar);

3    myFunction();

    return 0;
}
1
Inclusione del file di intestazione mymodule.h.
2
Accesso alla variabile myVar dichiarata in mymodule.h.
3
Chiamata della funzione myFunction dichiarata in mymodule.h.

6.0.4 C++

In C++, la parola chiave namespace è utilizzata per organizzare le classi, le funzioni e le variabili in spazi di nomi separati, simili ai pacchetti in Java.

Esempio di dichiarazione di uno spazio di nomi (mymodule.h):

#ifndef MYMODULE_H 
#define MYMODULE_H

1namespace mynamespace {
2    extern int myVar;

3    void myFunction();
}

#endif
1
Dichiarazione dello spazio di nomi mynamespace.
2
Dichiarazione della variabile globale myVar all’interno dello spazio di nomi.
3
Dichiarazione della funzione myFunction all’interno dello spazio di nomi.

Esempio di definizione dello spazio di nomi (mymodule.cpp):

#include "mymodule.h"
#include <iostream>

namespace mynamespace {
1    int myVar = 10;

    void myFunction() {
2        std::cout << "Funzione del namespace" << std::endl;
    }
}
1
Definizione della variabile globale myVar all’interno dello spazio di nomi.
2
Definizione della funzione myFunction all’interno dello spazio di nomi.

Utilizzo dello spazio di nomi in un altro file sorgente (main.cpp):

#include "mymodule.h"
#include <iostream>

int main() {
1    std::cout << mynamespace::myVar << std::endl;
2    mynamespace::myFunction();
    return 0;
}
1
Accesso alla variabile myVar all’interno dello spazio di nomi mynamespace.
2
Chiamata della funzione myFunction all’interno dello spazio di nomi mynamespace.

6.0.5 Impatti

L’uso di spazio di nomi, moduli e file di intestazione influisce sull’ambito e sulla visibilità delle variabili e delle funzioni. In generale, questi meccanismi consentono una maggiore modularità e organizzazione del codice, facilitando la gestione di grandi progetti.

  • Ambito: L’ambito delle variabili e delle funzioni può essere limitato a uno spazio di nomi o a un modulo, riducendo il rischio di conflitti di nome.

  • Visibilità: Le variabili e le funzioni dichiarate in namespace o moduli possono essere visibili solo all’interno di quel namespace o modulo, a meno che non vengano esplicitamente esportate.

  • Durata di vita degli oggetti: La durata di vita degli oggetti non è direttamente influenzata dallo spazio di nomi o moduli, ma l’organizzazione del codice può rendere più chiaro quando e dove gli oggetti vengono creati e distrutti.

6.0.6 La variabile e il tipo di dato

Una variabile in un linguaggio di programmazione è un nome simbolico associato a una locazione di memoria. Le variabili sono utilizzate per referenziare e manipolare dati durante l’esecuzione di un programma. Il valore associato alla variabile è memorizzato nella locazione di memoria a cui la variabile fa riferimento, e il tipo di dato della variabile determina come quel valore viene interpretato e quali operazioni possono essere eseguite su di esso.

Due operazioni importanti con le variabili sono:

  • Dichiarazione: Introduce una variabile nel programma, specificando il suo nome e, in molti linguaggi, il suo tipo di dato. La dichiarazione informa il compilatore o l’interprete che una variabile esiste.

  • Definizione: Oltre a dichiarare la variabile, le assegna anche uno spazio di memoria.

La gestione di queste operazioni varia significativamente tra linguaggi con tipizzazione statica e tipizzazione dinamica:

  • Linguaggi con tipizzazione statica (come Java, C++, TypeScript):

    • Richiedono generalmente una dichiarazione esplicita del tipo di variabile.

    • La verifica del tipo avviene prima dell’esecuzione del programma.

    • La dichiarazione e la definizione possono avvenire separatamente.

    Esempio in Java:

    1int x;
    
    2x = 5;
    1
    Dichiarazione di x.
    2
    Definizione.

    In Java, la verifica del tipo avviene durante la compilazione in bytecode. Anche se Java è eseguito su una macchina virtuale (JVM), il controllo dei tipi è effettuato prima dell’esecuzione.

    Esempio in C++:

    int y;  /* Dichiarazione e definizione (alloca spazio in memoria) */
    y = 10;  /* Assegnazione di un valore */

    In C++, la verifica del tipo avviene completamente durante la compilazione.

  1. Linguaggi con tipizzazione dinamica (come Python, JavaScript, Ruby):

    • Non richiedono una dichiarazione esplicita del tipo.
    • Il tipo viene inferito e verificato durante l’esecuzione del programma.
    • La dichiarazione e la definizione avvengono spesso contemporaneamente.

    Esempio in Python:

    z = 15  /* Dichiarazione e definizione simultanee */

    Esempio in JavaScript:

    let w = 20;  /* Dichiarazione e definizione con 'let' */
    var v;  /* Dichiarazione senza definizione (il valore sarà 'undefined') */

È importante notare che alcuni linguaggi, come TypeScript, offrono un approccio ibrido:

let a: number;  /* Dichiarazione con tipo esplicito */
a = 25;  /* Definizione */

let b = 30;  /* Dichiarazione e definizione con inferenza di tipo */

TypeScript effettua il controllo dei tipi durante la compilazione, ma viene poi compilato in JavaScript, che è dinamicamente tipizzato.

Alcuni linguaggi moderni, come Go o Kotlin, utilizzano l’inferenza di tipo con tipizzazione statica:

var c = 35  /* Go inferisce che c è di tipo int */
val d = 40  /* Kotlin inferisce che d è di tipo Int */

6.0.7 Altri aspetti delle variabili

Il ciclo di vita di una variabile si riferisce al periodo durante il quale la variabile esiste in memoria. Questo ciclo è influenzato dall’allocazione dinamica o statica della memoria e dall’ambito della variabile. Il ciclo di vita del dato si riferisce a quanto a lungo un dato rimane accessibile e utilizzabile nel programma.

L’ambito (scope) definisce la porzione di codice in cui la variabile può essere utilizzata, mentre la visibilità indica da dove la variabile può essere accessibile. Questi aspetti sono influenzati dal modo in cui le variabili vengono dichiarate e definite.

6.0.8 Il tipo di dato e l’inferenza di tipo

Il tipo di dato definisce l’insieme di valori che una variabile può assumere e le operazioni che possono essere eseguite su di essa. Nei linguaggi con tipizzazione statica, il tipo di dato deve essere specificato esplicitamente.

L’inferenza di tipo è una caratteristica in cui il compilatore deduce automaticamente il tipo di una variabile dal contesto, come avviene in linguaggi come TypeScript e Python.

Esempio in TypeScript:

let x = 5;  /* Il compilatore inferisce che x è di tipo number */
let y = "Hello";  /* Il compilatore inferisce che y è di tipo string */

6.0.9 Il modello dati

Il modello dati di un linguaggio di programmazione descrive come vengono rappresentati e manipolati i dati. Questo modello include:

  • Tipi di dati primitivi: Numeri interi, stringhe, booleani, ecc.
  • Tipi di dati complessi: Liste, array, oggetti, ecc.
  • Tipi generici: Permettono di scrivere codice che può lavorare con diversi tipi di dati mantenendo la sicurezza dei tipi.

Esempio di tipi generici in Java:

List<String> stringList = new ArrayList<>();  /* Una lista che può contenere solo stringhe */
List<Integer> intList = new ArrayList<>();  /* Una lista che può contenere solo interi */

6.0.10 Esempi di tipi semplici, contenitori e classi

Esempio in Python:

x = 5  /* Variabile intera */
y = [1, 2, 3]  /* Lista di interi */
class Persona:  /* Definizione di una classe */
    def __init__(self, nome):
        self.nome = nome

p = Persona("Alice")  /* Istanza della classe Persona */

Esempio in Java:

int x = 5;  /* Variabile intera */
int[] y = {1, 2, 3};  /* Array di interi */
class Persona {  /* Definizione di una classe */
    String nome;
    Persona(String nome) {
        this.nome = nome;
    }
}
Persona p = new Persona("Alice");  /* Istanza della classe Persona */

Questi concetti forniscono una panoramica più completa degli elementi semantici fondamentali nei linguaggi di programmazione moderni, coprendo sia aspetti di base che avanzati.

6.0.11 Il modello dati

Il modello dati di un linguaggio di programmazione descrive come vengono rappresentati e manipolati i dati. Questo modello include:

  • Tipi di dati primitivi: Numeri interi, stringhe, booleani, ecc.
  • Tipi di dati complessi: Liste, array, oggetti, ecc.
  • Tipi generici: Permettono di scrivere codice che può lavorare con diversi tipi di dati mantenendo la sicurezza dei tipi.

Esempio di tipi generici in Java:

List<String> stringList = new ArrayList<>();  // Una lista che può contenere solo stringhe
List<Integer> intList = new ArrayList<>();  // Una lista che può contenere solo interi
  1. stringList è una lista di stringhe.
  2. intList è una lista di interi.

6.0.12 Esempi di tipi semplici, contenitori e classi

Esempio in Python:

x = 5  # Variabile intera
y = [1, 2, 3]  # Lista di interi
class Persona:  # Definizione di una classe
    def __init__(self, nome):
        self.nome = nome

p = Persona("Alice")  # Istanza della classe Persona
  1. x è una variabile intera.
  2. y è una lista di interi.
  3. Persona è una classe con un attributo nome.
  4. p è un’istanza della classe Persona.

Esempio in Java:

int x = 5;  // Variabile intera
int[] y = {1, 2, 3};  // Array di interi
class Persona {  // Definizione di una classe
    String nome;
    Persona(String nome) {
        this.nome = nome;
    }
}
Persona p = new Persona("Alice");  // Istanza della classe Persona
  1. x è una variabile intera.
  2. y è un array di interi.
  3. Persona è una classe con un attributo nome.
  4. p è un’istanza della classe Persona.

Questi concetti forniscono una panoramica più completa degli elementi semantici fondamentali nei linguaggi di programmazione moderni, coprendo sia aspetti di base che avanzati.

6.0.13 Espressioni e valutazione

La semantica delle espressioni definisce come queste vengono valutate per produrre un risultato. Ciò include:

  • Ordine di valutazione degli operandi: La sequenza con cui vengono valutati i componenti di un’espressione.
  • Comportamento degli operatori: Come gli operatori agiscono su diversi tipi di dati.
  • Gestione degli errori: Come vengono trattate le situazioni eccezionali durante la valutazione delle espressioni.

Esempio in JavaScript:

let a = 5, b = 2;

1let result = a / b;

2let check = (a > b) && (b !== 0);
1
Il risultato sarà 2.5, a differenza di linguaggi come Java o C dove sarebbe 2 a causa della divisione intera.
2
La seconda condizione viene valutata solo se la prima è vera, grazie alla valutazione “short-circuit”.

6.0.14 Controllo del flusso

La semantica del controllo del flusso definisce come l’esecuzione del programma procede attraverso le istruzioni. Ciò include:

  • Istruzioni condizionali: Come if, else, switch.
  • Cicli: Come for, while.
  • Chiamate a funzione: E il passaggio dei parametri.

Esempio in Python:

1for i in range(5):
2    if i % 2 == 0:
3        continue
4    print(i)
1
Ciclo for che itera da 0 a 4.
2
Controllo condizionale per verificare se i è pari.
3
continue salta le iterazioni pari.
4
Stampa solo i valori dispari di i.

6.0.15 Gestione della memoria

La semantica della gestione della memoria definisce come il programma alloca, utilizza e libera la memoria. Ciò include:

  • Stack e heap: Distinzione tra memoria automatica (stack) e dinamica (heap).
  • Allocazione e deallocazione: Regole per riservare e liberare memoria.
  • Garbage collector: Comportamento nei linguaggi che lo utilizzano.

Esempio in C++:

1int* p = new int;
2*p = 10;
3delete p;
1
new alloca memoria dinamicamente sull’heap.
2
Il puntatore p viene utilizzato per accedere alla memoria allocata.
3
delete libera la memoria allocata, prevenendo potenziali perdite di memoria.

6.0.16 Funzioni e procedure

Le funzioni sono blocchi di codice riutilizzabili che eseguono specifiche operazioni. La semantica delle funzioni include:

  • Definizione e chiamata: Come vengono definite e chiamate le funzioni.
  • Passaggio dei parametri: Modalità di passaggio dei parametri (per valore, per riferimento, ecc.).
  • Restituzione dei valori: Come vengono restituiti i risultati.
  • Ambito delle variabili: Lo scope delle variabili all’interno delle funzioni.

Esempio in JavaScript:

1function add(a, b) {
2    return a + b;
}

3let result = add(3, 4);
console.log(result);  // Stampa 7
1
Definizione della funzione add con due parametri.
2
La funzione restituisce la somma dei parametri.
3
Chiamata della funzione con argomenti 3 e 4.

Nei linguaggi moderni, troviamo anche concetti avanzati come:

  • Funzioni di ordine superiore: Funzioni che possono accettare altre funzioni come argomenti o restituirle come risultato.

Esempio in Python:

def apply(func, x, y):
    return func(x, y)

def multiply(a, b):
    return a * b

result = apply(multiply, 3, 4)  # Restituisce 12
  • Chiusure: Funzioni che “catturano” variabili dal loro ambiente circostante.

Esempio in JavaScript:

function createCounter() {
    let count = 0;
    return function() {
        return ++count;
    };
}

let counter = createCounter();
console.log(counter());  // Stampa 1
console.log(counter());  // Stampa 2

6.0.17 Programmazione asincrona

La programmazione asincrona permette l’esecuzione di operazioni non bloccanti, migliorando l’efficienza e la reattività dei programmi. Concetti chiave includono:

  • Promise/Future: Rappresentano il risultato di un’operazione asincrona.
  • async/await: Sintassi che semplifica la scrittura di codice asincrono.
  • Callback: Funzioni che vengono chiamate al completamento di un’operazione asincrona.

Esempio in JavaScript:

async function fetchData() {
    try {
1        const response = await fetch('https://api.example.com/data');
2        const data = await response.json();
        console.log(data);
    } catch (error) {
        console.error('Errore:', error);
    }
}

3fetchData();
1
await sospende l’esecuzione della funzione finché la Promise restituita da fetch non si risolve.
2
Attende che il corpo della risposta venga convertito in JSON.
3
Chiama la funzione asincrona.

6.0.18 Sistemi di tipi

Il sistema di tipi di un linguaggio definisce come i tipi di dati vengono gestiti. Ciò include:

  • Conversione tra tipi: Regole per la conversione implicita o esplicita tra tipi.
  • Controllo dei tipi: Comportamento del controllo dei tipi (statico o dinamico).
  • Polimorfismo e ereditarietà: Implementazione nei linguaggi orientati agli oggetti.

Esempio in TypeScript:

interface Shape {
    area(): number;
}

class Circle implements Shape {
    constructor(private radius: number) {}
    
    area(): number {
        return Math.PI * this.radius ** 2;
    }
}

class Rectangle implements Shape {
    constructor(private width: number, private height: number) {}
    
    area(): number {
        return this.width * this.height;
    }
}

1function printArea(shape: Shape) {
   

```typescript
    console.log('Area:', shape.area());
}

let circle = new Circle(5);
let rectangle = new Rectangle(4, 6);

printArea(circle);  // Stampa: Area: 78.53981633974483
printArea(rectangle);  // Stampa: Area: 24
1
La funzione printArea accetta qualsiasi oggetto che implementa l’interfaccia Shape.

Questi concetti forniscono una panoramica più completa degli elementi semantici fondamentali nei linguaggi di programmazione moderni, coprendo sia aspetti di base che avanzati.