Uitwerking Derde deeltentamen Imperatief programmeren - versie 1 Vrijdag 6 november 2015, 11.00-13.00 uur • Schrijf op elk ingeleverd blad je naam. Schrijf op het eerste blad ook je studentnummer en het aantal ingeleverde bladen. • De lijst met standaardfuncties na afloop graag weer inleveren. De antwoorden komen binnenkort op de website. • Opgave 1 t/m 10 zijn meerkeuzevragen, die meetellen voor 10 × 4 = 40 punten. Opgave 11 en 12 zijn programmeervragen, die meetellen voor 20 en 40 punten.
Meerkeuzevragen: de letter van het goede antwoord volstaat. Belangrijk: dit is versie 1 van het tentamen, vermeld dat boven je antwoorden. 1. De methode Main mag gedefinieerd worden met een parameter. Deze parameter bevat: (a) De regels die door de gebruiker worden ingetikt (b) De files die door het programma gelezen moeten worden (c) • De woorden die bij het opstarten achter de programmanaam zijn ingetikt (d) De libraries die in het programma gebruikt worden Toelichting op het antwoord: (a) is de strings die je krijgt door aanroep van ReadLine. (b) lijkt aardig, omdat de ingetikte woorden vaak worden gebruikt als de namen van files die gelezen moeten worden, maar dat is niet altijd het geval: de woorden kunnen ook anders gebruikt worden. Bovendien zijn het dan de filenamen en niet de files zelf. (d) deze libraries staan in het programma achter using, maar dat heeft verder weinig met Main te maken. 2. Een lambda-expressie is (a) • Een manier om een naamloze methode aan te duiden (b) Een manier om een event-handler te registreren (c) Een manier om een methode te herdefini¨eren in een subklasse (d) Een manier om de methode zoals die in de superklasse was gedefinieerd aan te duiden Toelichting op het antwoord: (b) is niet goed, want event-handlers registreer je met +=. Weliswaar staat rechts daarvan vaak een lambda-expressie, maar de lambda-expressie als zodanig registreert niets. Methodes herdefini¨eren doe je met override, aanduiden van de superklasse gaat met base, maar dat heeft allebei niets met lambda-expressies te maken. 3. Wat is het belangrijkste voordeel van een virtuele methode boven een abstracte methode? (a) Je krijgt een waarschuwing als je hem vergeet te overriden in een subklasse (b) Je hoeft er nog geen implementatie van te geven (c) • Je kunt alvast een default-gedrag vastleggen (d) Je kunt hem niet per ongeluk aanroepen terwijl dat nog geen zin heeft Toelichting op het antwoord: (a) en (b) zijn verkeerdom: het zijn voordelen van abstracte methoden boven virtuele methoden. (d) is onzin: zowel abstracte als virtuele methoden mag je juist wel aanroepen, zelfs als het nog geen zin heeft. 1
4. Een abstracte klasse (a) • Kan abstracte methoden bevatten (b) Moet ook abstracte methoden bevatten (c) Mag alleen maar abstracte methoden bevatten (d) Heeft geen body Toelichting op het antwoord: Zodra er een abstracte methode in een klasse staat, moet die klasse ook abstract zijn. Maar je mag een klasse ook zo maar abstract maken, dus (b) is niet goed. In de body kunnen er abstracte methoden en gewone methoden door elkaar heen staan, dus (c) is niet goed. Het ontbreken van een body geldt bij abstracte methoden, niet bij abstracte klassen, dus (d) is niet goed. 5. Welk van onderstaande uitspraken is niet waar? (a) UTF8 kan dezelfde tekens opslaan als Unicode (b) UTF8 kost soms meer ruimte dan Unicode (c) UTF8 kost soms minder ruimte dan Unicode (d) • UTF8 codeert tekens met code 0 t/m 255 hetzelfde als Latin1 Toelichting op het antwoord: UTF8 is een coderingsmethode, maar kan net als Unicode alle 65536 verschillende tekens opslaan. Voor Chinese tekens kost dat 3 bytes per karakter, terwijl Unicode er maar 2 nodig heeft. Voor westerse tekens kost dat 1 byte per karakter, terwijl Unicode er altijd 2 nodig heeft. Dus (a), (b) en (c) zijn correcte uitspraken. Maar (d) is niet waar (en dus het goede antwoord). Wel is het zo dat UTF8 de tekens met code 0 t/m 127 hetzelfde codeert als Ascii, maar code 128 t/m 255 krijgen nou juist codes van 2 bytes, terwijl Latin1 dat met 1 byte doet. 6. Welke declaratie is correct? (a) IList<string> x = new IList<string>(); (b) IList<string> x = new LinkedList<string>(); (c) List<string> x = new IList<string>(); (d) • ICollection<string> x = new List<string>(); Toelichting op het antwoord: De naam van deen interface mag nooit achter new staan, dus (a) en (c) vallen af. Een LinkedList is gek genoeg geen implementatie van IList (zie het klasse-diagram), dus (b) is fout. Bijna alles is wel een implementatie van ICollection, dus ook List. 7. In een MDI-programma. . . (a) wordt het menu van het container-window samengevoegd met dat van het actieve childwindow en getoond in het child-window (b) • wordt het menu van het actieve child-window samengevoegd met dat van het containerwindow en getoond in het container-window (c) worden de menu’s van de child-windows gecombineerd weergegeven (d) vervangt het menu van het actieve child-window dat van het container-window 8. Het keyword value kan worden gebruikt binnen de definitie van een property (a) in de set-minimethode, en heeft dan de waarde van de parameter van de property 2
(b) • in de set-minimethode, en heeft dan de waarde van de rechterkant van een toekenning aan de property (c) in de get-minimethode, en heeft dan de waarde van het resultaat van de property (d) in de get-minimethode, en heeft dan de waarde van de private variabele die door de property wordt afgeschermd Toelichting op het antwoord: De get-minimethode wordt aangeroepen als je de property in een expressie gebruikt. De set-minimethode wordt aangeroepen als je de property aan de linkerkant van een toekenningsopdracht gebruikt. Het keyword value heeft dan de waarde van de rechterkant van die toekenning. 9. Na de declaratie int[,,,] a = new int[1,2,3,4]; heeft de array a (a) 4 variabelen (b) 10 variabelen (c) • 24 variabelen (d) nog geen variabelen Toelichting op het antwoord: Het gaat om een 4-dimensionale array, met 1×2×3×4 = 24 elementen. Je moet de lengtes van elke dimensie vermenigvudigen, niet optellen, dus 10 is fout. Vier elementen heeft een array waarvan je de elementen opsomt tussen accolades, maar die is ´e´en-dimensionaal: int[] a = new int{1,2,3,4};. Een array die je niet initialiseert int[] a; heeft nog geen variabelen. 10. Het verschil tussen een list en een collection is (a) • In een list staan de elementen in een bepaalde volgorde (b) In een list kun je het aantal elementen bepalen (c) In een list kun je controleren of een bepaalde waarde aanwezig is (d) In een list zitten geen dubbele elementen Toelichting op het antwoord: De interface ICollection specificeert een property Count en een methode Contains, dus (b) en (c) zijn niet goed. Geen dubbele elementen heb je in een Set. 11. (telt voor 20%) Bij het spel ‘Reversi’ leggen twee spelers om de beurt een gekleurde steen op een veld van een rechthoekig speelbord. Een steen mag alleen maar worden neergelegd op een veld als het veld nog leeg is, en met deze zet een rij van een of meer stenen van de andere kleur wordt ingesloten tussen de nieuwe steen en een al op het bord liggende steen van de eigen kleur. De ingesloten stenen kunnen in acht mogelijke richtingen naast de nieuwe steen liggen: horizontaal, verticaal of diagonaal. Stenen insluiten in meerdere richtingen mag ook. In de figuur is voor twee voorbeelden met open cirkels aangegeven op welke velden de speler met de lichte stenen mag zetten. 3
Als gevolg van een zet veranderen alle ingesloten stenen van kleur. In een programma wordt de situatie opgeslagen in een twee-dimensionale array: int[,] bord = new int[6,6];
Lege velden zijn gecodeerd met 0, gevulde velden met 1 of −1 voor de twee kleuren. De opgave: Schrijf een methode bool mag (int kleur, int x, int y)
die controleert of speler kleur een steen op veld (x, y) mag zetten. (De zet wordt dus nog niet uitgevoerd!). Hint: het is toegestaan (en handig) om een extra methode te schrijven die het insluiten in ´e´en van de 8 richtingen controleert. Antwoord: bool mag (int kleur, int x, int y) { if (bord[x,y]!=0) for (int dx=-1; dx<=1; dx++) for (int dy=-1; dy<=1; dy++) if (dx!=0 || dy!=0) { if (test(kleur, x, y, dx, dy); return true; } return false; } bool test (int kleur, int x, int y, int dx, int dy) { for (int t=1; ; t++) { int ix = x+t*dx; int iy = y+y*dy; if (ix<0 || ix>=6 || iy<0 || iy>=6) return false; int s = bord[ix, iy]; if (s==0) return false; if (s==kleur) return t>1; } return false; }
12. Met het programma in deze opgave kan de gebruiker een blokkenwereld tekenen. Op een groot gedeelte van het scherm is een ‘wereld’ zichtbaar waarin de gebruiker kan klikken. Ernaast staan een schuifregelaar, twee buttons, en twee radio-buttons. 4
Met de radio-buttons kan de gebruiker een ‘tekenmodus’ kiezen: ‘tekenen’ of ‘wissen’. Als de gebruiker in ‘tekenmodus’ op de wereld klikt, ontstaat er gecentreerd op dat punt een vierkant. De diameter van het vierkant is bepaald door de stand van de schuifregelaar op dat moment. De kleur van het vierkant is een grijstint, die afhangt van de positie op het scherm: helemaal links zijn de vierkanten zwart, rechts zijn ze wit; hoe verder naar rechts, hoe lichter de grijstint. De vierkanten kunnen elkaar overlappen. Als de gebruiker in ‘wissenmodus’ op een vierkant in de wereld klikt, verdwijnt dat vierkant. Bij overlappende vierkanten verdwijnt de bovenste. Wordt er geen vierkant geraakt, dan gebeurt er niets. In de bijlage aan het eind van dit tentamen staat een deel van het programma gegeven. Het bestaat uit vier klassen: Program, Scherm, Wereld, en Blok. Een aantal onderdelen moet nog worden ingevuld. (a) Schrijf de klasse Blok, zodat in een object van dit type alle relevante gegevens voor ´e´en blok worden opgeslagen. Zet daarin: • de benodigde member-variabelen • een constructormethode waarmee de members een waarde kunnen krijgen • de methode ToString, die de gegevens van een blok omzet naar string-formaat • een tweede constructormethode, die gegeven zo’n string het oorspronkelijke blok weer reconstrueert • een methode waarmee je kunt testen of het blok een bepaald punt bevat Antwoord: public class Blok { public Point pos; public int diam; public Blok(Point p, int d) { pos = p; diam = d; } public Blok(string s) { string[] ss = s.Split(); pos = new Point(int.Parse(ss[0]), int.Parse(ss[1])); diam = int.Parse(ss[2]); } public override string ToString() { return pos.X + " " + pos.Y + " " + diam; } public bool Bevat(Point p) { return p.X >= pos.X-diam/2 && p.X <= pos.X + diam/2 && p.Y >= pos.Y-diam/2 && p.Y <= pos.Y + diam/2; } }
(b) Schrijf de klasse Wereld. Zet daarin: • de benodigde member-variabelen • een constructormethode, die aangeroepen kan worden zoals dat in klasse Scherm gebeurt. 5
Hint: let op, de methode heeft een parameter. Dat is niet voor niets! • overige methoden die nodig zijn om ervoor te zorgen dat de tekenmodus gewisseld kan worden, en er inderdaad blokken worden getekend, respectievelijk gewist. Antwoord: public class Wereld : UserControl { List
blokken = new List(); Scherm hetScherm; bool tekenMode = true;
public Wereld(Scherm f) { this.Paint += teken; this.MouseClick += klik; hetScherm = f; } public void teken(object o, PaintEventArgs pea) { foreach (Blok b in blokken) { int k = b.pos.X / 2; Color col = Color.FromArgb(k, k, k); Brush br = new SolidBrush(col); pea.Graphics.FillRectangle(br, b.pos.X - b.diam / 2, b.pos.Y - b.diam / 2, b.diam, } } public void klik(object o, MouseEventArgs mea) { if (this.tekenMode) { Blok b = new Blok(mea.Location, hetScherm.schuif.Value); blokken.Add(b); } else { Blok hit = null; foreach (Blok b in blokken) if (b.Bevat(mea.Location)) hit = b; if (hit != null) blokken.Remove(hit); } this.Invalidate(); } public void modeChange(object o, EventArgs ea) { RadioButton b = (RadioButton)o; this.tekenMode = b.Text == "Tekenen"; } }
(c) Met de button ‘opslaan’ kan de hele situatie worden opgeslagen in een tekstbestand. Met de button ‘inlezen’ verdwijnt de huidige situatie, en wordt die vervangen door de eerder opgeslagen situatie. De te gebruiken filenaam staat gedeclareerd in de klasse Scherm. Schrijf de twee benodigde methoden, en geef aan in welke klasse die moeten staan. Antwoord: partial class Wereld { public void opslaan(object o, EventArgs ea)
6
{ StreamWriter w = new StreamWriter(hetScherm.filenaam); foreach (Blok b in blokken) w.WriteLine(b.ToString()); w.Close(); } public void inlezen(object o, EventArgs ea) { blokken.Clear(); StreamReader r = new StreamReader(hetScherm.filenaam); string regel; while ((regel = r.ReadLine()) != null) blokken.Add(new Blok(regel)); r.Close(); this.Invalidate(); } }
7
// Bijlage bij opgave 12 public class Program { static void Main() { Application.Run(new Scherm()); } } public class Scherm : Form { public TrackBar schuif; public string filenaam = "test.txt"; public Scherm() { this.Size = new Size(700, 400); Wereld w = new Wereld(this); w.Location = new Point(20, 20); this.Controls.Add(w);
this.Text = "BlokkenWereld";
w.Size = new Size(510, 300);
schuif = new TrackBar(); schuif.Location = new Point(550, 20); schuif.Orientation = Orientation.Vertical; schuif.Maximum = 100; schuif.Value = 20; this.Controls.Add(schuif);
w.BackColor = Color.White;
schuif.Size = new Size(40, 200); schuif.BackColor = Color.DarkGray; schuif.TickFrequency = 10;
Button b1 = new Button(); b1.Location = new Point(620, 20); this.Controls.Add(b1);
b1.Size = new Size(60, 24); b1.Text = "Opslaan";
Button b2 = new Button(); b2.Location = new Point(620, 50); this.Controls.Add(b2);
b2.Size = new Size(60, 24);
b2.Text = "Inlezen";
RadioButton r1 = new RadioButton(); r1.Location = new Point(620, 100); r1.Size = new Size(100, 24); r1.Text = "Tekenen"; this.Controls.Add(r1); r1.Select(); RadioButton r2 = new RadioButton(); r2.Location = new Point(620, 130); r2.Size = new Size(100, 24); r2.Text = "Wissen"; this.Controls.Add(r2); b1.Click b2.Click r1.Click r2.Click
+= += += +=
w.opslaan; w.inlezen; w.modeChange; w.modeChange;
} } public class Wereld : UserControl { // TODO } public class Blok { // TODO }
8