Approfondimento su LINQ - il metodo Where pt 2
Continuiamo con l'analisi del metodo LinqToSQL Where. La prima parte può essere trovata qui
Cominciamo col riportare il codice del metodo:
public static IEnumerable<TSource> Where<TSource>(this IEnumerable<TSource> source, Func<TSource, bool> predicate) { if (source == null) throw Error.ArgumentNull("source"); if (predicate == null) throw Error.ArgumentNull("predicate"); if (source is Iterator<TSource>) return ((Iterator<TSource>)source).Where(predicate); if (source is TSource[]) return new WhereArrayIterator<TSource>((TSource[])source, predicate); if (source is List<TSource>) return new WhereListIterator<TSource>((List<TSource>)source, predicate); return new WhereEnumerableIterator<TSource>(source, predicate); }
Analogamente all'altro overload la prima operazione effettuata è la validazione degli argomenti del metodo.
Successivamente troviamo tre if che testano se source è un particolare tipo di collezione:
- Iterator
- array
- list
Se falliscono tutti e tre i controlli viene chiamato un metodo generico valido per tutti gli altri tipi di collezioni.
Nel caso in cui "source" sia uno dei tipi testati vengono chiamati dei metodi ottimizzati per la specifica collezione. Se non avete mai visto la classe Iterator<T> non preoccupatevi è una classe astratta e privata contenuta in Enumerable.cs ad uso interno della quale vedremo i dettagli ora.
Le classi WhereEnumerableIterator, WhereListIterator e WhereArrayIterator derivano da Iterator, guardiamo quindi quest'ultima.
abstract class Iterator<TSource> : IEnumerable<TSource>, IEnumerator<TSource> { int threadId; internal int state; internal TSource current; public Iterator() { threadId = Thread.CurrentThread.ManagedThreadId; } public TSource Current { get { return current; } } public abstract Iterator<TSource> Clone(); public virtual void Dispose() { current = default(TSource); state = -1; } public IEnumerator<TSource> GetEnumerator() { if (threadId == Thread.CurrentThread.ManagedThreadId && state == 0) { state = 1; return this; } Iterator<TSource> duplicate = Clone(); duplicate.state = 1; return duplicate; } public abstract bool MoveNext(); public abstract IEnumerable<TResult> Select<TResult>(Func<TSource, TResult> selector); public abstract IEnumerable<TSource> Where(Func<TSource, bool> predicate); object IEnumerator.Current { get { return Current; } } IEnumerator IEnumerable.GetEnumerator() { return GetEnumerator(); } void IEnumerator.Reset() { throw new NotImplementedException(); } }
Osserviamo subito che la classe implementa le interfacce Ienumerable e ienumerator. Può sembrare strano perché ienumerable contiene un solo metodo che dipende da ienumerator e che logicamente può essere una istanza di un'altra classe ma ai fini di "risparmiare" lavoro al gcd è utile creare meno istanze di oggetti possibile e visto che l'utilizzo principale della interfaccia ienumerable è recuperare il corrispondente ienumerator ha senso riutilizzare l'istanza dell'oggetto stesso.
Per ottenere tutto questo è necessario:
- implementare l'interfaccia Ienumerator
- implementare l'interfaccia ienumerable, quindi i due metodi, generico e no, getenumerator i quali restituiranno l'istanza stessa.
Cosa succede se viene chiamato più volte il metodo getenumerator? Il framework si occupa anche di questo e in realtà restituisce l'istanza stessa della classe solo la prima volta (o dopo un dispose) che viene chiamato il metodo se, in aggiunta, viene chiamato dallo stesso thread che ha creato l'istanza della classe.
La responsabilità di completare l'implementazione delle interfacce viene lasciata alle classi concrete che dovranno obbligatoriamente definire il movenext.
Altri metodi vengono aggiunti e marcati abstract:
- where
- select
- clone
Qual'è quindi il funzionamento del metodo Where?
Chiamato per la prima volta su un'istanza dell'interfaccia ienumerable sicuramente non entrerà nel primo If ma in uno dei altri tre rami del codice. Le volte successive in cui verrà chiamato sulla stessa istanza invece l'oggetto sarà in realtà un'istanza della classe enumerator e quindi entrerà sempre nella prima If.
Le tre classi in esame implementano i metodi abstract della classe Iterator. Il movenext è lo stesso per WhereListIterator e WhereEnumerableIterator, è diverso per l'array in quanto non viene usato un iteratore ma un più semplice for. Apparentemente non ci sono differenze tra le prime due classi e quindi non è evidente l'esigenza di separarle. Forse nell'esame di altri metodi troveremo il motivo.
È importante notare l'implementazione del metodo Where nelle tre diverse classi. Infatti chiamando più volte il metodo Where le classi si occupano di combinare i predicati in uno solo. Questi significa che indipendentemente dal numero di Where chiamate a catena ogni elemento della collezione iniziale viene testato una sola volta con un predicato che racchiude tutti quelli delle varie chiamate. Osserviamo che un'ottimizzazione di questo tipo non è possibile nell'altro overload analizzato.
Applicando una Where con la condizione che dipende anche dall'indice dell'elemento è chiaro cosa fare ma quando ne concateniamo due l'indice che deve essere passato alla seconda Where quale deve essere? Quello che l'elemento aveva nella successione iniziale o quello che ha nella successione risultato della prima Where? Logicamente vale la seconda ipotesi, per questo motivo non è possibile combinare le due condizioni. Nessun elemento è a conoscenza degli elementi precedenti nella lista e tenere traccia del numero di elementi filtrati è un'operazione complicata soprattutto da gestire in un numero di chiamate alla Where imprecisato.
Tutti i riferimenti di codice possono essere trovati all’indirizzo http://referencesource.microsoft.com/#System.Core/System/Linq/Enumerable.cs