C# 4.0 biedt nieuwe mogelijkheden die het de ontwikkelaar in een dynamische omgeving makkelijker maken. Dynamic programming is het sleutelwoord en stelt de C# ontwikkelaar in staat om eenvoudiger samen te werken met dynamische talen zoals IronPython en IronRuby. Naast dit dynamic programming kent deze versie van C# optionele en benoemde parameters, betere integratie met COM. Andere vernieuwingen in C# 4.0 richten zich op multicore en declarative programming. In dit artikel wordt met name de features van dynamic programming onder de loep genomen en zie je hoe ze in de praktijk kunnen worden toegepast.
Om helder te maken waar het heen gaat met C# is het belangrijk om even terug te kijken. Microsoft kondigde C# aan op 16 juni 2000. Het was de eerste programmeertaal die specifiek gemaakt was om samen te werken met de .NET Common Language Runtime. Zoals bekend bouwt C# voort op de bouwstenen van C++ maar leende ook features van Delphi, Java en andere talen. Het idee achter C# was dat het object-georienteerde en componentgebaseerde taal moest zijn en die makkelijk in het gebruik was. De eerste versie van C# is uitgebracht op 13 februari 2002 en werd in relatief korte tijd bijzonder populair.
In C# 2.0 werd een hele reeks features toegevoegd die men vanwege tijdgebrek niet meer in de eerste versie had kunnen stoppen. Generics is daar een voorbeeld van, maar ook anonymous methods, iterators en nullable types. Misschien niet direct duidelijk, maar nullable types was een voorwaarde voor de daaropvolgende versie, 3.0, die meer focus had op data.
De meeste ontwikkelaars werken met data, en dat was het primaire thema van C# 3.0. De belangrijkste vernieuwing in deze versie was de introductie van Language Integrated Query – LINQ. De meeste andere features in deze versie waren bedoeld om LINQ mogelijk te maken. Niettemin hebben deze features, zoals anonymous types, lamba expressies, extension methods en object- en collectie-initializers hun eigen waarde, ook zonder LINQ.
C# versus Visual Basic
Tijdens de ontwikkeling van de talen Visual Basic en C# sprongen deze regelmatig haasje-over als het ging om ondersteuning van features. Wat in de ene taal wel mogelijk was, zoals XML Literals in Visual Basic 9.0, was in de ander taal niet mogelijk, en andersom zoals anonymous methods in C# 2.0. Microsoft heeft deze strategie verlaten en wil beide talen op het zelfde niveau verder ontwikkelen. De C# en VB-teams zijn daartoe samengevoegd. Het doel is dus dat de keuze voor de ene of de andere taal enkel gebaseerd kan zijn op persoonlijke voorkeuren, niet om technische obstakels.
Het nut van dynamic programming
C# 4.0 richt zich onder meer op dynamic programming. Dat heeft vooral waarde bij het intregraal gebruik van meerdere ontwikkeltalen, het gebruik van HTML DOM in webomgevingen en integratie met COM-gebaseerde toepassingen zoals de Microsoft Office applicaties.
De meeste ontwikkelaars gebruiken verschillende tools en frameworks om een toepassing te maken. Als je een webapplicatie maakt gebruik je C#, HTML en vaak ook JavaScript. Voor Silverlight-toepassingen heb je C# en XAML. Om een bepaald probleem op te lossen kan je wat code of codevoorbeelden tegenkomen, die misschien is geschreven in een andere taal zoals Visual Basic of F#. Een van de voordelen van .NET is natuurlijk de interoperabiliteit tussen ontwikkeltalen, mogelijk gemaakt door de Common Language Runtime. In de afgelopen jaren heeft Microsoft dynamische talen uitgebracht zoals IronPython en IronRuby maar het is niet zo eenvoudig om vanuit C# code met deze talen samen te werken. Dat is een reden om ondersteuning voor dynamic programming toe te voegen aan C# 4.0.
Je kunt gebruik maken van reflection om objecten en methoden van objecten te benaderen waarvan je op voorhand, tijdens compilatie, nog niet weet van welk type het object is. Maar hiervoor moet je een referentie hebben naar het type object, een referentie naar het MemberInfo-object, vaststellen welke bindings je kunt gebruiken en vervolgens de methode aanroepen. Het is natuurlijk best cool als het je lukt, maar eenvoudig is het niet, en het komt ook niet bijzonder stabiel en betrouwbaar over. Dynamic methods in C# 4.0 kunnen dan helpen.
Als je Silverlight-toepassingen maakt heb je meestal te maken met het HTML DOM-object waarin je Silverlight control wordt gehost. Ook dat is makkelijker met dynamic programming.
Het aanroepen van objecten en methoden in een COM-omgeving is altijd lastig geweest in C#, met name vanwege de extra code die je nodig hebt voor conversies van typen en optionele parameters. Sommige C# ontwikkelaars keken wellicht met een schuin oog naar Visual Basic omdat COM-interop in die taal beter ondersteund werd. Dynamic programming in C# haalt die achterstand meer dan in. Het ondersteunen van optionele en benoemde parameters is daar een belangrijk onderdeel van.
Optionele parameters
Een optionele parameter geeft de ontwikkelaar de mogelijkheid om een standaardwaarde in een methode-declaratie op te nemen en het is aan de aanroepende code of een andere waarde voor de parameter expliciet wordt meegegeven of niet. Tot versie 4.0 moest dit opgelost worden door optionele parameters te simuleren.
// versie zonder culture parameter
public static string DisplayAsPercent(double value)
{
CultureInfo culture = CultureInfo.GetCultureInfo("nl-NL");
return (value * 100).ToString("N", culture) + "%";
}
// versie met cultureName parameter
public static string DisplayAsPercent(double value, string cultureName)
{
CultureInfo culture = new CultureInfo(cultureName);
return (value * 100).ToString("N", culture) + "%";
}
De methode DisplayAsPercent is overloaded met en zonder parameter voor CultureInfo. De ontwikkelaar die in code deze methode aanroept ziet dezelfde methode met en zonder deze parameter, waardoor de volgende code geen fout oplevert.
// standaard geformatteerd met nederlandse notatie
Console.WriteLine(DisplayAsPercent(0.04233));
// geformateerd met amerikaans-engelse notatie
Console.WriteLine(DisplayAsPercent(0.04233, "en-US"));
Als je meer methoden had met meer parameters en een meer flexibeliliteit bij de ontwikkeling wilde hebben moet je meer overloaded methoden toevoegen. Dat is natuurlijk wat omslachtig en meer code betekent ook meer onderhoud. Optionele parameters lossen dit probleem op. De bovenstaande methode kan dan namelijk vervangen worden door de volgende code.
public static string DisplayAsPercent(double value, string cultureName = "nl-NL")
{
CultureInfo culture = CultureInfo.GetCultureInfo(cultureName);
return (value * 100).ToString("N", culture) + "%";
}
Het toevoegen van een optionele parameter wordt eenvoudig bereikt door een standaard waarde toe te kennen aan deze parameter. In dit voorbeeld wordt “nl-NL” standaard gebruikt als waarde voor cultureName tenzij het expliciet wordt opgeven bij het aanroepen van de methode. Het bijkomend voordeel hiervan is dat je bij het ontwikkelen via IntelliSense ziet welke waarde standaard wordt gehanteerd.
Let er wel op dat optionele parameters alleen kunnen worden opgegeven als de standaardwaarde tijdens compiletijd kan worden bepaald. Het is dus niet mogelijk om de methode als volgt te schrijven.
// Dit kan niet!
public static string DisplayAsPercent(double value, CultureInfo culture =
CultureInfo.GetCultureInfo("nl-NL"))
{
return (value * 100).ToString("N", culture) + "%";
}
Optionele parameters moeten bovendien aan het einde van de parameter-lijst staan.
Benoemde parameters
De belangrijkste reden voor benoemde parameters (named parameters) is het wegnemen van ambiguiteit tussen parameters. In het volgende voorbeeld zie je een reeks parameters die allemaal van het type double zijn.
public void AddQuote(DateTime tradeDate, double open, double high, double low, double close, long volume = 0)
{
// niet geïmplementeerd
}
In de methode AddQuote is het mogelijk om Quote waarden toe te voegen, bijvoorbeeld voor een lijst met koersen voor een aandeel of beurs-index. Een beurs-index kent, in tegenstelling tot een aandeel, geen volume. De methode is als volgt aan te spreken.
Issue aandeel = new Issue();
aandeel.AddQuote(tradeDate: DateTime.Now,
volume: 24323,open: 34.02,low: 33.98,high: 35.23,close: 34.99);
De aanroep van de methode AddQuote is uitgevoerd met meeneming van de naam van de parameter. Zoals je ziet is de volgorde van de parameters een beetje veranderd om te tonen dat de volgorde niet meer uitmaakt wanneer je benoemde parameters opgeeft.
Dynamic keyword
Alhoewel benoemde en optionele parameters handig en belangrijk zijn voor de dynamische features in C# is er één onderdeel essentieel: het dynamic keyword. Voordat je ziet hoe dit keyword precies kan worden toegepast is het voor het voorbeeld van belang een paar klassen neer te zetten.
public class Stock
{
public List<Quote> Quotes { get; set; }
public string Name { get; set; }
}
public class Bond
{
public List<Quote> Quotes { get; set; }
public string Name { get; set; }
}
public class Option
{
public List<Quote> Quotes { get; set; }
public DateTime ExpirationDate { get; set; }
}
Een aandeel of een obligatie hebben in dit voorbeeld beiden een naam. Een optie niet, deze kent wel een expiratie datum, die weer niet van toepassing is op een aandeel of obligatie. Ieder type belegging heeft wel weer een lijst van koersinformatie.
Zoals je ziet implementeren de verschillende typen geen specifieke interface of erven ze van een bepaalde basisklasse. Er is dan ook niets dat deze klassen met elkaar bindt. Er is, andergezegd, geen relatie tussen de objecten van deze typen.
De onderstaande code geeft een instantie van een Stock terug.
public static object GetIssue()
{
return new Stock();
}
Deze methode instantieert weliswaar een Stock-object, maar geeft een generiek object terug. In sommige situaties is een dergelijke opzet noodzakelijk als je niet vooraf weet welk type je terugkrijgt, bijvoorbeeld bij gebruik van een extern component of een taal als IronPython. De code hieronder geeft hiervan een beter voorbeeld.
public static object GetIssue(string issueType)
{
switch (issueType.ToLower())
{
case "stock":
return new Stock();
case "bond":
return new Bond();
default:
return null;
}
}
Het enige dat je weet, door het lezen van de documentatie, is dat het object een Name-eigenschap heeft. Maar omdat het resultaat van de methode een object is weet de C# compiler dat niet. Voor C# 4.0 had je reflectie als mogelijkheid om de eigenschappen van een ogenschijnlijk simpel object te benaderen. In C# 4.0 kan je echter het volgende doen:
dynamic issue = GetIssue("stock");
issue.Name = "Aegon";
In bovenstaande code zie je dat methode GetIssue een object terug geeft. Dit object wordt toegewezen aan de variabele issue. Deze variabele is van het het ‘type’ dynamic. Dit type is gelijk aan het type System.Object, maar je kunt er meer mee doen. Tijdens compilatie zal C# iedere methode of eigenschap op het object toestaan. Pas tijdens runtime wordt bekeken of het object de betreffende methode of eigenschap wel heeft. Als je wel eens met reflectie hebt gewerkt om hetzelfde te bereiken zal de eenvoudigheid wel aanspreken.
Als de methode of eigenschap niet bestaat wordt er dus geen compilatie-fout gegeven. Wanneer echter tijdens runtime een eigenschap of methode wordt aangesproken die niet bestaat op het object, dan treedt er wel een exceptie op. Dat laat het onderstaande voorbeeld zien.
public static IEnumerable<object> GetIssues()
{
yield return new Stock() { Name="Aegon"} ;
yield return new Bond() { Name = "ING 1,27%11" };
yield return new Option() { ExpirationDate = new DateTime(2010,10,1) };
}
Deze methode geeft een drietal objecten terug. Als je nog weet dat een Option-object geen Name-eigenschap heeft, zal het duidelijk zijn dat de volgende aanroep een fout opleveren.
foreach (dynamic issue in GetIssues())
{
Console.WriteLine(issue.Name);
}
Terwijl de code de lijst doorloopt gaat er niets mis bij de eerste twee objecten, maar zodra een issue van het type Option terugkomt zal de volgende exceptie optreden.
'Option' does not contain a definition for 'Name'
Dynamic met COM Interop
Zoals gezegd is COM Interop een van de redenen voor dynamic programming features in C# 4.0. In Visual Basic was dat al wat makkelijker, maar C#-ontwikkelaars halen die achterstand met versie 4.0 snel in. In het volgende voorbeeld zie je hoe dat in zijn werk gaat. Om de vergelijking makkelijker te maken, zie je eerst hoe je een Excel workbook kunt maken op de ‘oude’ manier:
static void ExcelVroeger()
{
// Instantieer ee nieuwe Excel applicatie
var excelApp = new Excel.Application();
// Maak een workbook
var workbook = excelApp.Workbooks.Add(Type.Missing);
var worksheet = (Excel.Worksheet)workbook.Sheets.Add(Type.Missing,
Type.Missing,
Type.Missing, Type.Missing);
// Voeg headers toe
worksheet.Cells[1, 1] = "Naam";
worksheet.Cells[1, 2] = "E-mail";
// Formatteer de header
worksheet.get_Range("A1", "B1").Font.Bold = true;
worksheet.get_Range("A1", "B1").VerticalAlignment =
Excel.XlVAlign.xlVAlignCenter;
// Maak een array om meerdere waarden direct te plaatsen
string[,] adressen = new string[3, 2];
adressen[0, 0] = "Piet";
adressen[0, 1] = "[email protected]";
adressen[1, 0] = "Karel";
adressen[1, 1] = "[email protected]";
adressen[2, 0] = "Kees";
adressen[2, 1] = "[email protected]";
// Vul een cellrange
worksheet.get_Range("A2", "B4").Value2 = adressen;
// Bewaar het Excel-bestand
workbook.SaveAs("adreslijst.xls", Type.Missing, Type.Missing,
Type.Missing, Type.Missing, Type.Missing, Excel.XlSaveAsAccessMode.xlShared,
Type.Missing, Type.Missing, Type.Missing, Type.Missing, Type.Missing);
excelApp.Quit();
}
Een paar zaken zullen hier snel opvallen. Methoden als Workbook.SaveAs() en Sheets.Add() kennen een reeks parameters. In C# moet je die parameters, voor zover niet relevant, vullen met de waarde Type.Missing. Dat is een statisch, readonly, veld dat een ontbrekende waarde vertegenwoordigt. De teruggegeven objecten in COM zijn altijd van het type System.Object en daarom moet specifiek meegegeven worden dat de methode Sheets.Add() een object van het type Worksheet retourneert.
Dat het eenvoudiger kan, laat de volgende code zien.
static void ExcelNieuw()
{
// Instantieer ee nieuwe Excel applicatie
var excelApp = new Excel.Application();
// Maak een workbook
dynamic workbook = excelApp.Workbooks.Add();
Excel.Worksheet worksheet = workbook.Sheets.Add();
// Voeg headers toe
worksheet.Cells[1, 1] = "Naam";
worksheet.Cells[1, 2] = "E-mail";
// Formatteer de header
worksheet.get_Range("A1", "B1").Font.Bold = true;
worksheet.get_Range("A1", "B1").VerticalAlignment =
Excel.XlVAlign.xlVAlignCenter;
// Maak een array om meerdere waarden direct te plaatsen
string[,] adressen = new string[3, 2];
adressen[0, 0] = "Piet";
adressen[0, 1] = "[email protected]";
adressen[1, 0] = "Karel";
adressen[1, 1] = "[email protected]";
adressen[2, 0] = "Kees";
adressen[2, 1] = "[email protected]";
// Vul een cellrange
worksheet.get_Range("A2", "B4").Value2 = adressen;
// Bewaar het Excel-bestand
workbook.SaveAs("adreslijst.xls", AccessMode:
Excel.XlSaveAsAccessMode.xlShared);
excelApp.Quit();
}
Door het type van de variable workbook op dynamic te zetten kan je de collectie Sheets benaderen en aan het resultaat direct het type Excel.Worksheet meegeven. Een ander verschil is direct duidelijk bij het opslaan van de Excel-workbook. Het is niet meer nodig om een reeks Type.Missing waarden mee te geven. Het is zelfs niet eens nodig om de AccessMode via een benoemde parameter op te geven. De runtime zal zelf kunnen bepalen dat de waarde Excel.XlSaveAsAccessMode.xlShared alleen bedoeld kan worden voor de parameter AccessMode. Het opgeven van de parameternaam kan echter wel, en maakt de code soms beter leesbaar.
Conclusies
Voor ontwikkelaars die te maken hebben met COM-objecten zijn de dynamische features van C# 4.0 een flinke steun in de rug. Met kracht komen echter ook risico’s. Het is eenvoudig om overal maar het dynamic keyword te gebruiken. Het ligt voor de hand dat onbezonnen gebruik van dynamic voor problemen kan zorgen. Problemen die zich pas tijdens runtime laten zien. Een taal als C# die eerst en vooral statisch was, schuift langzaam op naar dynamisch. Voor sommigen is dat een probleem, maar C# is nimmer bedoeld om statisch te zijn, maar juist produktief. Met de juiste richtlijnen levert dynamisch programmeren in C# produktiviteit op waar het moet, en blijft het achterwege als het niet nodig is.
Meer informatie