Aprile 2017

Volume 32 Numero 4

Il presente articolo è stato tradotto automaticamente.

Concetti essenziali su .NET - Informazioni sugli iteratori personalizzati e sugli elementi interni foreach C# con yield

Da Mark Michaelis

Mark MichaelisQuesto mese mi occuperò di esplorare i meccanismi interni di un costrutto di base di c# che noi programmare con frequenza, l'istruzione foreach. Data la comprensione del comportamento interno foreach, è possibile quindi esplorare implementare le interfacce di raccolta di foreach utilizzando l'istruzione yield, come spiegherò.

Sebbene sia facile da codificare l'istruzione foreach, mi sorprendente come alcuni sviluppatori comprendano come funziona internamente. Ad esempio, sono al corrente che funziona foreach in modo diverso per le matrici sugli insiemi IEnumerable < T >? Come si conoscono la relazione tra IEnumerator < T > e IEnumerable < T >? E, se si conoscono le interfacce enumerabili, si ha familiarità con l'implementazione di queste utilizza yield? 

Ciò che rende una classe di una raccolta

Per definizione, una raccolta all'interno di Microsoft .NET Framework è una classe che, come minimo, implementa IEnumerable < T > (o il tipo non generico IEnumerable). Questa interfaccia è fondamentale poiché l'implementazione dei metodi di IEnumerable < T > è il requisito minimo necessario per supportare l'iterazione su una raccolta.

La sintassi dell'istruzione foreach è semplice e consente di evitare le complicazioni correlate a dover conoscere quanti elementi sono disponibili. Il runtime non supporta direttamente l'istruzione foreach, tuttavia. Al contrario, il compilatore c# trasforma il codice come descritto nelle sezioni successive.

foreach con matrici: Di seguito viene illustrato un ciclo foreach semplice l'iterazione su una matrice di interi e la stampa ogni valore integer nella console:

int[] array = new int[]{1, 2, 3, 4, 5, 6};
foreach (int item in array)
{
  Console.WriteLine(item);
}

Da questo codice, il compilatore c# crea un equivalente CIL del ciclo for:

int[] tempArray;
int[] array = new int[]{1, 2, 3, 4, 5, 6};
tempArray = array;
for (int counter = 0; (counter < tempArray.Length); counter++)
{
  int item = tempArray[counter];
  Console.WriteLine(item);
}

In questo esempio, si noti che foreach si basa sul supporto per la proprietà di lunghezza e l'operatore di indice ([]). Con la proprietà di lunghezza, il compilatore c# è possibile utilizzare l'istruzione scorrere ogni elemento della matrice.

foreach con IEnumerable < T >: Anche se il codice precedente funziona bene in matrici di lunghezza fissa e l'operatore di indice è sempre supportato, non tutti i tipi di raccolte hanno un numero noto di elementi. Inoltre, molte delle classi di raccolta, tra cui Stack < T >, Queue < T > e Dictionary < TKey e TValue >, non supportano il recupero di elementi in base all'indice. Pertanto, è necessario un approccio più generico di scorrere le raccolte di elementi. Il modello di iteratore offre questa funzionalità. Supponendo che è possibile determinare il primo, è necessario successivo e l'ultimo elemento, conoscere il numero e il supporto di recupero di elementi in base all'indice.

Le interfacce non generiche System.Collections.IEnumerator System.Collections.Generic.IEnumerator < T > sono progettate per consentire il modello di iteratore per scorrere le raccolte di elementi, anziché il criterio di lunghezza indice indicato in precedenza. Verrà visualizzato un diagramma classi delle loro relazioni figura 1.

Un diagramma di classi di IEnumerator e IEnumerator interfacce
Figura 1 diagramma classi delle interfacce IEnumerator e IEnumerator < T >

Interfaccia IEnumerator, in cui IEnumerator < T > deriva da, include tre membri. Il primo è bool MoveNext. Utilizzando questo metodo, è possibile spostare da un elemento all'interno della raccolta al successivo, mentre nella stessa ora rilevare quando è stato enumerato mediante ogni elemento. Il secondo membro, una proprietà di sola lettura denominata corrente, restituisce l'elemento attualmente nel processo. Di IEnumerator < T >, che fornisce un'implementazione specifica del tipo di tale sovraccarico corrente. Con questi due membri della classe di raccolta, è possibile scorrere la raccolta utilizzando semplicemente un po' di tempo ciclo:

System.Collections.Generic.Stack<int> stack =
  new System.Collections.Generic.Stack<int>();
int number;
// ...
// This code is conceptual, not the actual code.
while (stack.MoveNext())
{
  number = stack.Current;
  Console.WriteLine(number);
}

In questo codice, il metodo MoveNext restituisce false quando si sposta oltre la fine della raccolta. Questa opzione sostituisce la necessità di conteggiare gli elementi durante il ciclo.

(Il metodo Reset generalmente genera l'eccezione NotImplementedException, in modo che non dovrebbe mai essere chiamato. Se è necessario riavviare un'enumerazione, creare un enumeratore aggiornato.)

Nell'esempio precedente è stato illustrato il concetto di output del compilatore c#, ma in realtà non viene compilato in questo modo quanto include due importanti dettagli riguardanti l'implementazione: interfoliazione e la gestione degli errori.

Lo stato condiviso: Il problema con un'implementazione simile a quella dell'esempio precedente è che se due tali cicli interleave tra loro, ovvero un foreach in un'altra, sia utilizzando la stessa raccolta, la raccolta deve mantenere un indicatore di stato dell'elemento corrente in modo che quando si chiama MoveNext, è possibile determinare l'elemento successivo. In tal caso, può influire sulle ciclo con interfoliazione uno a altro. (Lo stesso vale di cicli eseguiti da più thread.)

Per risolvere questo problema, le classi di raccolta non supportano direttamente le interfacce IEnumerator e IEnumerator < T >. È invece una seconda interfaccia, denominata IEnumerable < T >, il cui unico metodo è GetEnumerator. Lo scopo di questo metodo è di restituire un oggetto che supporta IEnumerator < T >. Anziché la classe di raccolta mantenere lo stato, una classe diversa, in genere una classe annidata in modo che abbia accesso agli elementi interni della raccolta, ovvero supporterà l'interfaccia IEnumerator < T > e manterrà lo stato del ciclo di iterazione. L'enumeratore è come un "cursore" o "segnalibro" nella sequenza. È possibile avere più segnalibri, e lo spostamento di uno di essi enumera la raccolta indipendentemente dagli altri. Utilizzando questo modello, l'equivalente di un ciclo foreach c# avrà un aspetto simile al codice illustrato figura 2.

Figura 2 enumeratore separato mantenimento dello stato durante un'iterazione

System.Collections.Generic.Stack<int> stack =
  new System.Collections.Generic.Stack<int>();
int number;
System.Collections.Generic.Stack<int>.Enumerator
  enumerator;
// ...
// If IEnumerable<T> is implemented explicitly,
// then a cast is required.
// ((IEnumerable<int>)stack).GetEnumerator();
enumerator = stack.GetEnumerator();
while (enumerator.MoveNext())
{
  number = enumerator.Current;
  Console.WriteLine(number);
}

Pulizia nell'iterazione successiva: Dato che le classi che implementano l'interfaccia IEnumerator < T > mantengono lo stato, a volte è necessario pulire lo stato al termine del ciclo (poiché completate tutte le iterazioni o viene generata un'eccezione). A tale scopo, l'interfaccia IEnumerator < T > deriva da IDisposable. Gli enumeratori che implementano l'interfaccia IEnumerator non necessariamente implementano IDisposable, ma in questo caso, verrà chiamato il metodo Dispose, nonché. In questo modo la chiamata al metodo Dispose dopo l'uscita dal ciclo foreach. Il linguaggio c# equivalente del codice CIL finale, pertanto, sembra figura 3.

Figura 3 compilati risultato foreach sulle raccolte

System.Collections.Generic.Stack<int> stack =
  new System.Collections.Generic.Stack<int>();
System.Collections.Generic.Stack<int>.Enumerator
  enumerator;
IDisposable disposable;
enumerator = stack.GetEnumerator();
try
{
  int number;
  while (enumerator.MoveNext())
  {
    number = enumerator.Current;
    Console.WriteLine(number);
  }
}
finally
{
  // Explicit cast used for IEnumerator<T>.
  disposable = (IDisposable) enumerator;
  disposable.Dispose();
  // IEnumerator will use the as operator unless IDisposable
  // support is known at compile time.
  // disposable = (enumerator as IDisposable);
  // if (disposable != null)
  // {
  //   disposable.Dispose();
  // }
}

Si noti che l'interfaccia IDisposable è supportato per l'utilizzo di IEnumerator < T >, istruzione può semplificare il codice in figura 3 a quello mostrato figura 4.

Figura 4 Error Handling e pulitura delle risorse con

System.Collections.Generic.Stack<int> stack =
  new System.Collections.Generic.Stack<int>();
int number;
using(
  System.Collections.Generic.Stack<int>.Enumerator
    enumerator = stack.GetEnumerator())
{
  while (enumerator.MoveNext())
  {
    number = enumerator.Current;
    Console.WriteLine(number);
  }
}

Tuttavia, è importante ricordare che l'elenco CIL non supporta direttamente l'utilizzo (parola chiave). Di conseguenza, il codice nella figura 3 è effettivamente una rappresentazione più precisa c# del codice CIL foreach.

foreach senza IEnumerable: C# non richiede che implementare IEnumerable o IEnumerable < T > per eseguire un'iterazione su un tipo di dati di utilizzo di foreach. Piuttosto, il compilatore Usa un concetto noto come duck digitando quanto segue: la ricerca di un metodo GetEnumerator che restituisce un tipo con una proprietà corrente e un metodo MoveNext. Duck digitando implica la ricerca per nome piuttosto che basarsi su un'interfaccia o una chiamata di metodo esplicita al metodo. (Il nome "duck typing" proviene l'idea originale che per essere considerate come un'anatra, l'oggetto deve solo implementare un metodo Quack; non è necessario implementare un'interfaccia IDuck). Duck digitando non riesce a trovare un'implementazione del modello enumerabile appropriata, il compilatore verifica se la raccolta implementa le interfacce.

Introduzione a iteratori

Dopo avere appreso i meccanismi interni dell'implementazione foreach, è necessario illustrare come gli iteratori sono utilizzati per creare implementazioni personalizzate di IEnumerator < T >, IEnumerable < T > e interfacce non generiche corrispondenti per le raccolte personalizzate. Gli iteratori forniscono una sintassi chiara per specificare come eseguire l'iterazione sui dati in classi di raccolta, soprattutto se si utilizzano il ciclo foreach, consentendo agli utenti finali di una raccolta passare la struttura interna senza alcuna conoscenza di tale struttura.

Il problema con il modello di enumerazione è che possono essere difficile da implementare manualmente perché è necessario mantenere tutti gli stati necessari descrivere la posizione corrente nella raccolta. Questo stato interno potrebbe essere semplice per una classe di tipo elenco raccolta. è sufficiente l'indice della posizione corrente. Al contrario, per le strutture di dati che richiedono l'attraversamento ricorsiva, ad esempio alberi binari, lo stato può essere piuttosto complicato. Per attenuare i problemi associati all'implementazione di questo modello, in c# 2.0 è stata aggiunta la parola chiave contestuale yield per rendere più semplice per una classe determinare come il ciclo foreach scorre il contenuto.

Definizione di un iteratore: iteratori consentono di implementare i metodi di una classe, e sono sintattici tasti di scelta rapida per il modello di enumeratore più complesso. Quando il compilatore c# rileva un iteratore, questo si espande il contenuto nel codice CIL che implementa il modello di enumeratore. Di conseguenza, non esistono dipendenze runtime per l'implementazione di iteratori. Poiché il compilatore c# gestisce l'implementazione tramite la generazione di codice CIL, non esiste alcun miglioramento delle prestazioni di runtime reale per l'utilizzo di iteratori. Tuttavia, esiste un miglioramento della produttività programmatore sostanziali nella scelta iteratori tramite l'implementazione manuale del modello di enumeratore. Per comprendere questo miglioramento, sarà considerata come un iteratore è definito nel codice.

Iteratore sintassi: Un iteratore fornisce un'implementazione di una sintassi abbreviata di interfacce iteratore, la combinazione delle interfacce di IEnumerator < T > e IEnumerable < T >. Figura 5 dichiara un iteratore per il tipo generico BinaryTree < T > tramite la creazione di un metodo GetEnumerator (sebbene, senza alcuna implementazione ancora).

Figura 5 iteratore interfacce modello

using System;
using System.Collections.Generic;
public class BinaryTree<T>:
  IEnumerable<T>
{
  public BinaryTree ( T value)
  {
    Value = value;
  }
  #region IEnumerable<T>
  public IEnumerator<T> GetEnumerator()
  {
    // ...
  }
  #endregion IEnumerable<T>
  public T Value { get; }  // C# 6.0 Getter-only Autoproperty
  public Pair<BinaryTree<T>> SubItems { get; set; }
}
public struct Pair<T>: IEnumerable<T>
{
  public Pair(T first, T second) : this()
  {
    First = first;
    Second = second;
  }
  public T First { get; }
  public T Second { get; }
  #region IEnumerable<T>
  public IEnumerator<T> GetEnumerator()
  {
    yield return First;
    yield return Second;
  }
  #endregion IEnumerable<T>
  #region IEnumerable Members
  System.Collections.IEnumerator
    System.Collections.IEnumerable.GetEnumerator()
  {
    return GetEnumerator();
  }
  #endregion
  // ...
}

Restituire i valori da un iteratore: Le interfacce di iteratore sono come le funzioni, ma anziché restituire un singolo valore, producono una sequenza di valori, uno alla volta. Nel caso di BinaryTree < T >, l'iteratore restituisce una sequenza di valori dell'argomento di tipo fornito per T. Se viene utilizzata la versione non generica di IEnumerator, i valori prodotti verranno invece essere di tipo object.

Per implementare correttamente il modello di iteratore, è necessario mantenere uno stato interno per tenere traccia della posizione corrente durante l'enumerazione dell'insieme. Nel caso BinaryTree < T >, per rilevare gli elementi all'interno della struttura sono già stati enumerati e che sono ancora disponibili. Gli iteratori sono trasformati dal compilatore in una "macchina" che tiene traccia della posizione corrente e sa come spostare "stesso" alla posizione successiva.

L'istruzione yield return produce un valore ogni volta che un iteratore incontra. controllo viene restituito immediatamente al chiamante che ha richiesto l'elemento. Quando il chiamante richiede l'elemento successivo, il codice inizia a eseguire immediatamente dopo il rendimento eseguito in precedenza istruzione return. In figura 6, le parole chiave dei dati incorporati in c# vengono restituite in modo sequenziale.

Figura 6 cede il controllo in modo sequenziale alcune parole chiave c#

using System;
using System.Collections.Generic;
public class CSharpBuiltInTypes: IEnumerable<string>
{
  public IEnumerator<string> GetEnumerator()
  {
    yield return "object";
    yield return "byte";
    yield return "uint";
    yield return "ulong";
    yield return "float";
    yield return "char";
    yield return "bool";
    yield return "ushort";
    yield return "decimal";
    yield return "int";
    yield return "sbyte";
    yield return "short";
    yield return "long";
    yield return "void";
    yield return "double";
    yield return "string";
  }
    // The IEnumerable.GetEnumerator method is also required
    // because IEnumerable<T> derives from IEnumerable.
  System.Collections.IEnumerator
    System.Collections.IEnumerable.GetEnumerator()
  {
    // Invoke IEnumerator<string> GetEnumerator() above.
    return GetEnumerator();
  }
}
public class Program
{
  static void Main()
  {
    var keywords = new CSharpBuiltInTypes();
    foreach (string keyword in keywords)
    {
      Console.WriteLine(keyword);
    }
  }
}

I risultati di figura 6 vengono visualizzati figura 7, ovvero un elenco dei tipi incorporati in c#.

Figura 7 elenco di Output alcune parole chiave c# dal codice nella figura 6

object
byte
uint
ulong
float
char
bool
ushort
decimal
int
sbyte
short
long
void
double
string

Chiaramente, è necessaria ulteriore spiegazione ma sono spazio insufficiente per questo mese in modo lascio è lettore col fiato sospeso per un'altra colonna. Basti a dire, con gli iteratori magicamente creare raccolte come proprietà, come illustrato nella figura 8, in questo caso, basarsi su c# 7.0 tuple solo per il gusto di farlo. Per coloro che desiderano lookahead, è possibile estrarre il codice sorgente o consultare il capitolo 16 del mio libro "essenziale c#".

Figura 8 utilizzo yield return implementare IEnumerable < T > proprietà

IEnumerable<(string City, string Country)> CountryCapitals
{
  get
  {
    yield return ("Abu Dhabi","United Arab Emirates");
    yield return ("Abuja", "Nigeria");
    yield return ("Accra", "Ghana");
    yield return ("Adamstown", "Pitcairn");
    yield return ("Addis Ababa", "Ethiopia");
    yield return ("Algiers", "Algeria");
    yield return ("Amman", "Jordan");
    yield return ("Amsterdam", "Netherlands");
    // ...
  }
}

Conclusioni

In questo articolo incrementata passo passo alla funzionalità che è stato parte di c# fin dalla versione 1.0 e non è stato modificato significativamente dall'introduzione dei generics in c# 2.0. Nonostante l'uso frequente di questa funzionalità, tuttavia, molti non comprendere i dettagli di ciò che viene eseguita internamente. Quindi un assaggio del modello iteratore, sfruttando il rendimento restituisca costrutto e fornito un esempio.

Gran parte di questa colonna è stata estratta dal mio libro "essenziale c#" (IntelliTect.com/EssentialCSharp), che sono attualmente durante l'aggiornamento a "Essenziale c# 7.0." Per ulteriori informazioni, vedere capitoli 14 e 16.


Mark Michaelisè fondatore di IntelliTect, dove ha serve come responsabile dell'architettura tecnico e istruttore. Per quasi due decenni lavora un Microsoft MVP e un Microsoft Regional Director poiché 2007. Michaelis viene utilizzato in diversi software progettazione revisione team Microsoft, tra cui c#, Microsoft Azure, SharePoint e Visual Studio ALM. Ha come relatore a conferenze per gli sviluppatori e ha scritto numerosi libri, tra cui il suo più recente, "Essential c# 6.0 (5 ° edizione)" (itl.tc/EssentialCSharp). È possibile contattarlo su Facebook al facebook.com/Mark.Michaelis, sul suo blog all'indirizzo IntelliTect.com/Mark, su Twitter: @markmichaelis o tramite posta elettronica all'indirizzo mark@IntelliTect.com.

Grazie per i seguenti esperti tecnici IntelliTect per la revisione dell'articolo: Kevin Bost


Viene illustrato in questo articolo nel forum di MSDN Magazine