1
Inleiding tot .NET
Software werd en wordt meestal geschreven in C of C++. De broncode van een C/C++ programma wordt dan gecompileerd naar machine code, die eventueel nog gelinkt wordt met machine code uit een aantal libraries, om zo ten slotte een executable te genereren die kan worden uitgevoerd op de machine waarop het programma gecompileerd werd. Deze manier van software produceren heeft echter een aantal nadelen: • Om de libraries te kunnen gebruiken, moet de programmeur (een deel van) zijn programma in C/C++ schrijven. • De gecompileerde code werkt enkel maar op het soort van systeem waarop ze gecompileerd werd. Om te draaien op andere besturingssystemen of hardware moet het programma opnieuw gecompileerd worden. • Als er iets misgaat, zijn foutenboodschappen vaak zeer cryptisch (“segfault”). Sinds midden jaren ’90 hebben de ontwikkelingen rond Java aangetoond dat ook andere manieren van softwareontwikkeling hun plaats hebben in de industrie. Deze ontwikkelingen zijn ook Microsoft niet ontgaan, die met hun .NET raamwerk mee op deze kar trachten te springen. Volgende eigenschappen van het raamwerk zijn cruciaal: • Platformonafhankelijkheid: .NET programma’s worden niet meer gecompileerd naar machine-code, maar naar een tussenliggende taal, de Common Intermediate Language (CIL). Deze gecompileerde IL-code kan dan probleemloos worden uitgevoerd op elke computer die beschikt over een Common Language Runtime (CLR), net zoals een Java .class bestand kan worden uitgevoerd door elk Java Runtime Environment. • Taalonafhankelijkheid: Verschillende programmeertalen kunnen gecompileerd worden naar de CIL. Zo is C#een variant van C die gecompileerd wordt naar CIL en is VB.NET een variant van Visual Basic die hiernaar gecompileerd wordt. Daarnaast bestaat er bijvoorbeeld ook de Javavariant J#. • Hoog-niveau runtime environment: het CLR voorziet een aantal handige diensten aan de programmeur. Zo zal het zelf zorgdragen voor het opruimen van geheugen dat niet meer nodig is (garbage collection) en daarmee geheugenlekken vermijden. Daarnaast zorgt het bijvoorbeeld ook voor een betere afhandeling van fouten. Het CLR kan hierbij natuurlijk profiteren van het feit dat een CLI programma meer nuttige informatie bevat dan machinecode. In deze cursus beschrijven we .NET aan de hand van de taal C#. We zullen hierbij vooral focussen op die aspecten waarin C# verschilt van Java. De vele gelijkenissen worden vaak niet expliciet vermeld.
1
2
C#
C#is een C-achtige taal, die door Microsoft speciaal voor zijn .NET raamwerk ontwikkeld werd. Samen met VB.NET is het de meest gebruikte .NET programmeertaal. Ondanks zijn naam, is C#meer verwant met Java dan met C of C++. Het volgende voorbeeld toont het gebruik van enkele methodes uit de bibliotheekklasse Console, waarmee invoer/uitvoer naar een terminal gedaan kan worden: u s i n g System ; c l a s s Klasse { p u b l i c s t a t i c v o i d Main ( ) { C o n s o l e . Write ( ”What i s your name ? : ” ) ; C o n s o l e . Write ( ” H e l l o , { 0 } ! ” , C o n s o l e . ReadLine ( ) ) ; } } De statische Write methode uit de Console klasse gebruikt hetzelfde idee als C’s printf functie: het eerste argument is een string met daarin een aantal parameters ({0}, {1}, . . . ) en de volgende argumenten vullen deze parameters in.
2.1
Data types
Een belangrijk onderscheid in C#is het verschil tussen value types en reference types. Een reference type stelt een referentie naar een bepaalde geheugenplaats op de heap voor. Als er met een variabele van een dergelijk type een toekenning x = y gebeurt, wordt gewoon deze referentie doorgegeven (zodat x en y nu naar dezelfde geheugenplaats wijzen). De waarde van een value type bevindt zich niet in zo’n geheugenplaats maar staat rechtstreeks op de stack. Als er een toekenning gebeurt, wordt de waarde gekopieerd (zodat x en y nu elk hun eigen kopietje van dezelfde waarde hebben. In Java bestaat hetzelfde onderscheid: alle primitieve types (int, float, ...) zijn value types, terwijl alle objecten (Object, String, Integer, ...) reference types zijn. Het is in Java dan ook niet mogelijk om zelf extra soorten value types bij te maken. In C#gaat dit wel. Hiervoor dient het keyword struct. Een struct is in C#een soort van “lichtgewicht” object, dat zich gedraagt als een value type ipv. als een reference type. Het volgende voorbeeld illustreert dit verschil: using System ; struct KommaGetalS {
2
public i nt VoorDeKomma ; public i nt NaDeKomma ; public override S t r i n g T o S t r i n g ( ) { return VoorDeKomma + ” , ” + NaDeKomma ; } } c l a s s KommaGetalC { public i nt VoorDeKomma ; public i nt NaDeKomma ; public override S t r i n g T o S t r i n g ( ) { return VoorDeKomma + ” , ” + NaDeKomma ; } } c l a s s Test { s t a t i c void Main ( ) { KommaGetalS x , y ; x . VoorDeKomma = 2 ; x . NaDeKomma = 3 ; y = x; y . VoorDeKomma = 1 ; C o n s o l e . WriteLine ( ”x = // U i t v o e r : x = 2 ,3 en KommaGetalC a , b ; a = new KommaGetalC ( ) ; a . VoorDeKomma = 2 ; a . NaDeKomma = 3 ; b = a; b . VoorDeKomma = 1 ; C o n s o l e . WriteLine ( ” a = // U i t v o e r : a = 1 ,3 en
{0} en y = {1} ” , x , y ) ; y = 1 ,3
{0} en b = {1} ” , a , b ) ; b = 1 ,3
} } Value types kunnen impliciet worden omgezet naar reference types: int i = 5 ; object o = i ; Nu is het object o een referentie naar een geheugenplaatsje met daarin de waarde 5. (Het keyword object is een synoniem voor de klasse System.Object, 3
de basisklasse waarvan alle objecten overerven.) Deze impliciete omzetting wordt boxing genoemd. Ook de omgekeerde weg (unboxing) bestaat, maar hiervoor is een expliciete typecast nodig: int j = ( int ) o ; Strings zijn objecten van de klasse System.String, waarvoor het keyword string een synoniem biedt.
2.2
Soorten parameters
Het onderscheid tussen value en references types wordt een beetje ingewikkelder gemaakt door het feit dat C#ook verschillende manieren biedt om parameters door te geven aan functies. De default is dat parameters met call-by-value worden doorgegeven, dwz. dat de functie een eigen kopietje krijgt van de waarde die wordt meegegeven. Bijvoorbeeld: c l a s s Test { public i nt g e t a l ; public s t a t i c void f o o ( int i ) { i = 0; } public s t a t i c void bar ( Test o ) { o . getal = 7; o = null ; } public s t a t i c void Main ( ) { int x = 5 ; Test y = new Test ( ) ; y . getal = 5; foo (x ) ; bar ( y ) ; // x i s 5 en y . g e t a l i s 7 } } Daarnaast is het echter ook mogelijk om parameters middels call-by-reference door te geven. Dit gebeurt dmv. het keyword ref, dat zowel bij de declaratie van de methode als bij de methode-oproep vermeld moet worden. c l a s s Test { public i nt g e t a l ; public s t a t i c void f o o ( r e f int i ) { i = 0; } public s t a t i c void bar ( r e f Test o ) { o . getal = 7; o = null ;
4
} public s t a t i c void Main ( ) { int x = 5 ; Test y = new Test ( ) ; y . getal = 5; foo ( ref x ) ; bar ( r e f y ) ; // x i s 0 en y i s n u l l } } Er bestaat ook een keyword out, waarmee uitvoerparameters kunnen worden aangegeven. Dit keyword doet hetzelfde als een ref, maar legt de bijkomende voorwaarde op dat de waarde die dit argument heeft op het moment van de functie-oproep niet gebruikt mag worden door de functie, en dat de functie verplicht is om dit argument zelf een waarde te geven. Tot slot is er ook nog het keyword params waarmee een methode een aantal optionele parameters kan krijgen, na zijn lijst van verplichte parameters. c l a s s Obj { private int data ; public Obj ( int v ) { data = v ; } int getData ( ) { return data ; } public s t a t i c void u i t v o e r ( out Obj o ) { // D i t mag n i e t : Console . WriteLine (” Foo : {0}” , o . g e t D a t a ( ) ) ; // D i t i s v e r p l i c h t : o = new Obj ( 2 5 6 ) ; } public s t a t i c void v a r i a b e l ( int i , params int [ ] r e s t j e s ) { C o n s o l e . WriteLine ( ” V e r p l i c h t : {0} ” , i ) ; foreach ( int r e s t in r e s t j e s ) { C o n s o l e . WriteLine ( ” o v e r s c h o t : {0} ” , r e s t ) ; } } public s t a t i c void Main ( ) { // Er z i j n ook u i t v o e r −p a r a m e t e r s : Obj o ; 5
o = new Obj ( 1 0 ) ; u i t v o e r ( out o ) ; C o n s o l e . WriteLine ( ” U i t v o e r : {0} ” , o . getData ( ) ) ; // Ook f u n c t i e s met v a r i a b e l a a n t a l argumenten kunnen ! variabel (1); variabel (1 ,2 ,3); variabel (1 ,2 ,3 ,4 ,5); } }
2.3
Naamruimtes
Om verwarring tussen gelijknamige onderdelen van een programma te voorkomen, gebruikt de CLI, en dus ook C#, het concept van naamruimtes. Een naamruimte wordt gedeclareerd met behulp van het keyword namespace en kan ge¨ımporteerd worden met het keyword using. Bij het importeren is het ook mogelijk om een afgekorte naam toe te kennen aan een naamruimte, of aan een specifieke klasse/methode uit deze ruimte. using System ; namespace j v e { namespace aa { c l a s s Obj { public string getNaam ( ) { return ” A p p l i c a t i e A r c h i t e c t u r e n ” ; } } } } namespace j v e . wt { c l a s s Obj { public string getNaam ( ) { return ” Webtec hnologie ” ; } } } namespace ns { using j v e . aa ; using wobj=j v e . wt . Obj ; c l a s s Bla { public s t a t i c void Main ( ) { Obj aa = new Obj ( ) ; j v e . wt . Obj w = new j v e . wt . Obj ( ) ; wobj wt = new wobj ( ) ; 6
C o n s o l e . WriteLine ( aa . getNaam ( ) ) ; C o n s o l e . WriteLine ( wt . getNaam ( ) ) ; C o n s o l e . WriteLine (w . getNaam ( ) ) ; } } }
2.4
Overerving
Net als elke objectgerichte taal, kent ook C# het concept van overerving. Volgende declaratie betekent dat klasse Kind overerft van de klasse Ouder. public c l a s s Kind : Ouder { ... } C# kent zoals gebruikelijk ook polymorfisme, dwz. dat een variabele die gedeclareerd is als Ouder ook een Kind-object mag bevatten, zoals hier: Ouder oud = new Kind ( ) ; Veronderstel nu dat er een methode foo() is in de klasse Ouder en in Kind: public c l a s s Ouder { public i nt f o o ( ) { return 3 5 ; } } public c l a s s Kind : Ouder { public i nt f o o ( ) { return 1 7 ; } } Als we nu deze methode willen oproepen op ons object: Ouder oud = new Kind ( ) ; oud . f o o ( ) ; dan moet er gekozen worden welke van deze twee (dwz. Ouder.foo() of Kind.foo()) er zal worden uitgevoerd. In tegenstelling tot bv. Java, wordt deze beslissing in C# standaard genomen tijdens het compileren van het programma. Op dat ogenblik kent de compiler natuurlijk enkel het statische type van de variabelen; dwz. hij weet enkel dat oud gedeclareerd is als een variabele van het type Ouder. Daarom zal de methode Ouder.foo() gekozen worden. Het is echter wel mogelijk om C# te instrueren dat deze beslissing moet worden uitgesteld tot tijdens de uitvoering van het programma. Op dat moment kan de runtime omgeving (CLR) zien dat hetgeen er werkelijk in de variabele oud zit, geen referentie is naar een Ouder object maar naar een Kind object, en dus
7
de specifiekere methode Kind.foo() selecteren. Hiervoor dienen de keywords virtual (in de superklasse) en override (in de subklasse): public c l a s s Ouder { public v i r t u a l int f o o ( ) { return 3 5 ; } } public c l a s s Kind : Ouder { public override int f o o ( ) { return 1 7 ; } } Dit tweede gedrag is meestal het “juiste”: er zijn erg weinig redenen om een methode niet virtual te maken. Zo ongeveer het enige voordeel van een niet-virtuele methode is dat ze een beetje sneller kan zijn tijdens het uitvoeren van het programma, aangezien bepaalde beslissingen al tijdens het compileren genomen konden worden. Meestal weegt dit echter niet op tegen de mogelijke bugs die hiermee ge¨ıntroduceerd kunnen worden. Daarom zal de C# compiler een waarschuwing genereren: vb7-overerving.cs(58,17): warning CS0108: ‘Kind.foo()’ hides inherited member ‘Ouder.foo()’. Use the new keyword if hiding was intended Als je echter zeker bent van je zaak, kan je deze waarschuwing laten verdwijnen met het keyword new: public c l a s s Ouder { public i nt f o o ( ) { return 3 5 ; } } public c l a s s Kind : Ouder { public new int f o o ( ) { return 1 7 ; } } Zoals gebruikelijk zal een subklasse in C# alle methodes van de superklasse overerven, behalve constructors. Om vanuit een constructor van een subklasse een constructor van de superklasse aan te noemen, wordt het keyword base gebruikt: public c l a s s Ouder { public i nt g e t a l ; public Ouder ( int i ) { 8
getal = i ; } } public c l a s s Kind : Ouder { public Kind ( int i ) : base ( i ) { g e t a l ++; } } Een oproep als new Kind(5) zal nu eerst als effect hebben dat de constructor van Ouder wordt opgeroepen. Een klasse in C# die zelf geen expliciete constructors aanbiedt, heeft impliciet een constructor zonder argumenten die niets doet. Als een constructor van een subklasse geen expliciete base(. . .) vermelding heeft, zal de constructor zonder argumenten van de superklasse toch sowieso worden opgeroepen. Indien deze constructor niet bestaat (omdat de superklasse wel minstens ´e´en constructor bevat, maar geen zonder argumenten), dan treedt er een compilatiefout op. Naast het keyword base bestaat er ook een keyword this om andere constructors van dezelfde klasse aan te roepen. public c l a s s Ouder { public i nt g e t a l ; public Ouder ( int i ) { getal = i ; } public Ouder ( ) : t h i s ( 7 ) { } } public c l a s s Kind : Ouder { public s t a t i c void Main ( ) { Kind k = new Kind ( ) ; // k . g e t a l i s 7 } }
2.5
Typecasts
C# kent twee verschillende manieren om typecasts te doen. De standaard manier is op deze manier: Kind k = ( Kind ) ouder ; Deze instructie zal een uitzondering gooien als de typecase in kwestie niet is toegestaan. Om dit te vermijden, kan het sleutelwoord as gebruikt worden: Kind k = ouder as Kind ; Indien de typecast niet is toegestaan, wordt de waarde van de uitdrukking ouder as Kind gewoon null. Controleren of een bepaalde variabele naar een bepaald
9
type getypecast kan worden, kan met de is operator. De uitdrukking ouder as Kind is indentiek aan ouder i s Kind ? ( Kind ) ouder : null ;
2.6
Properties
Properties zorgen in C# voor een betere inkapseling van gegevens. Een property is een namaak-attribuut, dat door get en set methodes ge¨ımplementeerd wordt. Cruciaal is dat het onderscheid tussen een echt attribuut of een property niet te zien is in code die dit attribuut/property gebruikt. Volgend fragmentje implementeert een property publiek die achter de schermen gebruik maakt van een volledig afgeschermd attribuut prive. c l a s s Obj { private int p r i v e ; public i nt p u b l i e k { get { return p r i v e ; } set { i f ( value < 0) C o n s o l e . WriteLine ( ” N i e t zo n e g a t i e f , g r a a g ! ” ) ; else prive = value ; } } } Deze property wordt nu op net dezelfde manier gebruikt alsof deze klasse een attribuut “public int publiek;” had, maar achter de schermen wordt gewoon telkens de get of set methode opgeroepen. Bijvoorbeeld: Obj o = new Obj ( ) ; o . publiek = 5; int i = o . p u b l i e k ; o . p u b l i e k = 0 ; // G e n e r e e r t f o u t e n b o o d s c h a p naar s t d o u t Dankzij deze properties wordt het “refactoren” van aan attribuut naar een methode of omgekeerd een veel minder ingrijpende operatie. Properties kunnen zowel read-write als read-only (of write-only, maar dat is niet zo nuttig). public i nt readOnly { get { return 7 ; } 10
} Hoewel het technisch gezien mogelijk is om in een get-methode ook de staat van het object te veranderen, werkt dit in het algemeen eerder verwarrend en het dan ook afgeraden.
2.7
Delegates
Een zeer krachtig concept in standaard C is de notie van een function pointer. Als we bijvoorbeeld volgende functie declareren: int f o o ( int i ) { return i ∗ 2 ; } dan is &foo een pointer naar deze functie, en deze kunnen we gebruiken zoals eender welke andere pointer. We kunnen hem bijvoorbeeld meegeven als argument aan een andere functie bar, die als argumenten een function pointer en geheel getal neemt: bar(& foo , 5 ) ; De notatie om het prototype van de functie bar te beschrijven, ziet er een beetje omslachtig uit, maar past uiteindelijk binnen de C filosofie dat een declaratie van een variabele en zijn gebruik er zoveel mogelijk hetzelfde moeten uitzien: int bar ( int ( ∗ f ) ( int i ) , int j ) ; Om dit een beetje leesbaarder te maken, is een typedef vaak aangewezen: t y p e d e f int ( ∗ f u n c t i e ) ( int i ) ; int bar ( f u n c t i e f , int j ) ; Tot slot kan de function pointer dan natuurlijk gebruikt worden om de functie op te roepen: int bar ( f u n c t i e f , int j ) { return ( ∗ f ) ( j +3); } In C# speelt het concept van een delegate een gelijkaardige rol. public delegate int f u n c t i e ( int i ) ; public c l a s s Test { public s t a t i c int bar ( f u n c t i e f , int j ) { return f ( j +3); } public s t a t i c int f o o ( int i ) { return i ∗ 2 ; 11
} public s t a t i c void Main ( ) { f u n c t i e f = new f u n c t i e ( f o o ) ; int i = bar ( f , 5 ) ; } } Hier declareren we de delegate functie als een functie die een int als argument neemt en een int als resultaat teruggeeft. Een dergelijke delegate functioneert in C# als een type, wat we ook terugzien in de declaratie van de methode bar. Om een “object” van dit type aan te maken, gebruiken we de new operator, samen met een constructor die 1 argument neemt, namelijk een functie van de juiste signatuur. Het bovenstaande voorbeeld gebruikt enkel statische functies, maar de echte kracht van delegates in C# komt pas naar boven als we ook methodes van een object beschouwen. Bijvoorbeeld: using System ; public delegate int f u n c t i e ( int i ) ; public c l a s s V e r m e n i g v u l d i g e r { private int f a c t o r ; public V e r m e n i g v u l d i g e r ( int x ) { factor = x; } public i nt v e r m e n i g v u l d i g ( int y ) { return f a c t o r ∗y ; } public s t a t i c void Main ( ) { V e r m e n i g v u l d i g e r maalVier = new V e r m e n i g v u l d i g e r ( 4 ) ; V e r m e n i g v u l d i g e r maalZes = new V e r m e n i g v u l d i g e r ( 6 ) ; f u n c t i e mV = new f u n c t i e ( maalVier . v e r m e n i g v u l d i g ) ; f u n c t i e mZ = new f u n c t i e ( maalZes . v e r m e n i g v u l d i g ) ; f u n c t i e [ ] malen = {mV, mZ} ; foreach ( f u n c t i e m in malen ) { C o n s o l e . WriteLine (m( 3 ) ) ; } } }
12
2.8
Herdefini¨ eren van operatoren
C# laat niet alleen het overloaden van gewone methodes toe, maar ook het overloaden van operator-symbolen zoals +, ++, *,. . . . Aan een dergelijke operator kan je een statische functie koppelen, die het juiste aantal argumenten van een bepaald type neemt. De onderstaande code defini¨eert bijvoorbeeld een unaire ++ en binaire +, die allebei toepasbaar zijn op Teller objecten: using System ; class Teller { private int waarde ; private T e l l e r ( int i ) { waarde = i ; } public T e l l e r ( ) : t h i s ( 0 ) { } public s t a t i c T e l l e r operator ++ ( T e l l e r t ) { t . waarde++; return t ; } public s t a t i c T e l l e r operator + ( T e l l e r t1 , T e l l e r t 2 ) { return new T e l l e r ( t 1 . waarde + t 2 . waarde ) ; } public s t a t i c void Main ( ) { T e l l e r x = new T e l l e r ( ) ; T e l l e r y = new T e l l e r ( ) ; x++; x++; y++; y += x ; // y . waarde i s 3 } } Ook type-cast operatoren, die een bepaald type proberen om te zetten naar een ander type, kunnen overladen worden. Er zijn twee soorten van dergelijke operatoren: impliciete en expliciete. Een impliciete typecast is eentje achter de schermen gebeurt, zonder dat de gebruiker dit expliciet moet opgeven zoals bijvoorbeeld de “boxing” typecasts. Deze kunnen we als volgt overladen: public s t a t i c i m p l i c i t operator int ( T e l l e r t ) { return t . waarde ; 13
} Als we nu iets doen als T e l l e r t = new T e l l e r ( ) ; int i = t + 4 ; zal deze conversie-operator achter de schermen worden toegepast om de Teller om te zetten naar een int. We kunnen ook als volgt een expliciete operator defini¨eren: public s t a t i c e x p l i c i t operator int ( T e l l e r t ) { return t . waarde ; } Deze moet dan als volgt gebruikt worden: T e l l e r t = new T e l l e r ( ) ; int i = ( ( int ) t ) + 4 ; 2.8.1
Indexers
Een speciaal soort operator is de indexatie-operator van arrays. Hiervoor gebruik je niet de notatie van hierboven, maar een speciale indexer property. using System ; c l a s s Ee nhe idsMa trix { private int rows ; public Ee nhe idsMa trix ( int i ) { rows = i ; } public i nt t h i s [ int i , int j ] { get { i f ( i == j ) return 1 ; return 0 ; } } public i nt Length { get { return rows ; } } public s t a t i c void Main ( ) { 14
Ee nhe idsMa trix I = new Ee nhe idsMa trix ( 5 ) ; f o r ( int i = 0 ; i < I . Length ; i ++) { f o r ( int j = 0 ; j < I . Length ; j ++) C o n s o l e . Write ( ” {0} ” , I [ i , j ] ) ; C o n s o l e . Write ( ” \n” ) ; } } }
2.9
Collecties
C# biedt een uitgebreide verzameling van collectie types (Stack, Queue, HashTable, ArrayList,...) aan in de System.Collections naamruimte. Itereren over deze types kan heel eenvoudig met een foreach constructie: public s t a t i c void Main ( ) { A r r a y L i s t l i j s t = new A r r a y L i s t ( ) ; l i j s t . Add( ” h a l l o ” ) ; l i j s t . Add( ” w e r e l d ” ) ; foreach ( string s in l i j s t ) { C o n s o l e . WriteLine ( s ) ; } } Deze foreach werkt achter de schermen met de interface IEnumerable1 Om ook je eigen klassen op deze manier te kunnen gebruiken, moet ze dus ook deze interface implementeren. Deze interface ziet er als volgt uit: public i n t e r f a c e IEnumerable { public IEnumerator GetEnumerator ( ) } public i n t e r f a c e IEnumerator { public object Current { g e t ; } //Read−o n l y p r o p e r t y public bool MoveNext ( ) ; // Returns f a l s e i f t h e r e // were no more ‘ next ’ e l e m e n t s public void Reset ( ) ; } Hier is een voorbeeld: using System ; using System . C o l l e c t i o n s ; c l a s s A f t e l l e r : IEnumerator , IEnumerable { 1 Per
conventie beginnen in C# de namen van interfaces beginnen met een hoofdletter I.
15
private int private int private int
van ; tot ; huidig ;
public A f t e l l e r ( int van ) { van = van ; tot = 0; Reset ( ) ; } public void Reset ( ) { h u i d i g = van ; } public object Current { get { return h u i d i g ; } } public bool MoveNext ( ) { i f ( h u i d i g <= t o t ) return f a l s e ; else { h u i d i g −−; return true ; } } public IEnumerator GetEnumerator ( ) { return t h i s ; } public s t a t i c void Main ( ) { foreach ( int i in new A f t e l l e r ( 8 ) ) C o n s o l e . WriteLine ( i ) ; } }
2.10
Attributen
C# ondersteunt attributen om extra informatie toe te voegen aan code. Hiervoor worden vierkante haken gebruikt. E´en nuttige toepassing is dat deze annotaties gebruikt kunnen worden om te zorgen voor conditionele compilatie,
16
zoals met de #ifdef preprocessorinstructie van C. Bijvoorbeeld: using System . D i a g n o s t i c s ; [ C o n d i t i o n a l ( ”DEBUG” ) ] public void C o n t r o l e e r O p l o s s i n g ( ) { ... } Het effect hiervan is dat alle oproepen van deze methode ControleerOplossing verwijderd zullen worden wanneer de code gecompileerd wordt zonder dat DEBUG gedefini¨eerd is. Om w´el deze methode-oproepen te krijgen, kan aan de Mono C# compiler de command line optie -d:DEBUG worden toegevoegd. Een andere interessant toepassing van attributen is de ingebouwde ondersteuning die C# biedt voor het omzetten van objecten naar XML.
17