1. Indicizzatori con più parametri
Un indicizzatore semplice (tipo this[int index]) è utile quando vuoi accedere agli elementi tramite un indice intero — quasi come in un array. Ma C# permette molto di più: puoi usare più parametri, parametri di tipi diversi, impostare modificatori di accesso diversi per get e set, implementare più indicizzatori nella stessa classe (se le firme sono diverse).
Inoltre, nelle nuove versioni di C# sono arrivate funzionalità extra che rendono la vita più facile quando lavori con gli indicizzatori, incluso un po' di syntactic sugar per scrivere codice più compatto.
Un indicizzatore non deve per forza accettare solo un indice e solo di tipo int. I parametri li decidi tu — e puoi averne diversi di tipi diversi, se ti serve per il tuo caso.
public class ChessBoard
{
private string[,] board = new string[8, 8];
// Indicizzatore con due parametri!
public string this[int row, int col]
{
get { return board[row, col]; }
set { board[row, col] = value; }
}
}
Ora possiamo scrivere codice così:
ChessBoard chess = new ChessBoard();
chess[0, 0] = "Torre";
chess[7, 7] = "Re";
string piece = chess[0, 0]; // "Torre"
Questo approccio è perfetto per matrici, mappe 2D, giochi da tavolo, collezioni complesse.
2. Indicizzatori con parametri di tipi diversi
Il tuo indicizzatore può accettare parametri non solo di tipo int, ma anche di qualsiasi altro tipo adatto. L'importante è che abbia senso per il tuo caso.
using System.Collections.Generic;
public class Employee
{
public string Name { get; set; }
public int Age { get; set; }
public string Position { get; set; }
}
public class EmployeeCollection
{
private List<Employee> employees = new List<Employee>();
// Indicizzatore per nome dipendente (string)
public Employee this[string name]
{
get
{
foreach (var employee in employees)
{
if (employee.Name == name)
return employee;
}
return null; // Oppure puoi lanciare un'eccezione
}
set
{
for (int i = 0; i < employees.Count; i++)
{
if (employees[i].Name == name)
{
employees[i] = value;
return;
}
}
// Se non esiste un dipendente con quel nome — aggiungiamo uno nuovo
employees.Add(value);
}
}
// Per compatibilità — indicizzatore per indice numerico
public Employee this[int index]
{
get { return employees[index]; }
set { employees[index] = value; }
}
}
var company = new EmployeeCollection();
company[0] = new Employee { Name = "Ivan", Age = 30, Position = "Programmatore" };
company[1] = new Employee { Name = "Maria", Age = 25, Position = "Designer" };
Employee employee = company["Ivan"];
company["Pietro"] = new Employee { Name = "Pietro", Age = 28, Position = "Tester" };
Importante: se hai più indicizzatori — le loro firme devono differire per numero e tipo di parametri.
3. Modificatori di accesso diversi per get e set
A volte vuoi permettere solo la lettura tramite indice, ma vietare la scrittura (o viceversa). In C# puoi impostare modificatori di accesso diversi per gli accessor get e set dell'indicizzatore.
public class SecureEmployeeCollection
{
private List<Employee> employees = new List<Employee>();
public Employee this[int index]
{
get { return employees[index]; }
internal set { employees[index] = value; }
}
}
Questo si usa spesso per proteggere le collezioni da modifiche non autorizzate, rendendo la classe più controllata.
4. Indicizzatori Read-only e Write-only
A volte vuoi permettere solo la lettura o solo la scrittura tramite indicizzatore.
public class ReadOnlyEmployeeCollection
{
private List<Employee> employees = new List<Employee>();
public Employee this[int index]
{
get { return employees[index]; }
// set assente — non puoi modificare!
}
}
public class WriteOnlyEmployeeCollection
{
private List<Employee> employees = new List<Employee>();
public Employee this[int index]
{
set
{
employees.Insert(index, value);
}
// get assente — non puoi leggere!
}
}
Nei progetti reali "write-only" si vede quasi mai: di solito servono i "read-only", ad esempio quando vuoi che dall'esterno si possa solo vedere ma non modificare i dati tramite indicizzatore.
5. Indicizzatori con controllo dei limiti e logica
Nei buoni esempi è importante non solo dare accesso tramite indice, ma anche gestire correttamente gli out-of-bounds, errori di ricerca e altre situazioni eccezionali.
public class SafeEmployeeCollection
{
private List<Employee> employees = new List<Employee>();
public Employee this[int index]
{
get
{
if (index < 0 || index >= employees.Count)
throw new IndexOutOfRangeException("Dipendente con questo indice non esiste!");
return employees[index];
}
set
{
if (index < 0 || index >= employees.Count)
throw new IndexOutOfRangeException("Non puoi sostituire un dipendente inesistente!");
employees[index] = value;
}
}
}
Puoi restituire null o usare pattern moderni per gestire valori mancanti — dipende dalla logica della tua app.
6. Indicizzatori con parametri insoliti
Puoi trovare collezioni dove l'indicizzatore accetta tipi non standard: enum (enum), strutture custom, anche più parametri di tipi diversi.
public enum Department { IT, HR, Finance, Marketing }
public class DepartmentEmployeeCollection
{
private Dictionary<Department , Employee> departmentLeads = new Dictionary<Department , Employee>();
public Employee this[Department department]
{
get { return departmentLeads.TryGetValue(department, out var employee) ? employee : null; }
set { departmentLeads[department] = value; }
}
}
var company = new DepartmentEmployeeCollection();
company[Department.IT] = new Employee { Name = "Anna", Age = 35, Position = "Responsabile IT" };
Employee itLead = company[Department.IT];
Questo approccio si usa quando hai identificatori unici o una chiara corrispondenza tra tipo e valore — comodo, chiaro e type-safe.
7. Funzionalità moderne: Range e Index
Con l'arrivo dei tipi Range e Index in C# 8 gli indicizzatori hanno nuove possibilità per lavorare con intervalli e indici dalla fine:
public class SmartArray
{
private int[] numbers = Enumerable.Range(0, 100).ToArray();
public int[] this[Range range] => numbers[range];
public int this[Index index] => numbers[index];
}
// Uso:
var smart = new SmartArray();
int[] middle = smart[20..30]; // dal 20° al 29° elemento
int last = smart[^1]; // ultimo elemento
Se implementi una tua collezione, il supporto per Range e Index la rende super "nativa" e comoda per gli sviluppatori.
8. Proprietà vs indicizzatori
Per capire meglio quando usare le proprietà e quando gli indicizzatori, è utile confrontare le loro caratteristiche.
- Le proprietà sono comode per accedere alle caratteristiche individuali di un oggetto per nome: person.Name, car.Speed.
- Gli indicizzatori sono adatti per accedere agli elementi di una collezione o struttura tramite chiave: employees[0], phoneBook["Ivan"].
- Una proprietà ha sempre un nome specifico e in una classe può esserci solo una proprietà con quel nome.
- L'indicizzatore usa la parola chiave this e può avere più varianti, se le firme sono diverse.
- Le proprietà possono essere statiche, gli indicizzatori no, perché this punta sempre a una specifica istanza dell'oggetto.
9. Errori tipici con gli indicizzatori
Errore n°1: manca il controllo dei limiti.
Se non controlli l'indice per out-of-bounds, puoi beccarti un IndexOutOfRangeException quando meno te lo aspetti.
Errore n°2: non gestisci il possibile null dall'indicizzatore.
Se l'indicizzatore per stringa restituisce null quando manca l'elemento, e il codice chiamante usa il risultato senza pensarci, arriva il NullReferenceException.
Errore n°3: firme duplicate degli indicizzatori.
C# non permette di creare due indicizzatori con gli stessi parametri.
Errore n°4: logica poco chiara nel set accessor.
Se nel set usi la logica "aggiungi se manca" invece di "sostituisci", può confondere. Queste scelte vanno rese esplicite e ben documentate.
10. Applicazione pratica e conclusione
Nei progetti veri gli indicizzatori si usano spesso per creare collezioni specializzate, cache, dizionari con logica extra, matrici e strutture dati multidimensionali. Rendono il codice più leggibile e intuitivo — invece di chiamare metodi tipo GetElementByIndex(5) puoi semplicemente scrivere collection[5].
Ricorda: gli indicizzatori devono avere senso per la natura della tua classe. Se la classe non è una collezione o una struttura dati, probabilmente non ti serve un indicizzatore. Ma se la tua classe gestisce un insieme di elementi, l'indicizzatore può renderne l'uso molto più comodo e naturale.
GO TO FULL VERSION