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.
  • Aumentare e ridurre dinamicamente: la maggior parte delle raccolte supporta l'aggiunta o la rimozione di elementi in modo dinamico. 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}}
    };