Array, stringhe e collezioni in C#

Array, stringhe e collezioni in C#

Raramente un programma lavora con dati isolati: molto più spesso deve gestire insiemi di valori, come una lista di utenti, una sequenza di misurazioni o una tabella di corrispondenze. C# mette a disposizione diversi strumenti per rappresentare e manipolare questi insiemi, dagli array a dimensione fissa alle collezioni dinamiche della libreria standard. In questo articolo esaminiamo gli array, la manipolazione delle stringhe e le collezioni generiche più usate.

Gli array

Un array è una sequenza di elementi dello stesso tipo, memorizzati in modo contiguo e accessibili tramite un indice numerico che parte da zero. La sua dimensione viene fissata al momento della creazione e non può cambiare in seguito.

// Creazione con dimensione e assegnazione per indice
int[] numbers = new int[3];
numbers[0] = 10;
numbers[1] = 20;
numbers[2] = 30;

// Creazione con inizializzazione diretta
string[] colors = { "rosso", "verde", "blu" };

// La proprieta Length restituisce il numero di elementi
Console.WriteLine(colors.Length); // Stampa 3
Console.WriteLine(colors[1]);     // Stampa verde

Poiché la dimensione è fissa, il tentativo di accedere a un indice fuori dai limiti provoca un'eccezione durante l'esecuzione. Per attraversare un array in sicurezza si usa comunemente il ciclo foreach o un ciclo for basato sulla proprietà Length.

Array multidimensionali

C# supporta anche array a più dimensioni, utili per rappresentare griglie o matrici. Esistono due varianti: gli array rettangolari, in cui ogni riga ha la stessa lunghezza, e gli array frastagliati, che sono array di array e possono avere righe di lunghezza diversa.

// Array rettangolare 2x3
int[,] matrix = new int[2, 3];
matrix[0, 0] = 1;
matrix[1, 2] = 6;

// Array frastagliato: righe di lunghezza diversa
int[][] jagged = new int[2][];
jagged[0] = new int[] { 1, 2 };
jagged[1] = new int[] { 3, 4, 5 };

Le stringhe

La stringa è tecnicamente una sequenza immutabile di caratteri: una volta creata, non può essere modificata. Ogni operazione che sembra alterare una stringa in realtà ne produce una nuova. Questa immutabilità garantisce sicurezza ma va tenuta presente quando si eseguono molte modifiche.

string text = "Programmazione";

// Proprieta e metodi comuni
Console.WriteLine(text.Length);            // numero di caratteri
Console.WriteLine(text.ToUpper());         // tutto maiuscolo
Console.WriteLine(text.Substring(0, 5));   // "Progr"
Console.WriteLine(text.Contains("gram"));  // true
Console.WriteLine(text.Replace("a", "@")); // sostituzione

L'interpolazione, introdotta in un articolo precedente, resta il modo più leggibile per comporre stringhe a partire da valori. Per concatenare molte stringhe in un ciclo, però, conviene usare la classe StringBuilder, che lavora su un buffer modificabile ed evita di creare una nuova stringa a ogni passaggio.

using System.Text;

// StringBuilder e efficiente per costruzioni ripetute
var builder = new StringBuilder();
for (int i = 1; i <= 3; i++)
{
    builder.Append($"Riga {i}\n");
}
string output = builder.ToString();

La lista generica

Quando serve una collezione la cui dimensione cresce e si riduce dinamicamente, la scelta naturale è List<T>, dove T rappresenta il tipo degli elementi. A differenza dell'array, la lista consente di aggiungere e rimuovere elementi liberamente.

using System.Collections.Generic;

// Lista di stringhe
var languages = new List<string>();
languages.Add("C#");
languages.Add("Go");
languages.Add("Python");

languages.Remove("Go");        // rimuove un elemento
Console.WriteLine(languages.Count); // numero di elementi: 2

// Iterazione
foreach (string language in languages)
{
    Console.WriteLine(language);
}

La lettera T tra parentesi angolari indica che si tratta di un tipo generico: la stessa classe List può contenere interi, stringhe, oggetti personalizzati o qualsiasi altro tipo, con piena sicurezza sul tipo garantita dal compilatore.

Il dizionario

Il dizionario Dictionary<TKey, TValue> associa una chiave a un valore, permettendo ricerche molto rapide tramite la chiave. È la struttura ideale quando si vuole recuperare un dato a partire da un identificatore, come un nome o un codice.

// Associa un nome a un'eta
var ages = new Dictionary<string, int>();
ages["Anna"] = 30;
ages["Marco"] = 25;

// Accesso tramite chiave
Console.WriteLine(ages["Anna"]); // Stampa 30

// Verifica sicura dell'esistenza di una chiave
if (ages.TryGetValue("Luca", out int lucaAge))
{
    Console.WriteLine(lucaAge);
}
else
{
    Console.WriteLine("Chiave non trovata");
}

Tentare di accedere a una chiave inesistente con l'indicizzatore solleva un'eccezione, per cui il metodo TryGetValue è spesso preferibile perché gestisce l'assenza in modo pulito.

Altre collezioni utili

La libreria standard offre altre collezioni specializzate, ciascuna ottimizzata per uno scenario specifico:

  • HashSet<T>: un insieme di elementi unici, senza duplicati, con verifiche di appartenenza molto rapide.
  • Queue<T>: una coda con politica "primo entrato, primo uscito", adatta a gestire elementi nell'ordine di arrivo.
  • Stack<T>: una pila con politica "ultimo entrato, primo uscito", utile per gestire annullamenti o percorsi ricorsivi.
// HashSet elimina automaticamente i duplicati
var uniqueNumbers = new HashSet<int>();
uniqueNumbers.Add(1);
uniqueNumbers.Add(1); // ignorato, e gia presente
Console.WriteLine(uniqueNumbers.Count); // Stampa 1

// Queue: elaborazione in ordine di inserimento
var queue = new Queue<string>();
queue.Enqueue("primo");
queue.Enqueue("secondo");
Console.WriteLine(queue.Dequeue()); // Stampa "primo"

Un primo sguardo a LINQ

Uno dei tratti distintivi di C# è LINQ (Language Integrated Query), un insieme di metodi che permette di interrogare e trasformare le collezioni con una sintassi dichiarativa. Anche senza approfondirlo, è utile vederne un assaggio per apprezzare quanto renda espressivo il lavoro sui dati.

using System.Linq;

var numbers = new List<int> { 1, 2, 3, 4, 5, 6 };

// Seleziona i numeri pari e li raddoppia
var result = numbers
    .Where(n => n % 2 == 0)
    .Select(n => n * 2)
    .ToList();

// result contiene 4, 8, 12

Conclusione

Abbiamo esplorato gli strumenti che C# offre per lavorare con insiemi di dati: gli array a dimensione fissa, incluse le varianti multidimensionali, la manipolazione efficiente delle stringhe con StringBuilder, e le collezioni generiche List, Dictionary, HashSet, Queue e Stack, ciascuna adatta a uno scenario diverso. Abbiamo anche intravisto la potenza espressiva di LINQ. Nel prossimo articolo entreremo nel cuore del paradigma orientato agli oggetti, imparando a definire classi e a creare oggetti.