1
Bijlage A
C# voor Java-kenners A.1
Taal- en compiler-versies
C# is een iets jongere taal dan Java: de eerste versie is van 2000, waar Java ontstond in 1996. Sinds april 2010 is er versie 4.0, die ondersteund wordt door de Visual Studio 2010 compiler. Dit sluit ook aan bij versie 4 van het .NET framework. Als Game-library gebruiken we XNA. Daarvan is de versie 4 nog in het beta-stadium; de recentste stabiele versie is 3.1, die ondersteund wordt door de Visual Studio 2008 compiler. Net als Java wordt C# vertaald naar code voor een virtuele machine. Bij Java wordt die code byte code genoemd, bij C# is dit code in de zogeheten common intermediate language. Een verschil met Java is dat Java byte code wordt ge¨ınterpreteerd, terwijl de intermediate language wordt gecompileerd naar machinecode. Dit gebeurt just in time (JIT), dat wil zeggen vlak voordat de code voor het eerst wordt uitgevoerd. De Visual Studio compiler genereert code voor het Windows operating system. Anders dan wel wordt gedacht zijn er ook andere compilers. De belangrijkste is Mono. Deze ligt iets achter bij in ondersteunde versies van taal en libraries. Vergeleken met Java laat de cross-platform mogelijkheid te wensen over. Daar staat tegenover dat de intermediate language het backend is van meerdere talen; C# programma’s kunnen dus samenwerken met modules die in andere talen, zoals Visual Basic of F# zijn geschreven. Zie figuur 1 voor een vergelijking van de compilatie-pipeline in Java en C#.
Java:
.java sourcecode
Compiler
.class bytecode
Interpreter voor processor 1 Interpreter voor processor 2
.cs sourcecode
Compiler voor taal 1
.vb sourcecode
Compiler voor taal 2
C#:
intermediate language .il “assembly”
Compiler voor processor 1
.exe machinecode voor processor 1
Compiler voor processor 2
.a machinecode voor processor 2
Figuur 1: Compilatie-pipeline voor Java en C#
2
A.2
blz. 3
C# voor Java-kenners
Hallo, C# (console-versie)
In Java hadden we de keuze om een applet of een application te maken. In C# zijn er geen applets, en zal een programma dus altijd een application met een methode Main zijn. Het simpelste voorbeeld is een console-applicatie, die van de standaardinvoer leest en naar de standaarduitvoer schrijft. In listing 1 staat een voorbeeld. De overeenkomst met een soortgelijk Java-programma is evident, maar er zijn ook kleine verschillen die in dit programma opvallen. Gebruik hoofdletters voor public members Net als in Java zijn namen in C# hoofdlettergevoelig, en is er een conventie (niet verplicht voor de compiler, maar zo gehanteerd in de library en door goed-opgevoede programmeurs) welke namen met een hoofdletter worden geschreven. Die conventie verschilt: werden in Java klassen met een hoofdletter geschreven en methoden met een kleine letter, in C# worden public methoden (en andere class-members) ook met een hoofdletter geschreven. Private class-members, lokale variabelen en parameters worden met een kleine letter geschreven. Net als in Java worden in C# primitieve typen (int, char enz.) met een kleine letter geschreven, en klassen met een hoofdletter. In het programma wordt het type string met een kleine letter geschreven, en inderdaad is string in C# een primitief type. Maar een string-variabele is wel degelijk, net als in Java, een object-verwijzing. De reden dat string toch een primitief type is, is dat de compiler speciale dingen doet met dit type (constanten maken als de programmeur quotes gebruikt, automatisch ToString aanroepen waar nodig, enz.). Voor het type object geldt iets dergelijks. Overigens zijn er in C# toch ook klassen String en Object, en zijn die types helemaal uitwisselbaar met string en object. Verstokte Java-programmeurs kunnen deze types dus gewoon met een hoofdletter blijven schrijven, maar in het wild zie je meestal de kleine-letterversies. Schrijven naar standaarduitvoer De methode WriteLine is een statische methode in de klasse Console. Op het eerste gezicht lijkt dat een verschil met de Java-aanpak, waar println een niet-statische methode is, die wordt aangeroepen met het statische object System.out onder handen. In C# is dat ook mogelijk, want er bestaat een statisch object Console.Out die een niet-statische methode WriteLine kent. De statische Console.WriteLine is alleen maar een convenience functie die alsnog Console.Out.WriteLine aanroept. Keus uit 4 versies van Main Het schrijven van een methode Main is, net als main in een Java-application, verplicht. Maar er is keuze uit 4 varianten: met of zonder een string-array parameter, en met void of int als resultaat. Als je de command-line arguments niet nodig hebt is het gemakkelijker om Main zonder parameters te declareren, en als je een exit-code naar het operating system wil teruggeven kun je Main een int laten opleveren. Importeren van libraries De klasse Console die in het voorbeeldprogramma wordt gebruikt is afkomstig uit een library. De bewuste library (System) moet met een using-directive worden aangevraagd. Dit is vergelijkbaar met het import-directive in Java. In Java importeer je echter losse klassen (al kun je met * alle klassen van een library tegelijk importeren), in C# krijg je altijd alle klassen van de library (al kun je met wat extra moeite ook losse klassen importeren). De library System moet, anders dan in Java, expliciet worden aangevraagd. Impliciete aanroep van ToString Net als in Java is de operator + overloaded om strings te concateneren. Indien ´e´en van de parameters een ander type heeft, wordt daarvan net als in Java de methode ToString aangeroepen. Dat geldt zelfs als zo’n parameter een standaardtype zoals int heeft, want in C# kunnen ook standaardtypen methodes hebben. Properties De lengte van de string naam wordt berekend door naam.Length. Anders dan in Java is dit geen methode-aanroep, en staan er dus geen haakjes achter Length. Maar wat is het dan wel? Op het eerste gezicht zou het om een public membervariabele van de klasse string kunnen gaan, maar dat is niet het geval: dan zou je immers zomaar de lengte van een string kunnen aanpassen, en
A.2 Hallo, C# (console-versie)
3
using System;
5
10
class Hallo2 { static void Main() { string naam; Console.WriteLine("Wat is je naam?"); naam = Console.ReadLine(); Console.WriteLine("Hallo, " + naam + "!"); Console.WriteLine("Je naam heeft " + naam.Length + " letters." ); Console.ReadLine(); } } Listing 1: Hallo2/Hallo2.cs
dat is natuurlijk niet mogelijk. De member Length van een string is een nieuw soort member: een property. De programmeur van een klasse bepaalt of een property read-only is, dus alleen maar opgevraagd mag worden, of dat hij ook (met een toekenningsopdracht) gewijzigd mag worden. Ook bepaalt de programmeur van de klasse wat er gebeurt als een property wordt opgevraagd en/of veranderd: correspondeert de property simpelweg met een private member-variabele? Of wordt de property uitgerekend aan de hand van andere member-variabelen? Als gebruiker van een property kun je niet zien hoe die is ge¨ımplementeerd. In het geval van de Length van een string zou het kunnen zijn dat de lengte expliciet is opgeslagen, maar net zo goed zouden de characters van de string alsnog geteld kunnen worden op het moment dat de Length wordt opgevraagd.
4
A.3 blz. 5 blz. 7
blz. 5
C# voor Java-kenners
Hallo, C# (window-versies)
We gaan twee window-gebaseerde versies van een Hallo-programma bekijken: eentje waarin de tekst op een Label-component wordt getoond (listing 2), en eentje waarin de tekst zelf met behulp van een Graphics-object wordt getekend (listing 3). Form: superklasse voor window-klassen In Java maak je een window-gebaseerde applicatie door een subklasse van Frame te schrijven, en daar in main een object van aan te maken. In C# gaat dat precies zo, alleen heet de superklasse hier Form, uit de library System.Windows.Forms. De klasse-header van HalloForm in listing 2 laat zien hoe je in C# een subklasse maakt: niet met extends zoals in Java, maar met een dubbelepunt zoals in C++. In de methode Main wordt een object van deze zelfgemaakte subklasse aangemaakt, net zoals je in Java zou doen. Waar je in Java vervolgens de methode show van het nieuwe object zou aanroepen, wordt in C# het nieuwe object echter meegegeven aan de statische methode Run van de klasse Application. Een programma met meerdere klassen In het voorbeeldprogramma is de methode Main ondergebracht in een eigen klasse. Dat is, net zoals in Java, niet strikt noodzakelijk: je zou hem er ook bij kunnen smokkelen in de andere klasse. Maar zeker als die andere klasse wat groter wordt, is het gebruikelijker om Main in een aparte klasse te zetten. Meerdere klassen kunnen samen in ´e´en source-file staan. anders dan in Java zijn er geen voorwaarden aan het publiek-zijn van die klasse of aan de naamgeving van de file. Properties Properties worden volop gebruikt in de Forms-library. In de constructor van HalloForm worden de properties Text, BackColor en Size aangepast om het hoofdwindow naar smaak aan te passen. Het vergeten aan te passen van de Size is overigens minder fataal dan in Java: in Java is de default size (0,0), in C# is dat (400,400) zodat er in ieder geval iets te zien is. In de constructormethode wordt verder een Label-object aangemaakt, waarvan ook de nodige properties worden aangepast. Omdat een C#-form standaard geen layoutmanager heeft, moet van elke control expliciet de Location worden vastgelegd. Controls in een GUI Alle controls waaruit de grafische userinterface is opgebouwd moeten worden meegegeven aan Add. In Java AWT schrijf je this.add(c), in Java Swing is dat this.getContentPane().add(c). In C# lijkt het op dat laatste: this.Controls.Add(c). De constructormethode van controls, zoals Label, heeft in C# geen parameters. Alles wat er aan controls valt te parameteriseren gebeurt via properties. Andere controls die er bestaan zijn onder andere Button, TextBox (met een boolean property Multiline) en TrackBar (schuifregelaar met een schaalverdeling).
A.3 Hallo, C# (window-versies)
using System.Windows.Forms; using System.Drawing;
5
10
class HalloForm : Form { public HalloForm() { this.Text = "Hallo"; this.BackColor = Color.Yellow; this.Size = new Size(200, 100); Label groet; groet = new Label(); groet.Text = "Hallo allemaal"; groet.Location = new Point(30, 20);
15
this.Controls.Add(groet); } } 20
25
class HalloWin2 { static void Main() { HalloForm scherm; scherm = new HalloForm(); Application.Run(scherm); } } Listing 2: HalloWin2/HalloWin2.cs
5
6
blz. 7
C# voor Java-kenners
Zelf tekenen in een Form In listing 3 staat een andere aanpak van Hallo-in-een-window. In plaats van een Label-control wordt hier de tekst direct op het scherm getekend. In Java zou je voor dit doel de paint-functie van het window herdefini¨eren. In C# gebeurt iets dergelijks, maar mag je de naam van de functie zelf bedenken (in het voorbeeld: tekenScherm). Zo’n zelfbedachte functie wordt natuurlijk niet automatisch aangeroepen, maar dat gebeurt wel als je deze functie aanmeldt met: this.Paint += this.tekenScherm;
Hierin is Paint een bijzonder soort property, namelijk een event. In een event kan een functie worden opgeslagen. Dus niet het resultaat van de functie, maar de functie zelf! Deze functie wordt pas aangeroepen op het moment dat het systeem het nodig vindt dat het scherm getekend moet worden. Eigenlijk is het nog iets subtieler: niet alleen de functie wordt opgeslagen (in het voorbeeld: tekenScherm, maar ook het object waarmee die functie later aangeroepen zal worden (in het voorbeeld: this). En bovendien is er ruimte voor meer dan ´e´en functie-met-context; daarom staat er in bovengenoemde opdracht niet = maar +=. Onze functie tekenScherm met zijn context this wordt daarmee ´e´en van de mogelijk vele abonnees op het Paint-event. Het is verboden om aan een event-property met = direct een waarde toe te kennen: daarmee zou je immers de hele collectie van alle abonnees kunnen wegvagen. Je mag daarom alleen met += een extra abonnee toevoegen, en eventueel (maar dat gebeurt niet zo vaak) met -= een bepaalde abonnee verwijderen. Event-properties Het event-mechanisme wordt in de library ook gebruikt op plekken waar in Java event-listeners nodig zijn. Bijvoorbeeld, een Button heeft een event-property Click. Met button.Click += object.functie;
kun je er voor zorgen dat op het moment dat de button wordt geklikt, de functie zal worden aangeroepen met object onder handen. In Java zou je dat bereiken met button.addActionListener(object);
maar dan zou je bovendien in de klasse van object de interface ActionListener moeten implementeren door de functie actionPerformed te defini¨eren. In C# is het, vooral als je meer dan ´e´en button hebt, eenvoudiger dan in Java: je kunt voor elke button (desgewenst) een eigen afhandel-functie maken. In Java kan dat alleen maar als je voor elke button een eigen klasse maakt die de methode actionPerformed heeft. (Dat wordt weliswaar in Java weer gemakkelijk gemaakt door de mogelijkheid om een naamloze klasse in zijn geheel op te schrijven in de aanroep van addActionListener, maar dat is syntactisch ook weer ingewikkeld). Het type van abonnee-functies op events Alleen functies die het juiste type parameters en resultaat hebben mogen zich abonneren op een bepaald event. Welke types dat zijn wordt vastgelegd door de programmeur van het event. In het geval van het Paint event moet de abonnee een functie zijn met void-resultaat en met twee parameters: een van het type object en een van het type PaintEventArgs. Voor bijvoorbeeld het Click event zijn dat weer andere types. Graphics In Java heeft de methode paint een Graphics als parameter. In C# heeft de eventhandler van het Paint-event (tekenScherm in het voorbeeld) als tweede parameter een PaintEventArgs-object. Dat is een datastructuur die, zoals de naam al aangeeft, alle belangrijke argumenten van een paintevent bevat. Je kunt daar een aantal eigenschappen van opvragen, onder andere Graphics. Dat levert een object op waarvan het type ook Graphics heet, en dat ongeveer dezelfde rol speelt als de Graphics-parameter van paint in Java: het kent methoden zoals DrawString, DrawLine, FillRectangle (let op: niet fillRect zoals in Java), en DrawEllipse (let op: niet drawOval zoals in Java). Afgezien van kleine variaties in de naam van de methodes en de volgorde van parameters is er een belangrijker verschil tussen de klasse Graphics in Java en in C#. In Java heeft het Graphics-object toestandsvariabelen voor de kleur en het font waarmee latere tekenopdrachten worden uitgevoerd. In C# zou je misschien properties voor dat soort dingen verwachten, maar die zijn er niet. In
A.3 Hallo, C# (window-versies)
7
using System.Windows.Forms; using System.Drawing;
5
10
class HalloForm : Form { public HalloForm() { this.Text = "Hallo"; this.BackColor = Color.Yellow; this.Size = new Size(200, 100); this.Paint += this.tekenScherm; } void tekenScherm(object obj, PaintEventArgs pea) { pea.Graphics.DrawString( "Hallo!" , new Font("Tahoma", 30) , Brushes.Blue , 10, 10 ); }
15
20
}
25
30
class HalloWin3 { static void Main() { HalloForm scherm; scherm = new HalloForm(); Application.Run(scherm); } } Listing 3: HalloWin3/HalloWin3.cs
plaats daarvan wordt de te gebruiken kleur en dergelijke bij elke aanroep van tekenopdrachten apart meegegeven. Dit gebeurt in de vorm van een Pen-object bij de Draw-methoden, en in de vorm van een Brush-opdracht bij de Fill-methoden. De methode DrawString krijgt ook een Brush mee (omdat letters een oppervlakte hebben) en daarnaast ook een Font-object. Voor veelgebruikte kleuren zijn er statische objecten kant-en-klaar beschikbaar in de klassen Pens respectievelijk Brushes. Maar je kunt natuurlijk ook pennen van afwijkende dikte, brushes met een bijzondere arcering, en pennen of brushes met een zelfgemaakte mengkleur aanmaken.
8
A.4
blz. 9
C# voor Java-kenners
Hallo, C# (Game-versie)
De XNA-library We kunnen met C# games ontwikkelen met behulp van een toolkit genaamd XNA. De actuele versie daarvan is XNA 3.1, die alleen samenwerkt met Visual Studio 2008; er is een beta-versie van XNA 4.0 die bedoeld is voor Visual Studio 2010. De opzet van zo’n game biedt weinig verrassingen voor wie de opzet van een applet in Java en/of een Forms-applicatie in C# kent. Het bijzondere van XNA is echter dat het programma zonder noemenswaardige aanpassingen van de sourcetekst ook gecompileerd kan worden voor andere hardware, zoals de Xbox. De algemene opzet van een XNA-game mag dan lijken op een Forms-applicatie, in de details zijn er wel (veel) verschillen. In listing 4 staat een programma met precies dezelfde functionaliteit als dat in de vorige sectie, maar dan met gebruikmaking van de XNA-library in plaats van de Forms-library. Opzet van een XNA-programma Net als in het vorige programma maken we een subklasse van een klasse uit de library: ditmaal niet van Form, maar van Game. In de using-directive bovenaan het programma wordt natuurlijk ook de betreffende librarynaam vermeld. In de functie Main wordt, ook weinig verrassend, ´e´en object van onze subklasse van Game aangemaakt. Verschillend is echter de daaropvolgende aanroep van Run: in Forms was dat een statische methode in de klasse Application, waaraan het nieuwe object als parameter wordt meegegeven; in XNA is het een niet-statische methode in de klasse Game zelf, waaraan het nieuwe object onderhanden wordt gegeven. Tekenen in een XNA-programma De XNA-aanpak om in een window te kunnen tekenen lijkt eigenlijk meer op de Java-aanpak dan de Forms-aanpak. Ditmaal geen event-properties waarop je functies kunt abonneren, maar de uit Java bekende herdefinitie van een bepaalde methode met een in de library vastgelegde naam. In een Java-applet was dat paint, in een XNA-programma is dat Draw. In C# moet je, als je een functie wilt herdefini¨eren, in de header het keyword override erbij schrijven. (Het alternatief, dat je kunt gebruiken als je juist niet het eventhandler-achtige gedrag van herdefinitie wilt hebben, is om new in de header te schrijven. Maar dat doen we hier natuurlijk niet). XNA Draw methode kent standaard animatie Een verschil met Java-applets, en ook met de Paint-eventhandler in een Forms-applicatie, is dat de methode Draw steeds opnieuw wordt aangeroepen. Je krijgt animatie dus kado: het is niet nodig om een Thread aan te maken zoals in Java en in Forms-applicaties. Er zijn twee herdefinieerbare methoden die door een Game-object afwisselend steeds opnieuw worden aangeroepen (in de zogeheten game-loop): Update en Draw. De bedoeling is dat in Update de toestand van het spel wordt aangepast, en dat die in Draw wordt gevisualiseerd. In ons eenvoudige voorbeeldprogramma gebruiken we alleen Draw, omdat er geen toestandsvariabelen zijn. SpriteBatch in plaats van Graphics In XNA is er geen klasse Graphics. Als je wilt tekenen heb je in plaats daarvan een SpriteBatch object nodig. Het woord sprite is standaard game-jargon voor bewegende figuurtjes en dergelijke. De klassenaam SpriteBatch is een misleidende naam, want een SpriteBatch is niet een collectie sprites, maar een apparaat waarmee je sprites (maar ook andere dingen) kunt tekenen. Zo is er een methode Draw waaraan je een sprite kunt meegeven die dan getekend wordt, maar ook een methode DrawString waarmee je een string kunt tekenen. Een SpriteBatch vervult dus dezelfde rol die een Graphics in Java en in een Forms-applicatie heeft. Hoe kom je aan zo’n SpriteBatch object? Het is niet een parameter van de methode, zoals de Graphics-parameter van paint in Java. Ook niet indirect, zoals de PaintEventArgs-parameter in een Forms-applicatie. In plaats daarvan moet je een SpriteBatch zelf new aanmaken. De constructormethode van SpriteBatch heeft een GraphicsDevice-object nodig als parameter, maar zo’n ding is gelukkig op te vragen als property van een Game-object. Die property is van het type GraphicsDevice, en heet zelf ook GraphicsDevice. Behalve bij de constructie van de SpriteBatch
A.4 Hallo, C# (Game-versie)
9
using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics;
5
10
public class HalloGame : Game { public HalloGame() { this.Window.Title = "HalloGame"; this.Content.RootDirectory = "Content"; GraphicsDeviceManager manager; manager = new GraphicsDeviceManager(this); } protected override void Draw(GameTime gameTime) { this.GraphicsDevice.Clear(Color.Yellow);
15
SpriteBatch spriteBatch; spriteBatch = new SpriteBatch(this.GraphicsDevice); spriteBatch.Begin(); spriteBatch.DrawString( this.Content.Load<SpriteFont>("SpelFont") , "Hallo!" , new Vector2(gameTime.TotalRealTime.Milliseconds, 20) , Color.Blue ); spriteBatch.End();
20
25
} } 30
35
class Program { static void Main() { HalloGame spel; spel = new HalloGame(); spel.Run(); } } Listing 4: HalloGame/HalloGame.cs
10
C# voor Java-kenners
is de GraphicsDevice ook nodig om de achtergrond te wissen: dat gebeurt namelijk niet automatisch. Let op dat de volgorde `en de types van de parameters van DrawString hier net een beetje anders zijn dan bij de naamgenoot in Graphics. Alle aanroepen van teken-methodes die je met een SpriteBatch doet moeten overigens nog worden ingeklemd tussen aanroepen van Begin en End. Initialisatie van een Game In de constructor van je Game-subklasse kun je de nodige initialisaties neerzetten. Vervelend is dat het daar altijd noodzakelijk is om de volgende regels op te nemen: GraphicsDeviceManager gdm; gdm = new GraphicsDeviceManager(this);
Zelfs als je, zoals in het voorbeeldprogramma, die variabele nergens meer gebruikt, is de aanmaak van een GraphicsDeviceManager-object verplicht. De reden hiervoor is dat deze aanroep als neveneffect een initialisatie doet op de meegegeven this: pas vanaf dit moment kent this een GraphicsDevice, en die hebben we later nodig. Zonder manager geen device, zonder device geen spritebatch, en zonder spritebatch kunnen we niet tekenen. . . Game-content Een beetje game heeft content nodig: (achtergrond-)plaatjes, geluidseffecten, en andere hulpbestanden. Ook fonts vallen in XNA in deze categorie. Het gebruik hiervan wordt door de library ondersteund, in de vorm van de Content-property van het Game-object. In het voorbeeldprogramma kun je zien dat het nodig is om van dat Content-object de property RootDirectory te zetten (dat is de directory waarin de hulpbestanden zich bevinden). Daarna kan de polymorfe methode Load worden aangeroepen om het gewenste hulpbestand te laden. In het voorbeeld wordt dit gebruikt om een SpriteFont te laden; een andere mogelijkheid is bijvoorbeeld een Sprite. Laden van Game-content In het voorbeeldprogramma staat de aanroep van Load in de methode Draw. Dat is niet verboden, maar een beetje ongebruikelijk: nu wordt het font elke keer (typisch 20 keer per seconde) opnieuw ingeladen. Effici¨enter is het om dit eenmalig in de constructormethode te doen, het op te slaan in een member-variabele, en die vervolgens te gebruiken in Draw. Omwille van de eenvoud is dat in het voorbeeldprogramma niet gedaan. Iets wat je ook vaak ziet in XNA-programma’s is dat het SpriteBatch-object eenmalig wordt aangemaakt in de constructor, in plaats van in de Draw-methode. Het is echter de vraag of de winst in performance die dat mogelijk oplevert opweegt tegen weer meer rommel bij de membervariabelen.
A.5 Programmastructuur
11
compilatie eenheid extern
naam
alias
; library
naam
using
;
klasse
naam
=
naam
toplevel declaratie
toplevel declaratie type declaratie namespace
naam
{
toplevel declaratie
}
Figuur 2: Syntax van een source-file
A.5
Programmastructuur
Namespaces In figuur 2 staat de opbouw van een compilatie-eenheid, oftewel een file met sourcecode. Het grootste deel van een compilatie-eenheid bestaat uit toplevel declaraties. Uit het syntaxdiagram van ‘toplevel declaratie’ blijkt dat je daarbij moet denken aan type-declaraties (klassen, interfaces en dergelijke), maar dat het ook mogelijk is om toplevel declaraties te groeperen onder een namespace header. Een namespace komt overeen met een Java package, met het verschil dat in C# de hele inhoud van de namespace tussen accolades moet staan, terwijl in Java de package-header een losse regel is. In C# kun je dus ook meerdere namespaces in ´e´en file declareren. Namespaces kunnen ook worden genest; op deze manier onstaan namespaces zoals System.Windows.Forms. Using In een namespace kun je type-declaraties uit andere namespaces gebruiken, door bovenin de sourcetext een using-directive te schrijven. Dit is te vergelijken met een import-directive in Java, met het verschil dat je in C# altijd een hele namespace importeert. In Java zou je daartoe nog .* achter de package-naam moeten schrijven. Je kunt dus niet, zoals in Java, een losse klassenaam importeren. Dat kan echter weer wel (zie in het syntax-diagram het tweede alternatief achter using) als je de klassenaam daarbij herbenoemt met een andere naam. Dit kan handig zijn als je twee klassen uit twee verschillende namespaces wilt importeren die ongelukkigerwijs dezelfde naam hebben. Extern alias Het herbenoemen helpt niet meer als er een naamconflict is tussen de volledige qualified name van twee namespaces. Kan dat dan? Ja dat kan, bijvoorbeeld als je twee verschillende versies van dezelfde library zou willen importeren. Voor deze situatie is de extern alias directive voorzien. Op de commandoregel moet dan worden gespecificeerd welke alias aan welke fysieke file met intermediate code (‘assembly’) wordt gekoppeld.
12
A.6
C# voor Java-kenners
Type-declaraties
Met de toplevel declaraties, al dan niet gebundeld in een namespace, declareer je nieuwe types. Net als Java kent C# klassen en interfaces, maar er zijn ook nog drie andere soorten typen mogelijk: struct, enum en delegate. In figuur 3 is de syntax van de diverse type-declaraties aangegeven. Klassen en interfaces Net als in Java kennen klassen (en interfaces) een overervings-hi¨erarchie. Net als in Java kan een klasse maar van ´e´en klasse de subklasse zijn, maar kun je zo veel interfaces implementeren als je wilt. De syntax is een beetje anders. Waar je in Java schrijft: class MijnKlasse extends SuperKlasse implements Interface1, Interface2
schrijf je in C#: class MijnKlasse : SuperKlasse, Interface1, Interface2
Achter de dubbele punt staan dus zowel de superklasse (als die er is, anders is dat impliciet Object) en de interfaces opgesomd. Aan de syntax van een header als class A : B kun je dus niet zien of B een superklasse of een ge¨ımplementeerde interface is. In libraries wordt daarom vaak de conventie aangehouden dat de naam van interfaces met een I begint, zoals ICollection en ISet. Direct achter de naam van de klasse kunnen tussen punthaken type-parameters worden geschreven, om de klasse generiek te maken. Denk aan klassen zoals List
, die je later kunt instanti¨eren met bijvoorbeeld List<String>. Deze syntax is hetzelfde als in Java. Als er echter voorwaarden aan de type-parameters zijn verbonden (namelijk dat ze een sub- of juist een superklasse van een andere klasse moeten zijn), dan wordt dat gespecificeerd in een apart blokje met constraints. De syntax daarvan is in het diagram in figuur 3 niet verder uitgewerkt. Structs Nieuw ten opzichte van Java zijn struct-types. Net als een klasse beschrijft een struct de opbouw en mogelijkheden van een object. Het verschil is echter dat een variabele van een struct-type het hele object bevat, terwijl een variabele van een class-type een verwijzing naar het object bevat. Dat betekent dat bij toekenningen aan variabelen met een struct-type het hele object gekopieerd wordt, en ook als je struct-waarden meegeeft als parameter. In de library worden struct-types vooral gebruikt voor ‘kleine’ objectjes. Belangrijkste voorbeelden zijn Color en Point. Een Color bestaat immers uit slechts vier bytes (rood-, groen-, blauw- en alpha-kanaal), en is daarmee net zo klein als een enkele int. De notatie voor het gebruik van struct-objecten is precies hetzelfde als die van klasse-objecten. Vergelijk bijvoorbeeld dit fragmentje, waar de struct Point en de klasse Bitmap worden gebruikt: Point p1, p2; Bitmap b1, b2; p1 = new Point(1,2); p2 = p1; b1 = new Bitmap(); b2 = b1;
Dat in dit voorbeeld het point p1 wordt gekopieerd naar p2, terwijl bij de bitmap b2 slechts een extra verwijzing naar het bestaande object b1 wordt, kun je aan het fragment niet zien! Het verschil in semantiek kun je alleen inzien als je weet hoe de types Point en Bitmap zijn gedeclareerd (respectievelijk als struct en als class). Enums Met een enum-type kun je gemakkelijk types maken die uit een opsomming van een eindig aantal elementen bestaan. Bijvoorbeeld: enum Beest = { Koe, Paard, Schaap };
In Java zou je de codering expliciet moeten uitschrijven: final int Koe=1, Paard=2, Schaap=3;
en dit is bovendien minder type-veilig: een programmeur zou per ongeluk beesten kunnen vermenigvuldigen en delen. Delegates Met delegate-types betreedt C# het pad van functioneel programmeren. Een delegate is, ruwweg gesproken, het type van een functie. In een variabele van een delegate-type kun je dus een functie opslaan, en die later aanroepen. In een parameter van een delegate-type kun je een functie meegeven aan een andere functie, en die vanuit de aangeroepen functie dan aanroepen. Op deze manier
A.6 Type-declaraties
13
type declaratie public
protected
private
internal
abstract
sealed
static
partial
class partial
naam
<
naam
>
,
struct :
interface
type ,
constraints member
{
enum
naam
:
}
type naam
{
}
, delegate
type
naam
<
;
naam
;
>
, (
parameters
)
;
Figuur 3: Syntax van type-declaraties
kun je dus hogere-ordefuncties maken, bijvoorbeeld een sorteerfunctie die het sorteercriterium als parameter meekrijgt. De declaratie van een delegate-type ziet er eigenlijk gewoon hetzelfde uit als een methode-header, met de naam van het nieuwe type op de plaats van de methode-naam. Bijvoorbeeld: delegate int NumeriekeFunctie(int x);
Na deze typedeclaratie kun je een variabele van dit type declareren: NumeriekeFunctie f;
en daar als waarde een functie met de juiste signatuur aan toekennen: f = kwadraat;
Het idee dat een delegate-variabele simpelweg een functiewaarde bevat is echter iets te na¨ıef. Het ligt op twee punten subtieler: • De functie kan ook een methode zijn, dat is een functie die met een bepaald object onderhanden wordt aangeroepen. Dat object wordt ook in een delegate-variabele opgeslagen. • Delegates zijn wat genoemd wordt multicast: er kan meer dan ´e´en functie in worden opgeslagen (steeds met bijbehorende object-context). Wordt een delegate aangeroepen, dan worden alle functies die zijn opgeslagen aangeroepen. Het multicast-principe is vooral handig voor eventhandlers. Bijvoorbeeld, de Paint-property van een Form heeft een delegate als type. Bij functies met een resultaat is de multicast-mogelijkheid niet erg zinvol; weliswaar worden alle functies aangeroepen, maar alleen het resultaat van de laatste aanroep wordt opgeleverd.
14
A.7
C# voor Java-kenners
Members van een klasse
Net als in Java bestaat een klasse uit members. Net als in Java kunnen dat variabele-declaraties zijn (die de opbouw van het object beschrijven), methode-definities (die beschrijven wat je met het object kunt doen), en constructor-methodes (die beschrijven hoe een object ge¨ınitialiseerd wordt). Een nieuw soort member in C# is de property. Verder zijn in C# officieel ook events, indexers, en operator-definities aparte soorten members, maar dat zijn in feite variaties op andere soorten. In figuur 4 is de syntax van de diverse soorten members aangegeven. Voor het gemak geven we geen apart diagram voor de members van een interface. In een interface zijn geen velden en constructors toegestaan, en hebben de methoden geen body. Deze restrictie is in het diagram niet aangegeven. Velden en methodes De syntax van velden (member-variabelen) en methodes is bijna hetzelfde als in Java. Het enige verschil is dat in de header van een methode niet expliciet wordt aangegeven of de methode exceptions opwerpt. Bij de constructor-methode is het enige verschil de manier waarop de constructormethode van de superklasse moet worden aangeroepen (dat is nodig als die parameters heeft). In Java moest dat door de aanroep van super als eerste opdracht in de body; in C# is er speciale syntax voor de aanroep van base, voorafgaand aan de body. Ook kan een constructor desgewenst een collegaconstructor aanroepen. Properties Vergeleken met Java is het belangrijkste nieuwe soort member de property. Voor de gebruiker lijkt een property op een publieke member-variabele, maar de implementatie is anders. Bij de declaratie van een property schrijf je een body erbij, die op zijn beurt bestaat uit twee parameterloze minimethodes, met de namen get en set. De mini-methode get beschrijft wat er moet gebeuren als de waarde van de property wordt opgevraagd; de mini-methode set beschrijft wat er moet gebeuren als de waarde van de property wordt aangepast met een toekenning. Bij de set mini-methode mag in de body het keyword value worden gebruikt, dat staat voor de nieuw verkregen waarde. (Alleen in de context van een property-body is value een keyword; in andere situaties kan het gewoon als naam worden gebruikt). Als de set mini-methode ontbreekt, onstaat er een read-only property. Een voorbeeld daarvan is de property Length van een string. Events Het keyword event mag op twee plaatsen staan: voor een property, en voor een veld. Bij een event-property moet je twee andere mini-methodes defini¨eren: add en remove. Het type moet een delegate-type zijn. Deze twee mini-methodes worden aangeroepen via de operatoren += en -=. Als alternatief kun je een event-veld maken. Er wordt dan achter de schermen een private veld met het delegate-type gemaakt, en de mini-methodes add en remove worden automatisch gegenereerd. Operator-definitie Een variant op de methode-definitie is de operator-definitie. Daarmee kun je operatoren zoals + en * defini¨eren voor nieuwe types. Bijvoorbeeld, als je een type Point hebt, kun je daar ee optelling op defini¨eren: static Point operator + (Point a, Point b) { return new Point(a.x+b.x, a.y+b.y); }
In Java is dat niet mogelijk. In C++ is het ook mogelijk om operatoren te overloaden. Daar gaan ze zelfs nog een stapje verder en staan overloading toe van functe-aanroep (), toekenning, en new. In C# mag dat niet. Ook mag je in C# de logische operatoren && en || niet overloaden. De samengestelde toekennings-operatoren, zoals += en *= mag je niet overloaden, maar deze operatoren zullen wel een eventuele overloaded versie van + of * aanroepen. Indexer Er is een aparte syntax om de notatie voor array-indicering to overloaden voor nieuwe types. De body is hetzelfde als die van een property: er zijn mini-methodes get en/of set om een waarde op te vragen of te veranderen. Dit mechanisme wordt in de library onder andere gebruikt om de array-indicering mogelijk te maken op strings, althans voor de get-situatie.
A.7 Members van een klasse
15
member type-declaratie public
protected
new
static
private
internal
sealed
override
abstract
virtual
extern
veld
const
initialisatie
=
type
naam
;
event indexer
property
, event
get
type
naam
{
set
;
remove
blok
}
add
type
this [ parameters ]
constructor
methode
void
type partial
naam operator
base this
naam
>
,
op
naam :
<
(
parameters
)
(
parameters
)
(
expressie
)
;
blok
,
Figuur 4: Syntax van members van een klasse
16
A.8
C# voor Java-kenners
Types en declaraties
Waarde-types en verwijzings-types In figuur 5 is de syntax van types in C# aangegeven. Net als in Java zijn er standaardtypes voor allerlei vormen van numerieke types. Variabelen van deze types worden, net als bool en char, als waarde opgeslagen. Dat geldt ook voor variabelen die een struct-object opslaan (in Java bestaan er geen struct-objecten). Variabelen die een klasse-object opslaan doen dat, net als in Java, in de vorm van een verwijzing. De twee toch wel speciale klassen String en Object zijn in C# ook beschikbaar via de keywords string en object (met een kleine letter). Het blijven niettemin verwijzings-typen! Arrays Er zijn in C# twee manieren om meer-dimensionale arrays te maken. Je kunt, net als in Java, meerdere paren vierkante haken achter het type schrijven, bijvoorbeeld int[][]. Maar je kunt ook komma’s tussen de haken schrijven: int[,]. Er is verschil tussen deze twee notaties: bij de Java-notatie kunnen de rijen van de array verschillende lengte hebben, bij de komma-notatie zijn de rijen van de array altijd allemaal even lang. Numerieke types Voor gehele getallen zijn er vier formaten types (1 byte, 2 byte, 4 bytes of 8 bytes), elk in een signed en een unsigned variant. Voor niet-gehele getallen is er naast de welbekende float (4 bytes) en double (8 bytes) ook nog decimal. Dat type is bedoeld voor financi¨ele berekeningen: de waarde wordt decimaal opgeslagen en er ontstaan dus geen afrondfouten bij de conversie van en naar binaire codering. Ook wordt er voor grote getallen nooit overgeschakeld op de E-notatie, en raakt er dus nooit een cent kwijt. Dat kost wel 16 bytes en extra rekentijd. De semantiek van numerieke constanten is een beetje anders dan in Java. In Java is een constante, zoals 17, altijd een int. Daarom is er in Java een cast nodig bij een toekenning als byte b=(byte)17;. In C# is een constante altijd van het kleinste type waar het nog in past, dus 17 is een byte, en er is geen cast nodig in byte b=17;. Declaraties Types worden uiteraard gebruikt in variabele-declaraties, zoals int x;. Net als in Java mag er bij de declaratie meteen al een initialisatie worden gedaan, zoals int x=5;. Net als in Java kun je aangeven dat een variabele niet meer mag veranderen, maar in C# wordt hiervoor het keyword const gebruikt in plaats van final. Uiteraard is in dit geval initialisatie verplicht (dit is niet in het syntax-diagram aangegeven). Anders dan in Java kun je in C# het type vervangen door het woord var. Ook in dit geval is initialisatie verplicht, omdat het type dan automatisch wordt afgeleid uit het type van de expressie. Variabelen blijven dus wel degelijk getypeerd, alleen hoef je het type niet meer zelf te bedenken. Je zou dus kunnen schrijven var x=5;, en dan krijgt x automatisch het type byte. Het is niet zo’n goede gewoonte om het type niet expliciet op te schrijven. De reden dat deze mogelijkheid bestaat, is dat C# een speciale expressie-notatie heeft waarmee getypeerde databasequeries geconstrueerd kunnen worden (bekend onder de naam LINQ). De types die daarmee gemoeid gaan zijn zo ingewikkeld dat het in dat geval wel gepermitteerd is om var te schrijven, in plaats van het volledige type.
A.9
Opdrachten
De syntax van opdrachten lijkt zo sterk op die van Java dat we het syntax-diagram hier niet helemaal weergeven. Net als in Java zijn er toekennings-opdrachten, if-, for-, while-, do-while-, switch-case-, try-catch-finally-, throw-, break-, continue-, en return-opdrachten. Een verschil is er in de syntax van de for-opdracht die gebruik maakt van een iterator. In Java gebruik je hiervoor een variant van de for-opdracht, bijvoorbeeld: for (String s : woorden) this.print(s);
In C# is er een aparte foreach-in constructie voor deze situatie: foreach (String s in woorden) this.Print(s);
Een nieuw soort opdracht in C# is de using-opdracht. Let op, dit is iets heel anders dan de usingdirective bovenaan een programma: er is hier sprake van creatief hergebruik van keywords. De
A.10 Expressies
17
type < struct
>
,
naam waarde
type
sbyte
byte
float
bool
short
ushort
double
char
int
uint
decimal
long
ulong
signed
unsigned integer numeriek
real
verwijzing
string object
type
>
,
naam
[
array
< class/itf
] ,
typevar
naam declaratie const
initialisatie
=
type var
naam
; ,
Figuur 5: Syntax van types en variabele-declaraties
syntax is using(declaratie)opdracht, de semantiek is dat de declaratie in de opdracht gebruikt mag worden, maar dat het object daarna direct wordt vrijgegeven, waarbij de interface IDisposable is betrokken. Dat kan van belang zijn om memory-leaks en locks op files te voorkomen. Synchronisatie van parallelle algoritmen gebeurt in C# anders dan in Java. In plaats van de synchronized opdracht in Java is er een lock opdracht met een andere semantiek. Verder is er in C# een yield-opdracht om lazy iteraties te implementeren, en een checked/unchecked modifier om de semantiek van exceptions in een blok te be¨ınvloeden.
A.10
Expressies
Ook de syntax van expressies lijkt zo sterk op die van Java dat we het syntax-diagram hier niet helemaal weergeven. Net als in Java zijn er (numerieke en string-) constanten, variabelen, operatorexpressies met prefix, infix, of postfix-operatoren, de conditionele ?: constructie, groepering met haakjes, type-casts, constructie van objecten met new, aanroep van methoden, indicering van arrays, en selectie van velden uit objecten. Nieuw is het opvragen van een property van een object, maar dat ziet er syntactisch hetzelfde uit als het opvragen van een public veld.
C# voor Java-kenners
lambda
18
(
parameters
)
=>
expressie
naam
blok
Figuur 6: Syntax van lambda-expressies
Lambda-expressies Echt nieuw is de zogeheten lambda-notatie. Dit is een notatie die afkomstig is uit het functioneleprogrammeerparadigma, die wordt gebruikt om waarden van een delegate-type op te schrijven. Er zijn een paar variaties van de lambda-notatie, maar in alle gevallen staat er een => in het midden die de parameters van het resultaat scheidt. Aan de linkerkant van het pijltje staan parameters, aan de rechterkant een expressie. Bijvoorbeeld: (int x) => x*x
is een manier om de kwadraat-functie op te schrijven. Als er maar ´e´en parameter is waarvan het type uit de context kan worden afgeleid, hoef je de haakjes en het type niet op te schrijven. Voor Haskell-programmeurs wordt het nu bekend terrein, met dien verstande dat de lambda niet eens meer wordt opgeschreven: x => x*x
Als er voorafgaand aan het uitrekenen van de expressie nog opdrachten nodig zijn, mag je aan de rechterkant van het pijltje ook een blok schrijven: (int x) => { int a=x+1; return a*a; }