DevTips.NET Logo

Je bent niet ingelogd. Login

Microsoft .NET Magazine voor developers

 
Gratis abonnement!

 

 
Gratis abonnement!
Gratis proberen!
Klik hier

MSDN Magazine


.NET Magazine

 
.NET Developer

Extension methods in C# 3.0

jouw beoordeling:
Heb je wel eens de behoefte gehad om extra functionaliteit te hebben in een klasse die je niet zelf gemaakt hebt? Dat kan er bijvoorbeeld een zijn uit het .NET Framework of een aangekochte control. Als je voor die vraag staat, kan je natuurlijk een nieuwe klasse laten erven van de oorspronkelijke. Werkt prima, alleen moet je ook die ervende klasse weer onderhouden, en het werkt niet als de klasse 'gesealed' is. In het .NET Framework komt dat laatste nogal eens voor, zoals bij ListItem, WebReference , StringBuilder en zelfs string. In C# 3.0 is het concept van Extension Methods toegevoegd om deze problemen te omzeilen. In dit artikel bespreken we extension methods en maken hierbij gebruik van Visual Studio Codename 'Orcas' Beta 1. 
 
 
Wat zijn Exension Methods? 
Extension methods maken het voor ontwikkelaars mogelijk om nieuwe methoden toe te voegen aan een bestaand type (klasse of struct) zonder dat je hiervoor een subklasse moet maken. Dit geeft extra flexibiliteit zonder dat je inboet op zaken als strong typing en validatie tijdens compiletijd. De introductie van extension methods is nauw verbonden met LINQ. Laten we eens zien hoe extension methods gebruikt kunnen worden.
 
 
Een eenvoudig voorbeeld
Als we willen zien of een ingevoerde tekst een valide url (http://... ) is, kan je dit doen door een helper-klasse te maken met een statische methode die de controle uitvoert.
 

      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();

 
De volledige implementatie van de compressie extension methods is hier te downloaden.
 
 
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);

 
 
Een paar regeltjes
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:
 

public class Persoon

{

  private string m_naam;

 

  public string Naam

  {

    get { return m_naam; }

    set { m_naam = value; }

  }

 

  public void PrintNaam()

  {

    Console.WriteLine("Naam: " + Naam);

  }

}

 

Wat zou er gebeuren als we nu een extension method toevoegen met de zelfde methodenaam PrintNaam()?
 

    public static class PersoonUtil

    {

        public static void PrintNaam(this Persoon p)

        {

            Console.WriteLine("Naam 2: " + p.Naam);

        }

    }

 
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
 
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:
 
Error 1 The call is ambiguous between the following methods or properties: 'EM2.ExtensionMethods2.Print(string)' and 'EM1.ExtensionMethods1.Print(string)' C:\\Projects\ExtensionMethodsDemo\Program.cs 16 13 ExtensionMethodsDemo
 
Het is dus van belang om de extension methods logisch bij elkaar te houden en liefst één namespace te hanteren voor zelf gemaakte uitbreidingen.
 
 
Conclusies
Hopelijk heeft dit artikel een beter beeld gegeven van extension methods en hun toepassing in eigen programmacode en als onderdeel van LINQ. Zoals we gezien hebben zijn extension methods erg handig voor die situaties waarin je de klasse-hiërarchie niet wilt of kunt aanpassen voor de extra functionaliteit die je wil toevoegen. Het is evenwel niet aan te raden om direct her en der extension methods te maken omdat het cool is. In ieder geval is het verstandig om extension methods logisch te groeperen in een namespace zodat je weet waar ze te vinden zijn en er geen conflicten ontstaan door dubbele naamgeving en functionaliteit.
 
Meer informatie:
 
Overige links

Gepubliceerd op maandag 21 mei 2007 door Sander Gerz


Commentaar



Reageer

Naam:

Jouw url (optioneel):

Commentaar:

HIP Voer de code in: