1. Due parole sulle collezioni
Finora abbiamo lavorato con gli array — è il modo più semplice per conservare più elementi dello stesso tipo. Ma in C# ci sono un sacco di collezioni più comode e potenti, che risolvono vari problemi.
Collezione — è un oggetto che può contenere un gruppo di altri oggetti. A differenza degli array, le collezioni spesso possono cambiare la loro dimensione dinamicamente, offrono metodi comodi per lavorare con i dati e sono ottimizzate per diversi scenari d'uso.
Tipi principali di collezioni (panoramica veloce):
List<T> — array dinamico che può crescere e restringersi:
List<string> names = new List<string>();
names.Add("Alessio");
names.Add("Maria");
Console.WriteLine(names[0]); // Alessio
Console.WriteLine(names.Count); // 2
Dictionary<TKey, TValue> — collezione di coppie "chiave-valore", dove ad ogni chiave corrisponde un valore:
Dictionary<string, int> ages = new Dictionary<string, int>();
ages["Alessio"] = 25;
ages["Maria"] = 30;
Console.WriteLine(ages["Alessio"]); // 25
Nota: negli esempi sopra usiamo le parentesi quadre [] per accedere agli elementi delle collezioni — proprio come con gli array! Questo è possibile grazie agli indexer, di cui parleremo oggi.
Questa è solo una prima occhiata alle collezioni — vedremo in dettaglio le loro possibilità, differenze e applicazioni nelle prossime lezioni. Ora è importante capire che le collezioni permettono di accedere ai loro elementi tramite parentesi quadre, e non è magia, ma un meccanismo speciale del linguaggio.
2. Indexer
Gli oggetti C# normali funzionano tramite proprietà e metodi. Ma che fare se il tuo oggetto è una specie di mini-collezione? Per esempio, immagina:
- Stai scrivendo una classe Week, che deve restituire il nome del giorno della settimana tramite numero: week[0] → "Lunedì".
- Oppure una classe Library, dove puoi accedere a un libro tramite il suo numero: library[3] → "Verso Swann".
Comodo, vero? Sarebbe strano scrivere library.GetBookByIndex(3) ogni volta — vuoi accedere al tuo oggetto come a un array!
Ed è qui che entrano in gioco gli indexer.
Indexer — è un membro speciale della classe che permette di usare gli oggetti di quella classe con la sintassi delle parentesi quadre, come gli array: obj[0], obj["chiave"], e così via.Un indexer sembra una proprietà, ma invece del nome — prende parametri tra parentesi quadre. È simile a quando scriviamo .Name, solo che qui usiamo [i].
3. Una collezione semplice con indexer
Mettiamo insieme una mini-classe che conserva i colori preferiti. Usiamo un array di stringhe per conservarli. Senza indexer dovresti fare un metodo tipo GetColor(int i). Ma con l'indexer:
using System;
public class FavoriteColors
{
// Campo privato per conservare i colori
private string[] colors = new string[5];
// Indexer:
public string this[int index]
{
get
{
// Controllo dei limiti dell'array (incapsulamento in azione!)
if (index < 0 || index >= colors.Length)
throw new IndexOutOfRangeException("Indice colore non valido!");
return colors[index];
}
set
{
if (index < 0 || index >= colors.Length)
throw new IndexOutOfRangeException("Indice colore non valido!");
colors[index] = value ?? throw new ArgumentNullException(nameof(value));
}
}
}
class Program
{
static void Main()
{
FavoriteColors favorites = new FavoriteColors();
favorites[0] = "Verde";
favorites[1] = "Blu";
favorites[2] = "Rosso";
favorites[10] = "Viola"; // Lancia eccezione!
Console.WriteLine(favorites[1]); // Blu
}
}
Cosa succede qui?
- Creiamo un array privato, così nessuno da fuori può giocarci direttamente.
- L'indexer è definito come public string this[int index], dove this — parola chiave che indica che l'indexer appartiene all'oggetto.
- Dentro get e set controlliamo il range, non lasciamo uscire dai limiti/scrivere null.
- Alla fine possiamo fare favorites[0], come con un array normale.
4. Sintassi dell'indexer: dettagli
La sintassi è simile a una proprietà, ma invece del nome (tipo Age) si usa la parola chiave this con parametri:
// Firma dell'indexer (schema generale)
[modificatore] TipoRisultato this[TipoIndice nomeIndice]
{
get { ... }
set { ... }
}
Esempio: Classico
public class MyCollection
{
private int[] data = new int[10];
// Indexer per lettura e scrittura
public int this[int index]
{
get { return data[index]; }
set { data[index] = value; }
}
}
Gli indexer non sono solo per int
La cosa bella: l'indexer non deve per forza accettare solo int. Puoi usare qualsiasi tipo (basta che abbia senso accedere tramite chiave):
public string this[string colorName]
{
get { /* ... */ }
set { /* ... */ }
}
Per esempio, in una rubrica telefonica ha senso cercare per nome:
public class PhoneBook
{
private Dictionary<string, int> entries = new Dictionary<string, int>();
public int this[string name]
{
get
{
if (entries.ContainsKey(name))
return entries[name];
return null;
}
set
{
entries[name] = value;
}
}
}
Delle collezioni e di come funziona Dictionary<string, string> parlerò nelle prossime lezioni :P
5. Esempio pratico: Contatore di parole in un testo
Continuiamo a sviluppare la nostra app. Supponiamo che ora abbiamo una classe che conta quante volte ogni parola appare in un testo. È comodo se l'utente può accedere all'oggetto tramite parentesi quadre per ottenere il conteggio per parola:
using System.Collections.Generic;
public class WordCounter
{
private Dictionary<string, int> counter = new Dictionary<string, int>();
// Indexer per stringa (parola)
public int this[string word]
{
get
{
if (counter.ContainsKey(word))
return counter[word];
return 0; // Se la parola non c'è, ritorna 0.
}
set
{
counter[word] = value;
}
}
// Metodo per contare le parole da una stringa
public void AddWords(string text)
{
foreach (var word in text.Split(' ', System.StringSplitOptions.RemoveEmptyEntries))
{
if (counter.ContainsKey(word))
counter[word]++;
else
counter[word] = 1;
}
}
}
// In Main:
var wc = new WordCounter();
wc.AddWords("mamma lavava finestra lavava mamma papà");
Console.WriteLine($"'mamma' appare {wc["mamma"]} volta(e)");
Console.WriteLine($"'finestra' appare {wc["finestra"]} volta(e)");
Console.WriteLine($"'gatto' appare {wc["gatto"]} volta(e)"); // 0
A cosa serve nella pratica? Questo approccio si usa spesso per implementare collezioni personalizzate, librerie di memoria, mapping (dizionari e indici) e persino DSL (linguaggi specializzati dentro C#).
6. Limiti e sfumature
Gli indexer sono potenti, ma ci sono alcune regole e trappole (ovvio!).
L'indexer non ha nome
A differenza di una proprietà, l'indexer non ha nome, solo la firma tipo this[tipoparametro]. Se provi a scrivere public int MyIndexer[int i] — il compilatore si arrabbia. Usiamo solo this.
Non esistono indexer statici
Gli indexer sono sempre creati per un'istanza della classe, non per membri statici. Quindi non puoi dichiarare static int this[int i] — la logica è che this punta sempre a una specifica istanza dell'oggetto.
Possono essere overloadati per tipo/numero di parametri
Puoi creare più indexer in una classe, se i loro parametri sono diversi per tipo o quantità. Per esempio:
public string this[int i] { get { ... } set { ... } }
public string this[string key] { get { ... } set { ... } }
È legale, il compilatore non si confonde — e ti avvisa se i parametri si sovrappongono.
Supporto solo tramite proprietà
Non puoi dichiarare un indexer senza accessor get o set. Se vuoi solo lettura — togli set, solo scrittura — get. Di solito si usano entrambi.
7. Utilità pratica e perché saperlo
- Gli indexer sono usati tantissimo nelle collezioni di dati. Molte classi .NET li hanno: per esempio, List<T>, Dictionary<TKey,TValue>. Quando scrivi list[2], stai usando proprio l'indexer!
- Gli indexer permettono di nascondere l'implementazione interna (incapsulamento!), ma offrire un'interfaccia comoda e familiare. L'utente della tua classe non pensa a come conservi i dati, usa solo le solite [index].
- Il tuo codice diventa compatto e intuitivo — cosa molto apprezzata ai colloqui (e dai tuoi futuri colleghi).
Proprietà e Indexer: confronto
| Proprietà | Indexer | |
|---|---|---|
| Nome | Sì (per esempio, Name) | No (invece del nome — this[parametro]) |
| Accesso | Per nome | Per indice (o altra chiave) |
| Statica | Può essere static | Solo per istanza |
| Più in una classe | Sì, quanti vuoi | Sì, ma con firma dei parametri diversa |
| Uso | Conservazione/accesso ai dati | Mini-collezioni, dati associativi |
8. Errori tipici e consigli
Errore n°1: tentare di fare un indexer static.
Non si può — this[...] funziona solo con l'oggetto.
Errore n°2: dimenticare il controllo dell'indice.
Se non controlli i limiti dell'array in get o set, il programma può crashare.
Errore n°3: tipi di parametri confusi.
Se crei due indexer con gli stessi parametri, il compilatore dà errore.
Errore n°4: dimenticare di implementare get o set.
Se vuoi accesso sia in lettura che in scrittura — entrambi devono essere implementati.
Consiglio: se la tua classe incapsula un array — passa semplicemente le chiamate all'array tramite l'indexer. Così il codice è più veloce e intuitivo.
9. Perché sapere tutto questo
Gli indexer rendono l'interfaccia dell'oggetto semplice, chiara e comoda. Permettono di nascondere l'implementazione interna, ma lasciare allo sviluppatore un modo pratico per accedere ai dati.
Proprio così sono fatte le collezioni integrate: string, List<T>, Dictionary<TKey, TValue>, Span<T> e molte altre. Quando scrivi array[2] o text[0], stai già usando un indexer.
E la cosa più importante — ora puoi scrivere le tue classi che funzionano in modo altrettanto flessibile e compatto. Quindi — sei un passo più vicino a scrivere codice professionale e leggibile.
GO TO FULL VERSION