string url = TextBoxWeblog.Text;
if (UrlValidator.IsValid(url))
{
}
Maar dit kan in C# 3.0 ook via een extension method uitgevoerd worden. Dat ziet er als volgt uit:
string url = TextBoxWeblog.Text;
if (url.IsValidUrl())
{
}
Hoe is dat nu mogelijk? Sinds wanneer heeft een string-variabele een methode IsValidUrl()? Dat komt doordat we een statische klasse hebben gemaakt met een statische methode. Deze statische methode ziet er als volgt uit:
public static class MyExtensionMethods
{
public static bool IsValidUrl(this string text)
{
Regex rx = new Regex(@"http(s)?://([\w-]+\.)+[\w-]+(/[\w- ./?%&=]*)?");
return rx.IsMatch(text);
}
}
De parameter-lijst van de statische methode begint met het this-keyword. Dit komt nog voor de eerste parameter van het type string. Met het this-keyword wordt aangegeven dat de extension method van toepassing is op het type dat er achter staat. In de IsValidUrl-methode heb je toegang tot het object, in dit geval van het type string, waarop de methode wordt aangeroepen.
De bereikbaarheid van exension methods is afhankelijk van de namespaces waarin de statische klasse is ondergebracht. Als MyExtensionMethods in de namespace DevTips.ExtensionMethods is geplaatst, dan moet deze namespace met behulp van het using-keyword meegenomen worden in de klasse waar je de extension method wil gebruiken.
using DevTips.ExtensionMethods;
De compiler zal dan de IsValidUrl() methode op iedere string kunnen terugvinden. In de Visual Studio "Orcas" Beta 1 versie wordt dit ook met IntelliSense ondersteund.
Nu is het bovenstaande een eenvoudig voorbeeld van extension methods. Maar er zijn natuurlijk legio voorbeelden te bedenken waar deze feature van nut kan zijn. Extension methods kunnen toegepast worden op individuele types, maar ook op base classes en interfaces in het .NET Framework. Het wordt op deze manier een stuk eenvoudiger om een eigen uitbreidbaar framework samen te stellen.
Gecomprimeerde objecten
Stel je voor dat je een generieke methode wil hebben om objecten te serialiseren en te comprimeren. Door een extension method op het object-type, krijg je dit eenvoudig voor elkaar.
Om te beginnen gaan we uit van een normale helper-klasse.
using System;
namespace ExtensionMethodsDemo
{
public class CompressionUtil
{
public static byte[] Compress(object o)
{
//...
}
public static object Decompress(byte[] compressedData)
{
//...
}
}
}
In het volgende voorbeeld zijn de wijzigingen, waarmee we de methoden ombouwen tot extension methods onderstreept.
public static class CompressionUtil
{
public static byte[] Compress(this object o)
{
//...
}
public static object Decompress(this byte[] compressedData)
{
//...
}
}
Nu is het mogelijk om ieder object (dat serialiseerbaar is, dat is wel een voorwaarde in dit geval) te comprimeren en weer uit te pakken. De extension methods laten aanroepen zoals in onderstaand voorbeeld.
DataTable table = new DataTable();
table.Columns.Add("Naam", typeof(string));
table.Columns.Add("Leeftijd", typeof(int));
table.Rows.Add(new object[] { "Pierre van Worteltaart", 54});
table.Rows.Add(new object[] { "Klaas Klompendans", 34 });
byte[] b = table.Compress();
DataTable table2 = (DataTable)b.Decompress();
Wat heeft dit met LINQ te maken?
De mogelijkheid van extension methods is essentieel voor LINQ (Language Integrated Query). Ze zijn immers niet alleen toe te passen op individuele typen, maar ook op base klassen en interfaces uit het .NET Framework. Daarom is het mogelijk een framework van exensies maken die door het gehele CLR is te gebruiken. Dat is precies wat Microsoft heeft gedaan in de System.Linq namespace.
De methoden die in deze namespace zijn geïmplementeerd zijn bedoeld om gegevens op te vragen, met name collecties. Je kunt ze toepassen op XML, relationele databases en .NET objecten die de IEnumerable-interface implementeren.
Het onderstaande voorbeeld toont de klasse Land waarin we naam en het inwoneraantal kunnen terugvinden. Er wordt gebruik gemaakt van automatische properties, eveneens nieuw in C# 3.0 waardoor we niet de volledige implementatie hoeven te schrijven. De compiler maakt aan de hand van {get; set;} zelf een volledige implementatie van deze properties.
public class Land
{
public string Naam { get; set;}
public int AantalInwoners { get; set;}
}
Nu kunnen we met de eveneens nieuwe object en collectie initialisatie mogelijkheid een lijst van landen vullen.
List<Land> landen = new List<Land>
{
new Land { Naam="Spanje", AantalInwoners=40341462 },
new Land { Naam="Italië", AantalInwoners=58103033 },
new Land { Naam="Frankrijk", AantalInwoners=60656178 },
new Land { Naam="Duitsland", AantalInwoners=82431390 },
};
(bron: http://nl.wikipedia.org/wiki/Lijst_van_onafhankelijke_staten_naar_inwonertal)
We kunnen zelf een Where() extension method maken waarmee we selecties kunnen maken in deze collectie. Deze Where() methode ziet er zo uit:
public static IEnumerable<T> Where<T>(this IEnumerable<T> sequence, Func<T, bool> predicate)
{
foreach (T item in sequence)
{
if (predicate(item)) yield return item;
}
}
De methode geeft een object terug dat de IEnumerable interface implementeert - een collectie - van type T. De eerste parameter zal intussen bekend voorkomen, en duidt erop dat ieder IEnumberable object deze extension method kan gebruiken. De predicate parameter refereert naar een delegate die één argument aanneemt die waar of onwaar kan zijn. Deze delegate ziet er zo uit:
public delegate TResult Func<TArg, TResult>(TArg arg);
Nu kunnen we een selectie maken van landen die bijvoorbeeld meer dan 50 miljoen inwoners hebben. Dat gaat zo:
IEnumerable<Land> groteLanden = landen.Where(l => l.AantalInwoners > 50000000);
foreach (Land l in groteLanden)
{
Console.WriteLine("{0}: {1}", l.Naam, l.AantalInwoners);
}
De syntax l => is een voorbeeld van een zogenaamde Lambda expressie. Zo'n expressie is een compactere manier om een anonieme methode, die we al in C# 2.0 hebben, aan te duiden. De bovenstaande regels leveren een lijst op met drie landen (Italië, Duitsland en Frankrijk).
Erg orgineel is de Where methode niet, want deze methode vinden we ook terug in de System.Linq namespace. Het is dus niet nodig om zelf deze extension method te schrijven.
Met een kleine aanpassing kunnen we echter wel een FindFirst methode creëren die niet in LINQ is opgenomen.
public static T FindFirst<T>(this IEnumerable<T> sequence, Func<T, bool> predicate)
{
foreach (T item in sequence)
{
if (predicate(item)) return item;
}
return default(T);
}
Deze methode kunnen we dan zo gebruiken.
Land land = landen.FindFirst(l => l.Naam.StartsWith("S"));
Console.WriteLine(land.Naam);
Zoals je gezien hebt is het maken van een extension method eenvoudig. Helper-achtige klassen die logisch niet bij één of enkele klassen behoren (en daar dus geen onderdeel van zijn) lenen zich prima voor een ombouw. Het kan overigens ook wel eens verwarrend worden. Laten we eens kijken naar een eenvoudige klasse:
Wat zou er gebeuren als we nu een extension method toevoegen met de zelfde methodenaam PrintNaam()?
Voor de compiler maakt dit niet zo veel uit. Dat wil zeggen, de extension method wordt niet gebruikt. Als we in IntelliSense kijken, wordt de methode wel gevonden, maar er is geen mogelijkheid om deze implementatie expliciet te kiezen.
Extension methods worden zichtbaar wanneer je de namespace opneemt die de extension methods bevat. Wat nu als er in twee verschillende namespaces eenzelfde extension method (qua naam en parameters) aanwezig is. Welke methode wordt dan gebruikt? Geen van beiden. De compiler weet niet wat de bedoeling is en komt met de volgende foutmelding: