CodeGym /Corsi /C# SELF /Funzioni pure e immutabilità

Funzioni pure e immutabilità

C# SELF
Livello 51 , Lezione 1
Disponibile

1. Introduzione

Funzione pura — è una funzione che con gli stessi input restituisce sempre lo stesso risultato e non provoca effetti collaterali. In parole semplici: una funzione pura non "rompe" nulla intorno a sé e non è influenzata dall'esterno.

Una funzione è considerata pura se:

  • Essa solamente calcola un valore basandosi sugli argomenti di input.
  • Non modifica variabili esterne o lo stato del programma.
  • Non dipende da variabili esterne o stati che possono cambiare.

Due regole d'oro della purezza

  1. Determinismo: lo stesso input — lo stesso output.
  2. Nessun effetto collaterale: la funzione non cambia nulla fuori di sé: né file, né variabili globali, né l'interfaccia utente (ciao, Console.WriteLine).

Non è religione, è buon senso

Potrebbe sembrare un'ossessione accademica, ma la pratica dimostra il contrario:

  • Una funzione pura è prevedibile. È facile da testare — la invochi con argomenti definiti e ottieni un risultato definito.
  • Una funzione pura può essere liberamente "rimescolata" nel codice senza temere che qualcosa si rompa altrove.
  • In un mondo multicore una funzione pura è una garanzia di sicurezza: può essere eseguita in parallelo senza paura di race condition.

2. Esempi di funzioni pure e impure

Funzione pura: tutto prevedibile

// Funzione pura: non tocca nulla fuori di sé
int Add(int a, int b)
{
    return a + b;
}

int Square(int x)
{
    return x * x;
}

Chiamare Add(2, 3) anche cento volte di seguito — restituirà sempre 5. Noioso, ma affidabile.

Funzione impura: infrange le regole

// Infrange la purezza: dipende dallo stato esterno (variabile statica)
int counter = 0;

int Increase()
{
    counter++;
    return counter;
}

Qui la chiamata Increase() restituirà ogni volta un valore diverso — quindi non c'è più determinismo.

// Infrange la purezza: provoca un effetto esterno (stampa a schermo)
int AddAndPrint(int a, int b)
{
    int sum = a + b;
    Console.WriteLine(sum); // Effetto collaterale!
    return sum;
}

E la casualità e il tempo?

Qualsiasi funzione che usa DateTime.Now o Random non è più pura:

// Non pura!
int GetRandomNumber()
{
    return new Random().Next();
}

Tabella delle differenze

Caratteristica Funzione pura Funzione impura
Sempre lo stesso risultato per gli stessi argomenti No
Effetti collaterali No
Dipendenza da stato esterno No

3. Immutabilità dei dati: teoria e pratica

Immutabilità (immutability) — è l'approccio in cui un oggetto non può essere modificato dopo la creazione. Se serve un nuovo valore — si crea un nuovo oggetto.

Perché è importante?

  • L'applicazione diventa resistente a modifiche accidentali dei dati.
  • Non ci sono "fughe" nascoste di modifiche: se hai un oggetto, nessuno lo cambierà di nascosto.
  • L'immutabilità è la base per molte ottimizzazioni automatiche e per il calcolo parallelo.

Esempi semplici in C#

Tipi immutabili in .NET

Le stringhe (string) in C# sono immutabili! Ogni volta che fai string.Concat(s, "world"), viene creata una nuova stringa.

string s = "Hello";
string t = s;
s = s + " World";
Console.WriteLine(t); // t == "Hello"

Array e collezioni: di default mutable

int[] numbers = { 1, 2, 3 };
numbers[0] = 42; // L'array è cambiato!

Immutabilità "su due piedi": come compili il codice

Invece di modificare un oggetto/valore esistente, restituiamo uno nuovo:

// Invece di questo:
void AddToList(List<int> list, int value)
{
    list.Add(value); // Mutazione!
}

// Meglio così:
List<int> AddToList(List<int> list, int value)
{
    var newList = new List<int>(list) { value }; // Nuova lista
    return newList;
}

Illustrazione: muta o non muta

flowchart LR
    A[Oggetto originale] --"mutazione"--> B[Stesso oggetto, ma interno diverso]
    A --"immutabilità"--> C[Nuovo oggetto]

4. Perché serve nel codice C# reale?

  • Nel C# moderno librerie come LINQ, Entity Framework e ASP.NET Core puntano su funzioni pure e immutabilità.
  • L'immutabilità riduce i bug "magici" dove qualcuno ha sovrascritto un valore importante da qualche parte.
  • Le funzioni pure permettono di usare agevolmente i test automatici (unit test), perché per testare basta controllare input e output, senza preoccuparsi del mondo esterno.

Esempio: lavoro con le stringhe

string s = "Hello";
string newS = s.Replace("H", "J"); // s rimane "Hello"; newS è "Jello"

Esempio: LINQ e collezioni

Where, Select e altri metodi restituiscono nuove collezioni senza toccare quelle vecchie.

var numbers = new List<int> { 1, 2, 3, 4 };
var evenNumbers = numbers.Where(n => n % 2 == 0).ToList();

// numbers è rimasto uguale!

Esempio pratico: configurazione con oggetti immutabili

Molti API moderni di .NET usano oggetti immutabili per la configurazione, per esempio JsonSerializerOptions:

var options = new JsonSerializerOptions
{
    WriteIndented = true
};
// Questo oggetto non viene cambiato "a runtime", aumentando l'affidabilità.

5. Errori tipici e trappole

La scivolata inizia quando "per sbaglio" muti dati in codice che dovrebbe essere puro.

Succede spesso con le collezioni: volevi filtrare una lista e nel processo hai cambiato quella originale.

O hai dimenticato che un metodo come List<T>.Add modifica l'oggetto sul posto.

Esempio insidioso:

List<int> DoubleTheNumbers(List<int> xs)
{
    // Errore! Mutiamo la lista originale, restituendo lo stesso oggetto.
    foreach (var i in xs)
        xs.Add(i * 2);
    return xs;
}

Questo codice provocherà anche un'eccezione di runtime (InvalidOperationException), perché stiamo modificando la collezione mentre la stiamo iterando — problema classico di mutazione.

Corretto:

List<int> DoubleTheNumbers(List<int> xs)
{
    var newList = new List<int>(xs.Select(x => x * 2));
    return newList;
}
Commenti
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION