C# voor Java

advertisement
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 één 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 één 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ëren. 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 één functie-met-context; daarom staat
er in bovengenoemde opdracht niet = maar +=. Onze functie tekenScherm met zijn context this
wordt daarmee één 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ëren.
In C# is het, vooral als je meer dan één 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, één 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ëren, 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 èn 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ënter 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 één 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ërarchie. Net als in Java kan een
klasse maar van één 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<A>, die je later kunt instantiëren
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 één 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ëren: 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ëren voor nieuwe types. Bijvoorbeeld, als je een type Point hebt, kun je daar ee optelling
op definiëren:
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ële 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 één 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; }
Download