La promessa di LINQ è quella di poter scrivere interrogazioni SQL-like per qualunque oggetto che implementi l'interfaccia IEnumerable<T>, dove T è il tipo degli elementi della "sequenza" da interrogare. Le query possono essere, tra l'altro, eseguite in parte da un DBMS (DLINQ) e in parte in memoria.
Gran parte delle feature di LINQ si basano su alcune estensioni a C# (idem per VB.Net) supportate dalla prossima versione del compilatore (3.0). Di queste feature, due sono già presenti nell'attuale compilatore C# 2: generics e iterators. Generics e iterators sono un'accoppiata formidabile per generare astrazioni.
Grazie a tutto questo è possibile scrivere una classe Sequence<T> che mimi alcune delle funzionalità implementate dagli extension method della omonima classe statica di LINQ:
public class Sequence<T> : IEnumerable<T>
{
private IEnumerable<T> sequence = null;
public Sequence(IEnumerable<T> sequence)
{
this.sequence = sequence;
}
public List<T> ToList()
{
List<T> list = new List<T>();
foreach (T item in this)
list.Add(item);
return list;
}
public T[] ToArray()
{
return ToList().ToArray();
}
// Cast implici da T[] e List<T> a Sequence<T>.
public static implicit operator Sequence<T>(T[] sequence)
{
return new Sequence<T>(sequence);
}
public static implicit operator Sequence<T>(List<T> sequence)
{
return new Sequence<T>(sequence);
}
private IEnumerable<T> ConcatIterator(Sequence<T> sequence)
{
foreach (T item in this.sequence)
yield return item;
foreach (T item in sequence)
yield return item;
}
public Sequence<T> Concat(Sequence<T> sequence)
{
return new Sequence<T>(ConcatIterator(sequence));
}
private IEnumerable<T> WhereIterator(Predicate<T> condition)
{
foreach (T item in sequence)
{
if (condition(item))
yield return item;
}
}
public Sequence<T> Where(Predicate<T> condition)
{
return new Sequence<T>(WhereIterator(condition));
}
public IEnumerator<T> GetEnumerator()
{
foreach (T item in sequence)
yield return item;
}
System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
{
foreach (object item in sequence)
yield return item;
}
}
Vediamo alcuni esempi di codice che sfruttano la precedente classe:
Sequence<int> sq = new int[] { 1, 2, 3 }; // Cast implicito.
sq = sq.Concat(new int[] { 4, 5, 6 });
foreach (int item in sq.Where(delegate (int i) { return i > 2; }))
Console.WriteLine(item);
Stampa:
3 4 5 6
Il seguente esempio trasforma invece una SqlDataReader, risultato di una query sul db Northwind, andando poi a filtrare i record in memoria:
static IEnumerable<string> GetEmployees()
{
using (SqlConnection cn = new SqlConnection("SERVER=(local);DATABASE=Northwind;USER ID=sa;PASSWORD=;"))
{
cn.Open();
SqlCommand cmd = new SqlCommand("select FirstName from Employees", cn);
using (SqlDataReader rd = cmd.ExecuteReader())
{
while (rd.Read())
yield return rd["FirstName"].ToString();
}
}
}
Sequence<string> s = new Sequence<string>(GetEmployees());
foreach (string name in s.Where(delegate(string item) { return item.StartsWith("A"); }))
Console.WriteLine(name);
Stamperebbe a console i soli impiegati il cui nome inizia per A.