Magazine

Custom Iterator

Luca Del Tongo

16/04/2007

In questo articolo esamineremo assieme le potenzialità degli iteratori in particolar modo di quelli custom che ben si adattano a specifiche esigenze applicative.

0%100%
per esprimere un voto è necessario registrarsi al sito

C#

XeDotNetIterator.zip ()

 

Quando abbiamo a che fare con insiemi o collezioni di elementi siamo abituati ad iterare su ciascun elemento dell' insieme andando magari a compiere delle azioni guidate dalla nostra logica applicativa; ad esempio supponiamo di voler ricercare  all' interno delle community .NET italiane i nominativi dei membri appartenenti alla community di XE.NET che hanno pubblicato almeno un articolo con un numero di voti superiori ad una certa soglia; potremmo scrivere un codice del tipo: 

   1: List<string> bestPosters = new List<string>();
   2: bestPosters = EvaluteBestPosters(DotNetCommunity, "XE.NET");
   3: foreach (string name in bestPosters)
   4:   {
   5:     Console.WriteLine(name);
   6:   } 
   7:  
   8:  
   9: public List<string> EvaluteBestPosters(DotNetCommunity XeDotNetCommunity, string communityName)
  10:  {
  11:     List<Member> xeMembers = XeDotNetCommunity.GetListFor(communityName);
  12:     List<string> bestPosters = new List<string>();
  13:     foreach (Member member in xeMembers)
  14:     {
  15:         foreach (Article article in member.articles)
  16:             {
  17:             if (article.numVotes > 5)
  18:              {
  19:                 bestPosters.Add(article.author.name);
  20:                 }
  21:             }
  22:     }
  23:     return bestPosters;
  24:  } 
  25:  
  26:   
  27: public List<Member> GetListFor(string communityName)
  28:  {
  29:       List<Member> xeMembers = new List<Member>();
  30:       foreach (Member member in members)
  31:        {
  32:         if (member.communityName==communityName)
  33:         {
  34:             xeMembers.Add(member);
  35:         }
  36:        }
  37:       return xeMembers;
  38:  }
 
 

Nel codice sopra definito gli step logici eseguiti per ottenere l' elenco dei nominativi di interesse sono i seguenti:

  1. Creazione della lista dei membri che appartengono alla community di XE.NET
  2. Iterazione per ogni membro appartenente alla lista dei membri di XE.NET sulla propria  lista di articoli
  3. Verifica del numero di voti ottenuto da ciascun articolo
  4. In caso che il numero di voti superi una certa soglia aggiunta del nomitivo dell' autore dell' articolo alla lista finale dei nominativi

Riflettiamo un attimo sugli step eseguiti e sugli eventuali problemi che un codice del genere può portare. Sicuramente tra tutti i membri appartenenti alle varie community .NET quelli che appartengono alla comunità di XE.NET saranno una frazione relativamente piccola ma comunque consistente; creando una lista locale xeMembers il Garbage Collector rilascerà le risorse allocate dalla lista solamente quando questa sarà stata processata per intero. Con l' utilizzo dei Custom Iterator possiamo avere maggiore controllo sul flusso di iterazione definibile per le nostre collezioni.

Cominciamo analizzando il pattern Iterator evidenziando come ogni volta che si ricorre all' utilizzo del costrutto foreach il compilatore si aspetta che l'espressione fornita risulti da un'implementazione dell' interfaccia IEnumerable oppure dell' equivalente generica IEnumerable<T>;  le interfacce IEnumerable<T> ed IEnumerable sono cosi definite:

   1: public interface IEnumerable<T> : IEnumerable
   2:     {
   3:         IEnumerator<T> GetEnumerator();     
   4:     }
   5: public interface IEnumerable
   6:     {   
   7:         IEnumerator GetEnumerator();
   8:     }

Queste due interfacce espongono quindi un enumeratore che permette di scorrere l'insieme del tipo per il quale viene specificato. Il compilatore quando si utilizza il costrutto foreach genera quindi del codice che utilizza l' implementazione fornita del metodo GetEnumerator(); definisce inoltre un ciclo che tiene traccia dell' elemento corrente, che viene restuituito al corpo del foreach, gestendone l' avanzamento tramite il metodo IEnumerator.MoveNext(). L' approccio che comunemente veniva adottato qualora fosse stato necessario fornire un meccanismo di iterazione prevedeva quindi la definizione di una classe apposita (solitamente nested) con il compito di fornire gli elementi al chiamante; segue un possibile esempio di tale implementazione: 

   1: public class NumberCollection : IEnumerable<int>
   2:      {
   3:         int[] nums;
   4:  
   5:         public int[] Nums
   6:         {
   7:             get { return nums; }
   8:             set { nums = value; }
   9:         }
  10:  
  11:         public NumberCollection(int[] numbers)
  12:         {
  13:             Nums = numbers;
  14:         }
  15:  
  16:         public IEnumerator<int> GetEnumerator()
  17:         {
  18:             return new IntegerEnumerator(nums);
  19:         }
  20:  
  21:         IEnumerator IEnumerable.GetEnumerator()
  22:         {
  23:             return this.GetEnumerator();
  24:         }
  25:  
  26:         #region Classe Nested che implementa la logica dell'enumeratore
  27:         private class IntegerEnumerator : IEnumerator<int>
  28:         {
  29:             int index = 0;
  30:             int[] internalCollection;
  31:  
  32:             public IntegerEnumerator(int[] items)
  33:             {
  34:                 this.internalCollection = items;
  35:                 this.index = -1;
  36:             }
  37:  
  38:             public int Current
  39:             {
  40:                 get { return internalCollection[index]; }
  41:             }
  42:  
  43:             public bool MoveNext()
  44:             {
  45:                 if (index < internalCollection.GetLength(0) - 1)
  46:                 {
  47:                     index++;
  48:                     return true;
  49:                 }
  50:  
  51:                 return false;
  52:             }
  53:  
  54:             public void Dispose() { }
  55:  
  56:             object IEnumerator.Current
  57:             {
  58:                 get
  59:                 {
  60:  
  61:                     return Current;
  62:                 }
  63:             }
  64:  
  65:             public void Reset()
  66:             {
  67:                 index = -1;
  68:             }
  69:         } 
  70:         #endregion
  71:      }
 

Sebbene questo codice non sia eccessivamente prolisso, manca di flessibilità, in quanto non è possibile definire su di esso un comportamento parametrico in modo tale che il chiamante, possa scorrere gli elementi secondo una propria logica (ad esempio in ordine inverso).

Ecco che a questo punto ci vengono in ausilio i generatori introdotti in C# 2.0. Per definizione un generatore è una categoria speciale di iteratore in cui l'oggetto contenitore non è realizzato pienamente. Ciò permette di elaborare un elemento alla volta di collezioni astratte o addirittura infinite. I generatori sono comuni nei linguaggi di programmazione funzionali, o nei linguaggi che prendono in prestito alcuni concetti funzionali. I generatori sono spesso implementati in termini di continuazioni.

L' implementazione di un generatore in C# 2.0 prevede l'esecuzione di due passi principali:

  1. Scrittura di una funzione il cui tipo di ritorno sia  IEnumerable oppure IEnumerable<T>.
  2. Utilizzo all' interno della funzione definita al punto 1 delle keywords yield return oppure yield break rispettivamente per restituire un valore al chiamante oppure per abbandonare l'iterazione.

Fatto ciò sara carico del compilatore tradurre il codice in una classe simile a quella definita in precedenza rendendo possibile per il chiamante utilizzare su di essa il costrutto foreach. Dietro le quinte questo è possibile in quanto dopo che ogni valore viene resituito (con l'istruzione yield return), lo stato della funzione viene congelato. Quando la funzione viene invocata nuovamente, l'esecuzione riprende dal punto in cui l'istruzione yield return l'aveva abbandonata, con tutte le variabili della funzione nello stato in cui si trovavano.

Tornando all' esempio mostrato ad inizio articolo vediamo come possiamo sfruttare le potenzialità dei Custom Iterator (basati sui generatori) per aumentarne flessibilità e performance adottando nell' affrontare il problema una strategia alternativa. Analizziamo il seguente codice:  

   1: foreach (String member in DotNetCommunity.MembersBestPoster("XE.NET"))
   2:             {
   3:                 Console.WriteLine(member);
   4:             }   
   5:  
   6:  
   7: public IEnumerable<string> MembersBestPoster(string communityName)
   8:        {
   9:  
  10:            IEnumerable<Member> xeMembers = this.FindListFor(communityName);
  11:            IEnumerable<Article> xeMemberArticle;
  12:            foreach (Member member in xeMembers)
  13:            {
  14:  
  15:                xeMemberArticle = member.GetArticles();
  16:                foreach (Article art in xeMemberArticle)
  17:                {
  18:                    if (OvercomeThreshold(art))
  19:                    {
  20:                        yield return art.author.name;
  21:                    }
  22:                }
  23:            }
  24:        }
  25:   public IEnumerable<Member> FindListFor(string communityName)
  26:         {
  27:             foreach (Member member in members)
  28:             {
  29:                 if (member.communityName==communityName)
  30:                 {
  31:                     yield return member;
  32:                 }
  33:             }
  34:         
  35:         }
  36:         
  37:   private bool OvercomeThreshold(Article article)
  38:         {
  39:             return (article.numVotes > 5 ? true : false);
  40:         }

 

Adesso gli step logici con cui eseguiamo la ricerca dei nominativi finale risulta modificata in quanto:

  1. Viene letto un membro appartenente ad una community .NET

  2. Viene verificata l'appartenenza alla community XE.NET

  3. Viene letto un articolo appartenente al membro sopra ottenuto

  4. Verifica del numero di voti ottenuti dall'articolo sotto esame

  5. In caso che il numero di voti superi una certa soglia aggiunta del nomitivo dell' autore dell' articolo alla lista finale dei nominativi

Con questa nuova strategia, basata sull'utilizzo di Custom Iterator, sia per recuperare l' elenco dei membri appartenenti alle varie community .NET che per scorrerne l'elenco degli articoli pubblicati, possiamo evidenziare come in memoria siano presenti contemporaneamente solamente un' istanza di un membro della community ed un'istanza di un articolo: questo nuovo approccio permette sia una gestione della memoria più efficiente che la possibilità di scorrere gli elementi in modo custom. 

Commenti
Nome

Sito web
Commento


indietro