Raccolte
Il runtime .NET fornisce molti tipi di raccolta che archiviano e gestiscono gruppi di oggetti correlati. Alcuni tipi di raccolta, ad esempio System.Array, System.Span<T>e System.Memory<T> vengono riconosciuti nel linguaggio C#. Inoltre, le interfacce come System.Collections.Generic.IEnumerable<T> sono riconosciute nel linguaggio per enumerare gli elementi di una raccolta.
Le raccolte consentono di lavorare in modo più flessibile con gruppi di oggetti. È possibile classificare raccolte diverse in base a queste caratteristiche:
- Accesso agli elementi: ogni raccolta può essere enumerata per accedere a ogni elemento in ordine. Alcune raccolte accedono agli elementi per indice, ovvero la posizione dell'elemento in una raccolta ordinata. L'esempio più comune è System.Collections.Generic.List<T>. Altre raccolte accedono agli elementi in base alla chiave, in cui un valore è associato a una singola chiave. L'esempio più comune è System.Collections.Generic.Dictionary<TKey,TValue>. Scegliere tra questi tipi di raccolta in base al modo in cui l'app accede agli elementi.
- Profilo prestazioni: ogni raccolta ha profili di prestazioni diversi per azioni come l'aggiunta di un elemento, la ricerca di un elemento o la rimozione di un elemento. È possibile selezionare un tipo di raccolta in base alle operazioni più usate nell'app.
- Eseguire un aumento e una riduzione in modo dinamico: la maggior parte delle raccolte supporta l’aggiunta o la rimozione dinamica degli elementi. In particolare, Array, System.Span<T> e System.Memory<T> non lo fanno.
Oltre a queste caratteristiche, il runtime fornisce raccolte specializzate che impediscono l'aggiunta o la rimozione di elementi o la modifica degli elementi della raccolta. Altre raccolte specializzate offrono sicurezza per l'accesso simultaneo nelle app multithread.
Tutti i tipi di raccolta sono disponibili nelle informazioni di riferimento sulle API .NET. Per altre informazioni, vedere Tipi di raccolte comunemente utilizzate, e Selezione di una classe Collection.
Nota
Per gli esempi in questo articolo, potrebbe essere necessario aggiungere le direttive using per gli spazi dei nomi System.Collections.Generic
e System.Linq
.
Le matrici sono rappresentate da System.Array e hanno il supporto della sintassi nel linguaggio C#. Questa sintassi fornisce dichiarazioni più concise per le variabili di matrice.
System.Span<T> è un tipo ref struct
che fornisce uno snapshot su una sequenza di elementi senza copiare tali elementi. Il compilatore applica le regole di sicurezza per garantire che Span
non sia accessibile dopo che la sequenza a cui fa riferimento non è più nell'ambito. Viene usato in molte API .NET per migliorare le prestazioni. Memory<T> offre un comportamento simile quando non è possibile usare un tipo ref struct
.
A partire da C# 12, è possibile inizializzare tutti i tipi di raccolta usando un'espressione Collection.
Raccolte indicizzabili
Una raccolta indicizzabile è una raccolta in cui è possibile accedere a ogni elemento usando il relativo indice. Il relativo indice è il numero di elementi prima della sequenza. Pertanto, il riferimento all'elemento per indice 0
è il primo elemento, l'indice 1
è il secondo e così via. Questi esempi usano la classe List<T>. È la raccolta indicizzata più comune.
Nell'esempio seguente viene creato e inizializzato un elenco di stringhe, viene rimosso un elemento e viene aggiunto un elemento alla fine dell'elenco. Dopo ogni modifica, scorre le stringhe usando un'istruzione foreach o un ciclo for
:
// Create a list of strings by using a
// collection initializer.
List<string> salmons = ["chinook", "coho", "pink", "sockeye"];
// Iterate through the list.
foreach (var salmon in salmons)
{
Console.Write(salmon + " ");
}
// Output: chinook coho pink sockeye
// Remove an element from the list by specifying
// the object.
salmons.Remove("coho");
// Iterate using the index:
for (var index = 0; index < salmons.Count; index++)
{
Console.Write(salmons[index] + " ");
}
// Output: chinook pink sockeye
// Add the removed element
salmons.Add("coho");
// Iterate through the list.
foreach (var salmon in salmons)
{
Console.Write(salmon + " ");
}
// Output: chinook pink sockeye coho
Nell'esempio seguente vengono rimossi elementi da un elenco generico. Invece di un'istruzione foreach
viene usata un'istruzione for
che esegue l'iterazione in ordine decrescente. Ciò è necessario perché il metodo RemoveAt fa sì che gli elementi dopo un elemento rimosso abbiano un valore di indice inferiore.
List<int> numbers = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
// Remove odd numbers.
for (var index = numbers.Count - 1; index >= 0; index--)
{
if (numbers[index] % 2 == 1)
{
// Remove the element by specifying
// the zero-based index in the list.
numbers.RemoveAt(index);
}
}
// Iterate through the list.
// A lambda expression is placed in the ForEach method
// of the List(T) object.
numbers.ForEach(
number => Console.Write(number + " "));
// Output: 0 2 4 6 8
Per il tipo di elementi in List<T>, è possibile anche definire una classe personalizzata. Nell'esempio seguente la classe Galaxy
viene usata dall'oggetto List<T> definito nel codice.
private static void IterateThroughList()
{
var theGalaxies = new List<Galaxy>
{
new (){ Name="Tadpole", MegaLightYears=400},
new (){ Name="Pinwheel", MegaLightYears=25},
new (){ Name="Milky Way", MegaLightYears=0},
new (){ Name="Andromeda", MegaLightYears=3}
};
foreach (Galaxy theGalaxy in theGalaxies)
{
Console.WriteLine(theGalaxy.Name + " " + theGalaxy.MegaLightYears);
}
// Output:
// Tadpole 400
// Pinwheel 25
// Milky Way 0
// Andromeda 3
}
public class Galaxy
{
public string Name { get; set; }
public int MegaLightYears { get; set; }
}
Raccolte di coppie chiave/valore
Questi esempi usano la classe Dictionary<TKey,TValue>. È la raccolta di dizionari più comune. La raccolta generica consente di accedere agli elementi di una raccolta usando la chiave di ogni elemento. Ogni aggiunta al dizionario è costituita da un valore e dalla chiave associata corrispondente.
L'esempio seguente crea una raccolta Dictionary
ed esegue l'iterazione nel dizionario usando un'istruzione foreach
.
private static void IterateThruDictionary()
{
Dictionary<string, Element> elements = BuildDictionary();
foreach (KeyValuePair<string, Element> kvp in elements)
{
Element theElement = kvp.Value;
Console.WriteLine("key: " + kvp.Key);
Console.WriteLine("values: " + theElement.Symbol + " " +
theElement.Name + " " + theElement.AtomicNumber);
}
}
public class Element
{
public required string Symbol { get; init; }
public required string Name { get; init; }
public required int AtomicNumber { get; init; }
}
private static Dictionary<string, Element> BuildDictionary() =>
new ()
{
{"K",
new (){ Symbol="K", Name="Potassium", AtomicNumber=19}},
{"Ca",
new (){ Symbol="Ca", Name="Calcium", AtomicNumber=20}},
{"Sc",
new (){ Symbol="Sc", Name="Scandium", AtomicNumber=21}},
{"Ti",
new (){ Symbol="Ti", Name="Titanium", AtomicNumber=22}}
};
L'esempio seguente usa il metodo ContainsKey e la proprietà Item[] di Dictionary
per trovare rapidamente un elemento in base alla chiave. La proprietà Item
consente di accedere a un elemento nella raccolta elements
usando il codice elements[symbol]
in C#.
if (elements.ContainsKey(symbol) == false)
{
Console.WriteLine(symbol + " not found");
}
else
{
Element theElement = elements[symbol];
Console.WriteLine("found: " + theElement.Name);
}
L'esempio seguente usa invece il metodo TryGetValue per individuare rapidamente un elemento in base alla chiave.
if (elements.TryGetValue(symbol, out Element? theElement) == false)
Console.WriteLine(symbol + " not found");
else
Console.WriteLine("found: " + theElement.Name);
Iteratori
Un iteratore viene usato per eseguire un'iterazione personalizzata in una raccolta. Un iteratore può essere un metodo o una funzione di accesso get
. Un iteratore usa un'istruzione yield return per restituire ogni elemento della raccolta, uno alla volta.
Per chiamare un iteratore usare un'istruzione foreach. Ogni iterazione del ciclo foreach
chiama l'iteratore. Quando si raggiunge un'istruzione yield return
nell'iteratore, viene restituita un'espressione e viene mantenuta la posizione corrente nel codice. L'esecuzione viene ripresa a partire da quella posizione la volta successiva che viene chiamato l'iteratore.
Per altre informazioni, vedere Iteratori (C#).
Nell'esempio seguente viene usato un metodo iteratore. Il metodo iteratore dispone di un'istruzione yield return
all'interno di un ciclo for
. Nel metodo ListEvenNumbers
ogni iterazione del corpo dell'istruzione foreach
crea una chiamata al metodo iteratore, che procede all'istruzione yield return
successiva.
private static void ListEvenNumbers()
{
foreach (int number in EvenSequence(5, 18))
{
Console.Write(number.ToString() + " ");
}
Console.WriteLine();
// Output: 6 8 10 12 14 16 18
}
private static IEnumerable<int> EvenSequence(
int firstNumber, int lastNumber)
{
// Yield even numbers in the range.
for (var number = firstNumber; number <= lastNumber; number++)
{
if (number % 2 == 0)
{
yield return number;
}
}
}
LINQ e raccolte
È possibile usare LINQ (Language-Integrated Query) per accedere alle raccolte. Le query LINQ forniscono funzionalità di filtro, ordinamento e raggruppamento. Per altre informazioni, vedere Introduzione a LINQ in C#.
Nell'esempio seguente viene eseguita una query LINQ su un oggetto List
generico. La query LINQ restituisce una raccolta diversa che contiene i risultati.
private static void ShowLINQ()
{
List<Element> elements = BuildList();
// LINQ Query.
var subset = from theElement in elements
where theElement.AtomicNumber < 22
orderby theElement.Name
select theElement;
foreach (Element theElement in subset)
{
Console.WriteLine(theElement.Name + " " + theElement.AtomicNumber);
}
// Output:
// Calcium 20
// Potassium 19
// Scandium 21
}
private static List<Element> BuildList() => new()
{
{ new(){ Symbol="K", Name="Potassium", AtomicNumber=19}},
{ new(){ Symbol="Ca", Name="Calcium", AtomicNumber=20}},
{ new(){ Symbol="Sc", Name="Scandium", AtomicNumber=21}},
{ new(){ Symbol="Ti", Name="Titanium", AtomicNumber=22}}
};