Iteratori

Scegliere un iteratore quando si desidera restituire una sequenza i cui elementi possono essere utilizzati uno alla volta dai chiamanti, senza prima compilare il risultato completo in una raccolta. Questo approccio si adatta ai valori generati, ai filtri con flusso di controllo personalizzato e alle sequenze che i chiamanti potrebbero smettere di leggere prima. Rispetto a una raccolta completata, un iteratore cambia quando viene eseguita l'elaborazione, non solo come la si scrive.

Tip

Questo articolo illustra gli iteratori sincroni per gli sviluppatori che conoscono metodi, cicli e collezioni. Per la sintassi completa yield e le restrizioni, vedere il yield riferimento dell'istruzione.

Un metodo iteratore produce una sequenza di valori uno alla volta. Questo articolo è incentrato sui metodi iteratori che restituiscono IEnumerable<T>, i cui valori vengono in genere usati dai chiamanti con un'istruzione foreach.

Produrre elementi con yield return

Usare yield return per fornire l'elemento successivo nella sequenza. L'iteratore seguente seleziona le letture di temperatura accettabili da un sensore. In questo scenario, -1 contrassegna la fine dei dati del sensore.

int[] readings = [21, 27, 24, -1, 19];

IEnumerable<int> acceptedReadings =
    SelectTemperatures(readings, maximum: 25);

Console.WriteLine("Sequence created.");

foreach (int temperature in acceptedReadings)
{
    Console.WriteLine($"Accepted: {temperature}°C");
}

static IEnumerable<int> SelectTemperatures(
    IEnumerable<int> readings,
    int maximum)
{
    foreach (int reading in readings)
    {
        Console.WriteLine($"Checking {reading}°C");

        if (reading == -1)
        {
            yield break;
        }

        if (reading <= maximum)
        {
            yield return reading;
        }
    }
}

SelectTemperatures restituisce IEnumerable<int>, che rappresenta una sequenza di int valori. Ciascun yield return reading fornisce un valore al ciclo foreach. Dopo che il chiamante gestisce tale valore, l'iteratore continua con l'input successivo.

Usare il formato generico IEnumerable<T> per il codice C# più moderno. L'argomento di tipo, ad esempio int, indica ai chiamanti quale tipo di elemento contiene la sequenza.

Terminare la sequenza con yield break

Utilizzare yield break per terminare un iteratore prima che il controllo raggiunga la fine del metodo. Nell'esempio, il valore -1 sentinel indica che non sono disponibili altre letture:

if (reading == -1)
{
    yield break;
}

L'iteratore termina anche naturalmente quando l'esecuzione raggiunge la fine del corpo. Usare yield break quando una condizione deve arrestare immediatamente la produzione di elementi.

Consumare un iteratore con foreach

Un iteratore espone la stessa struttura di molte raccolte .NET. Chi invoca può usare foreach senza sapere come la sequenza produce i relativi elementi:

foreach (int temperature in SelectTemperatures(readings, 25))
{
    Console.WriteLine($"Accepted: {temperature}°C");
}

Il chiamante richiede ogni elemento man mano che il ciclo avanza. L'iteratore mantiene la posizione tra le richieste e continua dopo il precedente yield return.

Comprendere la produzione differita degli elementi

La chiamata a un metodo iteratore non esegue immediatamente il corpo. Il metodo inizia a produrre elementi quando il chiamante inizia l'enumerazione. Questo comportamento viene chiamato esecuzione posticipata.

Nell'esempio viene stampato Sequence created. prima di qualsiasi Checking ... messaggio. L'iteratore inizia a controllare le letture solo quando il ciclo foreach richiede il primo elemento.

L'esecuzione posticipata ha effetti pratici:

  • Il chiamante può iniziare l'elaborazione prima che l'iteratore produa ogni elemento.
  • L'iteratore può arrestarsi in anticipo con yield break.
  • Un chiamante che non enumera mai la sequenza non esegue il corpo dell'iteratore.
  • Ogni nuova enumerazione esegue di nuovo l'iteratore.

Rendere chiara l'esecuzione posticipata nello scopo e nella documentazione del metodo. I lettori non possono dedurre i tempi di esecuzione da IEnumerable<T> soli perché sia gli iteratori che le raccolte possono esporre tale interfaccia. Mantenere la produzione degli elementi priva di effetti collaterali non correlati perché i chiamanti potrebbero enumerare la sequenza in un secondo momento o più volte. I messaggi di diagnostica dell'esempio illustrano tale tempistica.

Scegliere un iteratore o una raccolta

Restituisce un iteratore quando i valori arrivano naturalmente uno alla volta o quando il chiamante non richiede immediatamente il risultato completo. Restituisce una raccolta completata quando i chiamanti richiedono uno snapshot stabile, un accesso indicizzato o un'enumerazione ripetuta senza ricalcolare gli elementi.

Entrambi gli approcci possono restituire gli stessi elementi. L'iteratore lascia la produzione di elementi sotto il controllo del chiamante; un metodo che compila una raccolta completa quel lavoro prima di restituire.

Usare un ciclo ordinario quando si vuole eseguire un'azione per ogni elemento senza restituire una sequenza. Per un filtro o una trasformazione standard, preferire un'operazione LINQ, Where ad esempio o Select; scrivere un iteratore quando la logica di produzione di elementi personalizzata è più chiara come una serie di istruzioni.

I metodi iteratore compongono con LINQ perché entrambi usano IEnumerable<T>. Ad esempio, un chiamante può aggiungere un altro filtro:

var comfortableReadings = SelectTemperatures(readings, 25)
    .Where(static temperature => temperature >= 18);

L'iteratore e l'operazione LINQ rimangono differiti fino a quando il codice non enumera comfortableReadings.

Per le sequenze asincrone, vedere Generare e usare flussi asincroni. Per i modelli di enumerazione personalizzati e tutte le yield restrizioni, usare il riferimento del linguaggio.

Vedere anche