Efter at have set på de mange mindre udvidelser til C# 3.0, er vi endelig kommet til den store nyhed: Language-INtegrated Query eller bare LINQ. Skåret til benet er LINQ blot en SQL-lignende syntaks til forespørgsler på typer, der understøtter IEnumerable<T>. Det lyder måske ikke af meget, men når man tænker på hvor mange typer, der understøtter IEnumerable<T> samt tager i betragtning, at egne typer naturligvis også kan implementere dette interface, begynder det at blive interessant. I praksis betyder det, at vi f.eks. kan anvende LINQ mod collections, XML og naturligvis relationelle data.
En stor del af LINQs SQL-lignende funktionalitet kommer af et sæt udvidelsesmetoder til IEnumerable<T>. C# 3.0 har dog reserverede nøgleord for disse, så syntaksen bliver mere koncis, men under kølerhjelmen er der altså blot tale om metodekald. Med brug af de specielle LINQ nøgleord, bliver syntaksen som følger:
from [type] id in expr
[ from [type] id in expr ]
[ let id = expr ... ]
[ where bool-expr ]
[ join [type] id in expr on expr equals expr ]
[ orderby order-expr [ ascending | descending ], order-expr, ... ]
[ select expr | group expr by key ]
[ into id query ] ...
men det er måske lettere blot at se på et eksempel. Jeg vil springe det åbenlyse databaseeksempel over, da det allerede er beskrevet så mange steder og i stedet se på LINQ til XML. Her viser LINQs elegance sig nemlig med stor overbevisning.
Lad os sige, at vi har en liste af kunder ud fra hvilken, vi ønsker at lave et XML-dokument med alle vores kontakter i Danmark. Med en traditionel tilgang til XML kunne det se ud som vist nedenfor:
XmlDocument document = new XmlDocument();
XmlElement contacts = document.CreateElement("contacts");
// dump all DK contacts to document
foreach (Contact customer in customers) {
if (customer.Country == "DK") {
XmlElement contact = document.CreateElement("contact");
XmlElement name = document.CreateElement("name");
name.InnerText = customer.Name;
contact.AppendChild(name);
XmlElement phone = document.CreateElement("phone");
phone.InnerText = customer.Phone;
contact.AppendChild(phone);
contacts.AppendChild(contact);
}
}
document.AppendChild(contacts);
Koden er ganske ligetil. Vi opretter et dokument, indsætter en sektion for vores kontakter, og løber derefter vores interne datastruktur igennem for at finde alle danske kontakter. For hver af disse opretter vi et contact-element og indsætter passende elementer for navn og telefonnummer. Det er der næppe noget odiøst i. Metoden er drevet af dokumentets struktur, og der er ikke nogen direkte forbindelse mellem udvælgelsen af poster og selve dokumentet. Den forbindelse skaber vi i koden. Koden er også relativ omfattende.
Med LINQ kunne den tilsvarende løsning se således ud:
XElement contacts =
new XElement("contacts",
from customer in customers
where customer.Country == "DK"
select new XElement("contact",
new XElement("name", customer.Name),
new XElement("phone", customer.Phone)
)
);
Ikke alene er ovenstående langt kortere, men læg også mærke til hvordan strukturen af koden stemmer fuldstændig overens med vores datastruktur. Bemærk ligeledes hvordan udvælgelsen og skabelsen af contact-elementerne er en samlet operation.
Den resulterende XML-stump er naturligvis identisk i begge tilfælde, og ifølge Microsoft bruger LINQ 30-50% mindre hukommelse end XML DOM. Jeg kunne ærlig talt ikke se det smarte ved LINQ, de første par gange jeg så det omtalt, men for mit vedkommende er LINQ fremover måden at arbejde med XML på.
I og med at C# 3.0 compiler kode til at køre på CLR 2.0, kan vi bruge Reflector til at se på, hvad ovenstående kode egentlig resulterer i og ikke overraskende, kan vi konstatere, at det hele er implementeret via kald til de føromtalte udvidelsesmetoder samt diverse anonyme delegates.
XElement contacts = new XElement("contacts",
customers.Where<Contact>(delegate (Contact customer) {
return (customer.Country == "DK");
}).Select<Contact, XElement>(delegate (Contact customer) {
return new XElement("contact", new object[] {
new XElement("name", customer.Name),
new XElement("phone", customer.Phone) });
}));
Console.WriteLine(contacts);
(Bemærk, at Reflector kan sættes til at analysere IL til forskellige versioner af .NET. Hvis ovenstående vises i Reflector med de korrekte C# nøgleord, er det fordi den står til at Disassemble til .NET 3.5 (ja, det kalder han det altså). Indstillingen kan ændres under View > Options > Disassembler > Optimization.)