4. Klassen, structuren, interfaces
4.1. Het object aan de hand van het voorbeeld
4.1.1. Algemeen
We gaan nu aan de hand van een voorbeeld in op objectgeoriënteerd programmeren. Een object is een entiteit die gegevens bevat die de toestand ervan bepalen (deze worden velden, attributen, ... genoemd) en functies (deze worden methoden genoemd). Een object wordt aangemaakt volgens een model dat een klasse wordt genoemd:
public class C1{
Type1 p1; // veld p1
Type2 p2; // veld p2
…
Type3 m3(…){ // methode m3
…
}
Type4 m4(…){ // methode m4
…
}
…
}
Op basis van de vorige klasse C1 kunnen we talrijke objecten aanmaken, zoals O1, O2,… Ze zullen allemaal de velden p1, p2, … en de methoden m3, m4, … hebben. Maar ze zullen verschillende waarden hebben voor hun velden pi, waardoor ze elk een eigen toestand hebben. Als o1 een object is van het type C1, dan verwijst o1.p1 naar de eigenschap p1 van o1 en o1.m1 naar de methode m1 van O1.
Laten we een eerste objectmodel bekijken: de klasse Personne.
4.1.2. Het C#-project aanmaken
In de voorgaande voorbeelden hadden we in een project slechts één bronbestand: Program.cs. Vanaf nu kunnen we meerdere bronbestanden in één project hebben. We laten zien hoe je dit doet.
![]() |
Maak in [1] een nieuw project aan. Kies in [2] een Console-toepassing. Laat in [3] de standaardwaarde staan. Bevestig in [4]. In [5] staat het gegenereerde project. De inhoud van Program.cs is als volgt:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace ConsoleApplication1 {
class Program {
static void Main(string[] args) {
}
}
}
Laten we het aangemaakte project opslaan:
![]() |
In [1], de optie om op te slaan. In [2], geef de map aan waarin het project moet worden opgeslagen. In [3], geef het project een naam. Geef in [5] aan dat u een oplossing wilt aanmaken. Een oplossing is een verzameling projecten. Geef in [4] de naam van de oplossing op. Bevestig in [6] het opslaan.
![]() |
In [1]: het opgeslagen project. In [2]: voeg een nieuw element toe aan het project.
![]() |
In [1], geef aan dat u een klasse wilt toevoegen. In [2], de naam van de klasse. In [3], bevestig de gegevens. In [4] heeft het project [01] een nieuw bronbestand Personne.cs:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace ConsoleApplication1 {
class Personne {
}
}
We wijzigen de naamruimte van elk van de bronbestanden in Chap2 en verwijderen de import van overbodige naamruimten:
using System;
namespace Chap2 {
class Personne {
}
}
using System;
namespace Chap2 {
class Program {
static void Main(string[] args) {
}
}
}
4.1.3. Definitie van de klasse Personne
De definitie van de klasse Personne in het bronbestand [Personne.cs] ziet er als volgt uit:
using System;
namespace Chap2 {
public class Personne {
// attributen
private string prenom;
private string nom;
private int age;
// methode
public void Initialise(string P, string N, int age) {
this.prenom = P;
this.nom = N;
this.age = age;
}
// methode
public void Identifie() {
Console.WriteLine("[{0}, {1}, {2}]", prenom, nom, age);
}
}
}
Hier hebben we de definitie van een klasse, dus van een gegevenstype. Wanneer we variabelen van dit type aanmaken, noemen we deze objecten of instanties van de klasse. Een klasse is dus een mal op basis waarvan objecten worden geconstrueerd.
De leden of velden van een klasse kunnen gegevens (attributen), methoden (functies) of eigenschappen zijn. Eigenschappen zijn speciale methoden die dienen om de waarde van attributen van het object te achterhalen of vast te leggen. Deze velden kunnen vergezeld gaan van een van de volgende drie sleutelwoorden:
Een privéveld (private) is alleen toegankelijk via de interne methoden van de klasse | |
Een openbaar (public) veld is toegankelijk via elke methode, ongeacht of deze al dan niet binnen de klasse is gedefinieerd | |
Een beschermd veld (protected) is alleen toegankelijk via de interne methoden van de klasse of van een afgeleid object (zie later het concept van overerving). |
Over het algemeen worden de gegevens van een klasse als privé gedeclareerd, terwijl de methoden en eigenschappen als openbaar worden gedeclareerd. Dit betekent dat de gebruiker van een object (de programmeur)
- geen directe toegang heeft tot de privégegevens van het object
- wel gebruik kan maken van de openbare methoden van het object, en met name van die methoden die toegang geven tot de privégegevens ervan.
De syntaxis voor het declareren van een C-klasse is als volgt:
public class C{
private donnée ou méthode ou propriété privée;
public donnée ou méthode ou propriété publique;
protected donnée ou méthode ou propriété protégée;
}
De volgorde waarin de attributen private, protected en public worden gedeclareerd, is willekeurig.
4.1.4. De methode Initialise
Laten we teruggaan naar onze klasse Personne, die als volgt is gedeclareerd:
using System;
namespace Chap2 {
public class Personne {
// attributen
private string prenom;
private string nom;
private int age;
// methode
public void Initialise(string p, string n, int age) {
this.prenom = p;
this.nom = n;
this.age = age;
}
// methode
public void Identifie() {
Console.WriteLine("[{0}, {1}, {2}]", prenom, nom, age);
}
}
}
Wat is de functie van de methode Initialise? Omdat nom, prenom en age privégegevens zijn van de klasse Personne, zijn de instructies:
zijn ongeldig. We moeten een object van het type Personne initialiseren via een openbare methode. Dat is de taak van de methode Initialise. We schrijven dan:
De notatie p1.Initialise is toegestaan, omdat Initialise openbaar toegankelijk is.
4.1.5. De operator new
De reeks instructies
is onjuist. De instructie
definieert p1 als een verwijzing naar een object van het type Personne. Dit object bestaat nog niet en daarom is p1 niet geïnitialiseerd. Het is alsof men het volgende zou schrijven:
waarbij we met het sleutelwoord null expliciet aangeven dat de variabele p1 nog niet naar een object verwijst. Wanneer we vervolgens schrijven
roep je de methode Initialise aan van het object waarnaar p1 verwijst. Dit object bestaat echter nog niet en de compiler zal de fout melden. Om ervoor te zorgen dat p1 naar een object verwijst, moet je het volgende schrijven:
Hierdoor wordt een nog niet geïnitialiseerd object van het type Personne aangemaakt: de attributen nom en prenom, die verwijzingen zijn naar objecten van het type String, krijgen de waarde null, en age de waarde 0. Er vindt dus een standaardinitialisatie plaats. Nu p1 naar een object verwijst, is de initialisatie-instructie voor dit object
geldig.
4.1.6. Het sleutelwoord this
Laten we de code van de methode initialise eens bekijken:
public void Initialise(string p, string n, int age) {
this.prenom = p;
this.nom = n;
this.age = age;
}
De instructie this.prenom=p betekent dat het attribuut prenom van het huidige object (this) de waarde p krijgt. Het sleutelwoord this verwijst naar het huidige object: het object waarin de uitgevoerde methode zich bevindt. Hoe weten we dat? Laten we eens kijken hoe het object waarnaar p1 verwijst, wordt geïnitialiseerd in het aanroepende programma:
Het is de methode Initialise van het object p1 die wordt aangeroepen. Wanneer in deze methode naar het object this wordt verwezen, wordt in feite naar het object p1 verwezen. De methode Initialise had ook als volgt geschreven kunnen worden:
public void Initialise(string p, string n, int age) {
prenom = p;
nom = n;
this.age = age;
}
Wanneer een methode van een object verwijst naar een attribuut A van dat object, wordt impliciet de notatie this.A gebruikt. Deze moet expliciet worden gebruikt wanneer er een conflict tussen identificatoren bestaat. Dit is het geval bij de instructie:
this.age=age;
waarbij age verwijst naar zowel een attribuut van het huidige object als naar de parameter age die door de methode wordt ontvangen. De dubbelzinnigheid moet dan worden opgeheven door het attribuut age aan te duiden als this.age.
4.1.7. Een testprogramma
Hier volgt een kort testprogramma. Dit staat geschreven in het bronbestand [Program.cs]:
using System;
namespace Chap2 {
class P01 {
static void Main() {
Personne p1 = new Personne();
p1.Initialise("Jean", "Dupont", 30);
p1.Identifie();
}
}
}
Voordat u het project [01] uitvoert, kan het nodig zijn om het uit te voeren bronbestand te specificeren:
![]() |
In de eigenschappen van het project [01] wordt in [1] de uit te voeren klasse aangegeven.
De resultaten van de uitvoering zijn als volgt:
4.1.8. Een andere methode Initialise
Laten we nogmaals de klasse Personne bekijken en de volgende methode eraan toevoegen:
public void Initialise(Personne p) {
prenom = p.prenom;
nom = p.nom;
age = p.age;
}
We hebben nu twee methoden met de naam Initialise: dit is toegestaan zolang ze verschillende parameters accepteren. Dat is hier het geval. De parameter is nu een verwijzing p naar een persoon. De attributen van de persoon p worden vervolgens toegewezen aan het huidige object (this). Merk op dat de methode Initialise directe toegang heeft tot de attributen van het object p, hoewel deze van het type private zijn. Dit geldt altijd: een object o1 van een klasse C heeft altijd toegang tot de attributen van objecten van dezelfde klasse C.
Hier volgt een test van de nieuwe klasse Personne:
using System;
namespace Chap2 {
class Program {
static void Main() {
Personne p1 = new Personne();
p1.Initialise("Jean", "Dupont", 30);
p1.Identifie();
Personne p2 = new Personne();
p2.Initialise(p1);
p2.Identifie();
}
}
}
en de resultaten daarvan:
4.1.9. Constructors van de klasse Personne
Een constructor is een methode die de naam van de klasse draagt en die wordt aangeroepen bij het aanmaken van het object. Deze wordt doorgaans gebruikt om het object te initialiseren. Het is een methode die argumenten kan accepteren, maar geen resultaat retourneert. Het prototype of de definitie ervan wordt niet voorafgegaan door een type (zelfs niet void).
Als een klasse C een constructor heeft die n argumenten argi accepteert, kunnen de declaratie en initialisatie van een object van deze klasse als volgt plaatsvinden:
of
Wanneer een klasse C één of meerdere constructors heeft, moet verplicht één van deze constructors worden gebruikt om een object van deze klasse aan te maken. Als een klasse C geen constructors heeft, beschikt deze over een standaardconstructor, namelijk de constructor zonder parameters: public C(). De attributen van het object worden dan geïnitialiseerd met standaardwaarden. Dit is wat er gebeurde in de voorgaande programma's, waar we het volgende hadden geschreven:
Laten we twee constructors maken voor onze klasse Personne:
using System;
namespace Chap2 {
public class Personne {
// attributen
private string prenom;
private string nom;
private int age;
// constructors
public Personne(String p, String n, int age) {
Initialise(p, n, age);
}
public Personne(Personne P) {
Initialise(P);
}
// methode
public void Initialise(string p, string n, int age) {
...
}
public void Initialise(Personne p) {
...
}
// methode
public void Identifie() {
Console.WriteLine("[{0}, {1}, {2}]", prenom, nom, age);
}
}
}
Onze twee constructors roepen gewoon de eerder besproken methoden Initialise aan. Ter herinnering: wanneer we in een constructor bijvoorbeeld de notatie Initialise(p) tegenkomen, vertaalt de compiler dit naar this.Initialise(p). In de constructor wordt de methode Initialise dus aangeroepen om te werken aan het object waarnaar wordt verwezen door this, dat wil zeggen het huidige object, het object dat op dat moment wordt geconstrueerd.
Hier volgt een kort testprogramma:
using System;
namespace Chap2 {
class Program {
static void Main() {
Personne p1 = new Personne("Jean", "Dupont", 30);
p1.Identifie();
Personne p2 = new Personne(p1);
p2.Identifie();
}
}
}
en de verkregen resultaten:
4.1.10. Objectreferenties
We gebruiken altijd dezelfde klasse Personne. Het testprogramma ziet er als volgt uit:
using System;
namespace Chap2 {
class Program2 {
static void Main() {
// p1
Personne p1 = new Personne("Jean", "Dupont", 30);
Console.Write("p1="); p1.Identifie();
// p2 verwijst naar hetzelfde object als p1
Personne p2 = p1;
Console.Write("p2="); p2.Identifie();
// p3 verwijst naar een object dat een kopie zal zijn van het object waarnaar p1 verwijst
Personne p3 = new Personne(p1);
Console.Write("p3="); p3.Identifie();
// de status van het object waarnaar p1 verwijst, wordt gewijzigd
p1.Initialise("Micheline", "Benoît", 67);
Console.Write("p1="); p1.Identifie();
// aangezien p2 = p1, moet de toestand van het object waarnaar p2 verwijst, zijn gewijzigd
Console.Write("p2="); p2.Identifie();
// aangezien p3 niet naar hetzelfde object verwijst als p1, kan het object waarnaar p3 verwijst niet van status zijn veranderd
Console.Write("p3="); p3.Identifie();
}
}
}
De verkregen resultaten zijn als volgt:
Wanneer we de variabele p1 declareren met
verwijst p1 naar het object Personne("Jean","Dupont",30), maar is niet het object zelf. In C zouden we zeggen dat het een pointer is, c.a.d, naar het adres van het aangemaakte object. Als we vervolgens schrijven:
Dan wordt niet het object Personne("Jean","Dupont",30) gewijzigd, maar verandert de waarde van de verwijzing p1. Het object Personne("Jean","Dupont",30) gaat „verloren” als er geen enkele andere variabele naar verwijst.
Wanneer men schrijft:
wordt de pointer p2 geïnitialiseerd: deze ‘wijst’ naar hetzelfde object (verwijst naar hetzelfde object) als de pointer p1. Als we dus het object wijzigen waarnaar p1 „wijst” (of waarnaar wordt verwezen), wijzigen we ook het object waarnaar p2 verwijst.
Wanneer we schrijven:
wordt er een nieuw object Personne aangemaakt. Naar dit nieuwe object wordt verwezen door p3. Als het object waarnaar p1 verwijst, wordt gewijzigd, heeft dit geen enkele invloed op het object waarnaar p3 verwijst. Dit blijkt uit de verkregen resultaten.
4.1.11. Doorgeven van parameters van het type objectreferentie
In het vorige hoofdstuk hebben we gekeken naar de manieren waarop parameters aan een functie worden doorgegeven wanneer deze een eenvoudig C#-type vertegenwoordigen, weergegeven door een structuur .NET. Laten we eens kijken wat er gebeurt wanneer de parameter een objectreferentie is:
using System;
using System.Text;
namespace Chap1 {
class P12 {
public static void Main() {
// voorbeeld 4
StringBuilder sb0 = new StringBuilder("essai0"), sb1 = new StringBuilder("essai1"), sb2 = new StringBuilder("essai2"), sb3;
Console.WriteLine("Dans fonction appelante avant appel : sb0={0}, sb1={1}, sb2={2}", sb0,sb1, sb2);
ChangeStringBuilder(sb0, sb1, ref sb2, out sb3);
Console.WriteLine("Dans fonction appelante après appel : sb0={0}, sb1={1}, sb2={2}, sb3={3}", sb0, sb1, sb2, sb3);
}
private static void ChangeStringBuilder(StringBuilder sbf0, StringBuilder sbf1, ref StringBuilder sbf2, out StringBuilder sbf3) {
Console.WriteLine("Début fonction appelée : sbf0={0}, sbf1={1}, sbf2={2}", sbf0,sbf1, sbf2);
sbf0.Append("*****");
sbf1 = new StringBuilder("essai1*****");
sbf2 = new StringBuilder("essai2*****");
sbf3 = new StringBuilder("essai3*****");
Console.WriteLine("Fin fonction appelée : sbf0={0}, sbf1={1}, sbf2={2}, sbf3={3}", sbf0, sbf1, sbf2, sbf3);
}
}
}
- regel 8: definieert 3 objecten van het type StringBuilder. Een object van het type StringBuilder bevindt zich naast een object van het type string.. Wanneer we een object van het type string bewerken, krijgen we een nieuw object van het type string terug. Zo zien we in de codereeks:
Regel 1 maakt een object string aan in het geheugen en s is het adres daarvan. In regel 2 maakt s.ToUpperCase() een ander object string aan in het geheugen. Tussen regel 1 en 2 is de waarde van s dus veranderd (het verwijst nu naar het nieuwe object). Met de klasse StringBuilder kan een tekenreeks worden omgezet zonder dat er een tweede object wordt aangemaakt. Dit is het voorbeeld dat hierboven is gegeven:
- regel 8: 4 verwijzingen [sb0, sb1, sb2, sb3] naar objecten van het type StringBuilder
- regel 10: worden doorgegeven aan de methode ChangeStringBuilder met verschillende modi: sb0, sb1 met de standaardmodus, sb2 met het sleutelwoord ref, sb3 met het sleutelwoord out.
- regels 15-22: een methode met de formele parameters [sbf0, sbf1, sbf2, sbf3]. De relaties tussen de formele parameters sbfi en de effectieve parameters sbi zijn als volgt:
- sbf0 en sb0 zijn bij het starten van de methode twee afzonderlijke verwijzingen die naar hetzelfde object verwijzen (overdracht van adressen per waarde)
- hetzelfde geldt voor sbf1 en sb1
- sbf2 en sb2 zijn bij het starten van de methode dezelfde referentie naar hetzelfde object (trefwoord ref)
- sbf3 en sb3 zijn, na uitvoering van de methode, dezelfde verwijzing naar hetzelfde object (sleutelwoord out)
De verkregen resultaten zijn als volgt:
Toelichting:
- sb0 en sbf0 zijn twee afzonderlijke verwijzingen naar hetzelfde object. Dit object is gewijzigd via sbf0 – regel 3. Deze wijziging is te zien via sb0 – regel 4.
- sb1 en sbf1 zijn twee afzonderlijke verwijzingen naar hetzelfde object. De waarde van sbf1 is in de methode gewijzigd en verwijst nu naar een nieuw object – regel 3. Dit heeft geen invloed op de waarde van sb1, dat nog steeds naar hetzelfde object verwijst – regel 4.
- sb2 en sbf2 zijn dezelfde referentie naar hetzelfde object. De waarde van sbf2 wordt in de methode gewijzigd en verwijst nu naar een nieuw object – regel 3. Aangezien sbf2 en sb2 één en dezelfde entiteit zijn, is de waarde van sb2 eveneens gewijzigd en verwijst sb2 naar hetzelfde object als sbf2 – regels 3 en 4.
- Vóór het aanroepen van de methode had sb3 geen waarde. Na de methode krijgt sb3 de waarde van sbf3. We hebben dus twee verwijzingen naar hetzelfde object – regels 3 en 4
4.1.12. Tijdelijke objecten
In een uitdrukking kan expliciet de constructor van een object worden aangeroepen: het object wordt aangemaakt, maar we hebben er geen toegang toe (om het bijvoorbeeld te wijzigen). Dit tijdelijke object wordt aangemaakt om de uitdrukking te kunnen evalueren en wordt daarna vrijgegeven. De geheugenruimte die het in beslag nam, wordt later automatisch vrijgemaakt door een programma dat „garbage collector“ wordt genoemd en dat tot taak heeft de geheugenruimte vrij te maken die wordt ingenomen door objecten waarnaar niet langer wordt verwezen door gegevens in het programma.
Laten we eens kijken naar het volgende nieuwe testprogramma:
using System;
namespace Chap2 {
class Program {
static void Main() {
new Personne(new Personne("Jean", "Dupont", 30)).Identifie();
}
}
}
en laten we de constructors van de klasse Personne aanpassen zodat ze een bericht weergeven:
// constructors
public Personne(String p, String n, int age) {
Console.WriteLine("Constructeur Personne(string, string, int)");
Initialise(p, n, age);
}
public Personne(Personne P) {
Console.Out.WriteLine("Constructeur Personne(Personne)");
Initialise(P);
}
We krijgen de volgende resultaten:
waarin de opeenvolgende constructie van de twee tijdelijke objecten te zien is.
4.1.13. Methoden voor het lezen en schrijven van privé-attributen
We voegen aan de klasse Personne de methoden toe die nodig zijn om de status van de attributen van de objecten te lezen of te wijzigen:
using System;
namespace Chap2 {
public class Personne {
// attributen
private string prenom;
private string nom;
private int age;
// constructors
public Personne(String p, String n, int age) {
Console.WriteLine("Constructeur Personne(string, string, int)");
Initialise(p, n, age);
}
public Personne(Personne p) {
Console.Out.WriteLine("Constructeur Personne(Personne)");
Initialise(p);
}
// methode
public void Initialise(string p, string n, int age) {
this.prenom = p;
this.nom = n;
this.age = age;
}
public void Initialise(Personne p) {
prenom = p.prenom;
nom = p.nom;
age = p.age;
}
// accessors
public String GetPrenom() {
return prenom;
}
public String GetNom() {
return nom;
}
public int GetAge() {
return age;
}
// modificatoren
public void SetPrenom(String P) {
this.prenom = P;
}
public void SetNom(String N) {
this.nom = N;
}
public void SetAge(int age) {
this.age = age;
}
// methode
public void Identifie() {
Console.WriteLine("[{0}, {1}, {2}]", prenom, nom, age);
}
}
}
We testen de nieuwe klasse met het volgende programma:
using System;
namespace Chap2 {
class Program {
static void Main(string[] args) {
Personne p = new Personne("Jean", "Michelin", 34);
Console.Out.WriteLine("p=(" + p.GetPrenom() + "," + p.GetNom() + "," + p.GetAge() + ")");
p.SetAge(56);
Console.Out.WriteLine("p=(" + p.GetPrenom() + "," + p.GetNom() + "," + p.GetAge() + ")");
}
}
}
en we krijgen de volgende resultaten:
4.1.14. De eigenschappen
Er is nog een andere manier om toegang te krijgen tot de attributen van een klasse, namelijk door eigenschappen aan te maken. Hiermee kunnen we privé-attributen bewerken alsof ze openbaar zijn.
Laten we eens kijken naar de volgende klasse Personne, waarin de eerdere accessors en modifiers zijn vervangen door lees- en schrijf-properties:
using System;
namespace Chap2 {
public class Personne {
// attributen
private string prenom;
private string nom;
private int age;
// constructors
public Personne(String p, String n, int age) {
Initialise(p, n, age);
}
public Personne(Personne p) {
Initialise(p);
}
// methode
public void Initialise(string p, string n, int age) {
this.prenom = p;
this.nom = n;
this.age = age;
}
public void Initialise(Personne p) {
prenom = p.prenom;
nom = p.nom;
age = p.age;
}
// eigenschappen
public string Prenom {
get { return prenom; }
set {
// geldige voornaam?
if (value == null || value.Trim().Length == 0) {
throw new Exception("prénom (" + value + ") invalide");
} else {
prenom = value;
}
}//if
}//voornaam
public string Nom {
get { return nom; }
set {
// geldige achternaam?
if (value == null || value.Trim().Length == 0) {
throw new Exception("nom (" + value + ") invalide");
} else { nom = value; }
}//if
}//achternaam
public int Age {
get { return age; }
set {
// geldige leeftijd?
if (value >= 0) {
age = value;
} else
throw new Exception("âge (" + value + ") invalide");
}//als
}//leeftijd
// methode
public void Identifie() {
Console.WriteLine("[{0}, {1}, {2}]", prenom, nom, age);
}
}
}
Met een eigenschap kun je de waarde van een attribuut opvragen (get) of instellen (set). Een eigenschap wordt als volgt gedeclareerd:
waarbij Type het type moet zijn van het attribuut dat door de eigenschap wordt beheerd. Deze eigenschap kan twee methoden hebben, namelijk get en set. De get-methode is doorgaans bedoeld om de waarde van het attribuut dat zij beheert terug te geven (ze zou ook iets anders kunnen teruggeven; niets staat dat in de weg). De methode set ontvangt een parameter met de naam **value**, die ze normaal gesproken toekent aan het attribuut dat ze beheert. Ze kan van de gelegenheid gebruikmaken om de geldigheid van de ontvangen waarde te controleren en eventueel een uitzondering te genereren als de waarde ongeldig blijkt te zijn. Dat is wat hier gebeurt.
Hoe worden deze get- en set-methoden aangeroepen? Laten we eens kijken naar het volgende testprogramma:
using System;
namespace Chap2 {
class Program {
static void Main(string[] args) {
Personne p = new Personne("Jean", "Michelin", 34);
Console.Out.WriteLine("p=(" + p.Prenom + "," + p.Nom + "," + p.Age + ")");
p.Age = 56;
Console.Out.WriteLine("p=(" + p.Prenom + "," + p.Nom + "," + p.Age + ")");
try {
p.Age = -4;
} catch (Exception ex) {
Console.Error.WriteLine(ex.Message);
}//try-catch
}
}
}
In de instructie
Console.Out.WriteLine("p=(" + p.Prenom + "," + p.Nom + "," + p.Age + ")");
wordt getracht de waarden van de eigenschappen Prenom, Nom en Age van de persoon p op te halen. De get-methode van deze eigenschappen wordt dan aangeroepen en retourneert de waarde van het attribuut dat ze beheren.
In de instructie
willen we de waarde van de eigenschap Age vastleggen. De set-methode van deze eigenschap wordt dan aangeroepen. Deze ontvangt 56 als parameter value.
Een eigenschap P van een klasse C die alleen de get-methode definieert, wordt ‘alleen-lezen’ genoemd. Als c een object van klasse C is, wordt de bewerking c.P=waarde dan door de compiler geweigerd.
De uitvoering van het voorgaande testprogramma levert de volgende resultaten op:
Met eigenschappen kunnen we dus privé-attributen bewerken alsof ze openbaar zijn. Een ander kenmerk van eigenschappen is dat ze in combinatie met een constructor kunnen worden gebruikt volgens de volgende syntaxis:
Deze syntaxis komt overeen met de volgende code:
De volgorde van de eigenschappen doet er niet toe. Hier volgt een voorbeeld.
Aan de klasse Personne wordt een nieuwe constructor zonder parameters toegevoegd:
public Personne() {
}
De constructor initialiseert de leden van het object niet. Dit wordt de standaardconstructor genoemd. Deze wordt gebruikt wanneer de klasse geen constructor definieert.
De volgende code maakt een nieuwe Personne aan en initialiseert deze (regel 6) met de eerder gepresenteerde syntaxis:
using System;
namespace Chap2 {
class Program {
static void Main(string[] args) {
Personne p2 = new Personne { Age = 7, Prenom = "Arthur", Nom = "Martin" };
Console.WriteLine("p2=({0},{1},{2})", p2.Prenom, p2.Nom, p2.Age);
}
}
}
In regel 6 hierboven wordt de constructor zonder parameters Personne() gebruikt. In dit specifieke geval had men ook het volgende kunnen schrijven
Personne p2 = new Personne() { Age = 7, Prenom = "Arthur", Nom = "Martin" };
maar de haakjes van de parameterloze constructor Personne() zijn in deze syntaxis niet verplicht.
De resultaten van de uitvoering zijn als volgt:
In veel gevallen beperken de methoden get en set van een eigenschap zich tot het lezen en schrijven van een privéveld zonder verdere verwerking. In dit scenario kan dan een automatische eigenschap worden gebruikt die als volgt is gedeclareerd:
Het privéveld dat aan de eigenschap is gekoppeld, wordt niet gedeclareerd. Het wordt automatisch door de compiler gegenereerd. Toegang hiertoe is alleen mogelijk via de eigenschap. In plaats van te schrijven:
private string prenom;
...
// bijbehorende eigenschap
public string Prenom {
get { return prenom; }
set {
// geldige voornaam?
if (value == null || value.Trim().Length == 0) {
throw new Exception("prénom (" + value + ") invalide");
} else {
prenom = value;
}
}//if
}//voornaam
dan kunnen we schrijven:
zonder het veld prenom als privé te declareren. Het verschil tussen de twee voorgaande eigenschappen is dat de eerste de geldigheid van de voornaam in set controleert, terwijl de tweede geen controle uitvoert.
Het gebruik van de automatische eigenschap Prenom komt neer op het openbaar maken van een veld Prenom:
Men kan zich afvragen of er een verschil is tussen beide declaraties. Het wordt afgeraden om public als veld van een klasse te declareren. Dit druist in tegen het concept van inkapseling van de toestand van een object, een toestand die privé moet zijn en via openbare methoden moet worden blootgesteld.
Als de automatische eigenschap wordt gedeclareerd als virtuelle,, kan deze vervolgens in een afgeleide klasse opnieuw worden gedefinieerd:
class Class1 {
public virtual string Prop { get; set; }
}
class Class2 : Class1 {
public override string Prop { get { return base.Prop; } set {... } }
}
Regel 2 hierboven: kan de afgeleide klasse Class2 in set, code opnemen die de geldigheid controleert van de waarde die is toegewezen aan de automatische eigenschap base.Prop van de bovenliggende klasse Class1.
4.1.15. De methoden en attributen van de klasse
Stel dat we het aantal Personne-objecten willen tellen dat in een applicatie is aangemaakt. We kunnen zelf een teller bijhouden, maar dan lopen we het risico dat we tijdelijke objecten vergeten die hier en daar worden aangemaakt. Het lijkt veiliger om in de constructors van de klasse Personne een instructie op te nemen die een teller verhoogt. Het probleem is dat er een verwijzing naar deze teller moet worden doorgegeven, zodat de constructor deze kan verhogen: er moet een nieuwe parameter aan worden doorgegeven. Men kan de teller ook opnemen in de definitie van de klasse. Aangezien het een attribuut is van de klasse zelf en niet van een specifiek exemplaar van deze klasse, wordt het anders gedeclareerd met het sleutelwoord static:
private static long nbPersonnes;
Om ernaar te verwijzen, schrijven we Personne.nbPersonnes om aan te geven dat het een attribuut is van de klasse Personne zelf. Hier hebben we een privé-attribuut aangemaakt waartoe we buiten de klasse geen directe toegang hebben. We maken daarom een openbare eigenschap aan om toegang te geven tot het klasseattribuut nbPersonnes. Om de waarde van nbPersonnes in te stellen, heeft de methode get van deze eigenschap geen specifiek object Personne nodig: nbPersonnes is immers het attribuut van een hele klasse. Daarom is er een eigenschap nodig die eveneens static heet:
public static long NbPersonnes {
get { return nbPersonnes; }
}
die van buitenaf wordt aangeroepen met de syntaxis Personne.NbPersonnes. Hier volgt een voorbeeld.
De klasse Personne ziet er dan als volgt uit:
using System;
namespace Chap2 {
public class Personne {
// klasse-attributen
private static long nbPersonnes;
public static long NbPersonnes {
get { return nbPersonnes; }
}
// instantie-attributen
private string prenom;
private string nom;
private int age;
// constructors
public Personne(String p, String n, int age) {
Initialise(p, n, age);
nbPersonnes++;
}
public Personne(Personne p) {
Initialise(p);
nbPersonnes++;
}
...
}
In de regels 20 en 24 verhogen de constructors het statische veld van regel 7.
Met het volgende programma:
using System;
namespace Chap2 {
class Program {
static void Main(string[] args) {
Personne p1 = new Personne("Jean", "Dupont", 30);
Personne p2 = new Personne(p1);
new Personne(p1);
Console.WriteLine("Nombre de personnes créées : " + Personne.NbPersonnes);
}
}
}
krijgt men de volgende resultaten:
4.1.16. Een tabel met personen
Een object is een gegeven zoals elk ander en daarom kunnen meerdere objecten in een array worden verzameld:
using System;
namespace Chap2 {
class Program {
static void Main(string[] args) {
// een array van personen
Personne[] amis = new Personne[3];
amis[0] = new Personne("Jean", "Dupont", 30);
amis[1] = new Personne("Sylvie", "Vartan", 52);
amis[2] = new Personne("Neil", "Armstrong", 66);
// weergave
foreach (Personne ami in amis) {
ami.Identifie();
}
}
}
}
- regel 7: maakt een array aan met 3 elementen van het type Personne. Deze 3 elementen worden hier geïnitialiseerd met de waarden null en c.a.d, aangezien ze naar geen enkel object verwijzen. Ook hier wordt, in de volksmond, gesproken van een array van objecten, terwijl het slechts een array van verwijzingen naar objecten is. Het aanmaken van de array van objecten, die zelf ook een object is (aanwezigheid van new), creëert geen enkel object van het type van de elementen ervan: dat moet daarna gebeuren.
- regels 8-10: aanmaken van de 3 objecten van het type Personne
- regels 12-14: weergave van de inhoud van de array amis
Dit levert de volgende resultaten op:
4.2. Overerving aan de hand van een voorbeeld
4.2.1. Algemeen
We gaan hier in op het begrip overerving. Het doel van overerving is om een bestaande klasse te „aanpassen” zodat deze aan onze behoeften voldoet. Stel dat we een klasse Enseignant willen maken: een leraar is een bijzonder persoon. Hij heeft eigenschappen die een ander niet heeft: het vak dat hij onderwijst bijvoorbeeld. Maar hij heeft ook de eigenschappen die iedereen heeft: voornaam, achternaam en leeftijd. Een leraar maakt dus volledig deel uit van de klasse Personne, maar heeft extra eigenschappen. In plaats van een klasse Enseignant helemaal vanaf nul te schrijven, geven we er de voorkeur aan om voort te bouwen op de bestaande klasse Personne en deze aan te passen aan het specifieke karakter van leraren. Het concept van overerving maakt dit mogelijk.
Om aan te geven dat de klasse Enseignant de eigenschappen van de klasse Personne erft, schrijven we:
Personne wordt de bovenliggende (of moeder)klasse genoemd en Enseignant de afgeleide (of dochter)klasse. Een object Enseignant bezit alle eigenschappen van een object Personne: het heeft dezelfde attributen en dezelfde methoden. Deze attributen en methoden van de bovenliggende klasse worden niet herhaald in de definitie van de afgeleide klasse: we volstaan met het vermelden van de attributen en methoden die door de afgeleide klasse zijn toegevoegd:
We nemen aan dat de klasse Personne als volgt is gedefinieerd:
using System;
namespace Chap2 {
public class Personne {
// klasse-attributen
private static long nbPersonnes;
public static long NbPersonnes {
get { return nbPersonnes; }
}
// instantie-attributen
private string prenom;
private string nom;
private int age;
// constructors
public Personne(String prenom, String nom, int age) {
Nom = nom;
Prenom = prenom;
Age = age;
nbPersonnes++;
Console.WriteLine("Constructeur Personne(string, string, int)");
}
public Personne(Personne p) {
Nom = p.Nom;
Prenom = p.Prenom;
Age = p.Age;
nbPersonnes++;
Console.WriteLine("Constructeur Personne(Personne)");
}
// eigenschappen
public string Prenom {
get { return prenom; }
set {
// geldige voornaam?
if (value == null || value.Trim().Length == 0) {
throw new Exception("prénom (" + value + ") invalide");
} else {
prenom = value;
}
}//if
}//voornaam
public string Nom {
get { return nom; }
set {
// geldige achternaam?
if (value == null || value.Trim().Length == 0) {
throw new Exception("nom (" + value + ") invalide");
} else { nom = value; }
}//if
}//achternaam
public int Age {
get { return age; }
set {
// geldige leeftijd?
if (value >= 0) {
age = value;
} else
throw new Exception("âge (" + value + ") invalide");
}//als
}//leeftijd
// eigendom
public string Identite {
get { return String.Format("[{0}, {1}, {2}]", prenom, nom, age);}
}
}
}
De methode Identifie is vervangen door de alleen-lezen eigenschap Identite, die de persoon identificeert. We maken een klasse Enseignant aan die erft van de klasse Personne:
using System;
namespace Chap2 {
class Enseignant : Personne {
// attributen
private int section;
// constructor
public Enseignant(string prenom, string nom, int age, int section)
: base(prenom, nom, age) {
// de sectie wordt opgeslagen via de eigenschap Section
Section = section;
// tracking
Console.WriteLine("Construction Enseignant(string, string, int, int)");
}//constructor
// eigenschap Section
public int Section {
get { return section; }
set { section = value; }
}// Section
}
}
De klasse Enseignant voegt het volgende toe aan de methoden en attributen van de klasse Personne:
- regel 4: de klasse Enseignant is afgeleid van de klasse Personne
- regel 6: een attribuut section dat het sectienummer aangeeft waartoe de docent behoort binnen het docentencorps (grofweg één sectie per vak). Dit privé-attribuut is toegankelijk via de openbare eigenschap Section in de regels 18-21
- regel 9: een nieuwe constructor waarmee alle attributen van een docent kunnen worden geïnitialiseerd
4.2.2. Constructie van een Docent-object
Een dochterklasse erft de constructors van haar bovenliggende klasse niet. Ze moet daarom haar eigen constructors definiëren. De constructor van de klasse Enseignant is als volgt:
// constructor
public Enseignant(string prenom, string nom, int age, int section)
: base(prenom, nom, age) {
// de sectie wordt opgeslagen
Section = section;
// opvolging
Console.WriteLine("Construction enseignant(string, string, int, int)");
}//fabrikant
De declaratie
public Enseignant(string prenom, string nom, int age, int section)
: base(prenom, nom, age) {
geeft aan dat de constructor vier parameters ontvangt: prenom, nom, age, section ontvangt en drie parameters (prenom,nom,age) doorgeeft aan zijn basisklasse, in dit geval de klasse Personne. We weten dat deze klasse een constructor Personne(string, string, int) heeft waarmee een persoon kan worden aangemaakt met de doorgegeven parameters (prenom,nom,age). Zodra de aanmaak van de basisklasse is voltooid, wordt de aanmaak van het object Enseignant voortgezet door de uitvoering van de body van de constructor:
Merk op dat links van het =-teken niet het attribuut section van het object is gebruikt, maar de bijbehorende eigenschap Section. Hierdoor kan de constructor profiteren van eventuele geldigheidscontroles die deze methode zou kunnen uitvoeren. Zo hoeft men deze niet op twee verschillende plaatsen te plaatsen: in de constructor en in de eigenschap.
Kortom, de constructor van een afgeleide klasse:
- geeft aan zijn basisklasse de parameters door die deze nodig heeft om zichzelf te construeren
- gebruikt de overige parameters om zijn eigen attributen te initialiseren
Men had ook het volgende kunnen schrijven:
// fabrikant
public Enseignant(string prenom, string nom, int age, int section){
this.prenom=prenom;
this.nom=nom;
this.age=age;
this.section=section;
}
Dat is onmogelijk. De klasse Personne heeft haar drie velden prenom, nom en age als privé (private) gedeclareerd. Alleen objecten van dezelfde klasse hebben directe toegang tot deze velden. Alle andere objecten, inclusief afgeleide objecten zoals hier, moeten via openbare methoden werken om er toegang toe te krijgen. Dit zou anders zijn geweest als de klasse Personne de drie velden als beschermd (protected) had gedeclareerd: dan zou zij afgeleide klassen toestaan om directe toegang te hebben tot de drie velden. In ons voorbeeld was het gebruik van de constructor van de bovenliggende klasse dus de juiste oplossing en dat is ook de gebruikelijke werkwijze: bij het aanmaken van een onderliggend object roepen we eerst de constructor van het bovenliggende object aan en vullen we vervolgens de initialisaties aan die specifiek zijn voor het onderliggende object (section in ons voorbeeld).
Laten we een eerste testprogramma [Program.cs] proberen:
using System;
namespace Chap2 {
class Program {
static void Main(string[] args) {
Console.WriteLine(new Enseignant("Jean", "Dupont", 30, 27).Identite);
}
}
}
Dit programma beperkt zich tot het aanmaken van een object Enseignant (new) en het identificeren ervan. De klasse Enseignant heeft geen methode Identite, maar de bovenliggende klasse heeft er wel een, die bovendien openbaar is: door overerving wordt dit een openbare methode van de klasse Enseignant.
Het volledige project ziet er als volgt uit:
![]() |
De verkregen resultaten zijn als volgt:
We zien dat:
- een object Personne (regel 1) vóór het object Enseignant (regel 2) is aangemaakt
- de verkregen identiteit is die van het object Personne
4.2.3. Een methode of eigenschap opnieuw definiëren
In het vorige voorbeeld hadden we de identiteit van het onderdeel Personne van de docent, maar er ontbreekt bepaalde informatie die specifiek is voor de klasse Enseignant (de sectie). We moeten dus een eigenschap schrijven waarmee de docent kan worden geïdentificeerd:
using System;
namespace Chap2 {
class Enseignant : Personne {
// attributen
private int section;
// constructor
public Enseignant(string prenom, string nom, int age, int section)
: base(prenom, nom, age) {
// de sectie wordt opgeslagen via de eigenschap Section
Section = section;
// tracking
Console.WriteLine("Construction Enseignant(string, string, int, int)");
}//constructor
// eigenschap Section
public int Section {
get { return section; }
set { section = value; }
}// sectie
// eigenschap Identiteit
public new string Identite {
get { return String.Format("Enseignant[{0},{1}]", base.Identite, Section); }
}
}
}
Regels 24-26: de eigenschap Identite van de klasse Enseignant is gebaseerd op de eigenschap Identite van de bovenliggende klasse (base.Identite) (regel 25) om het deel „Personne” weer te geven en vult dit vervolgens aan met het veld section, dat eigen is aan de klasse Enseignant. Let op de declaratie van de eigenschap Identite:
public new string Identite{
Stel dat we een object enseignant E hebben. Dit object bevat een object Personne:
![]() |
De eigenschap Identite is zowel gedefinieerd in de klasse Enseignant als in de bovenliggende klasse Personne. In de dochterklasse Enseignant moet de eigenschap Identite worden voorafgegaan door het sleutelwoord new om aan te geven dat er een nieuwe eigenschap Identite wordt gedefinieerd voor de klasse Enseignant.
public new string Identite{
De klasse Enseignant beschikt nu over twee eigenschappen Identite:
- de eigenschap die is overgenomen van de bovenliggende klasse Personne
- een eigen eigenschap
Als E een object Enseignant is, verwijst E.Identite naar de eigenschap Identite van de klasse Enseignant. Men zegt dat de eigenschap Identite van de dochterklasse de eigenschap Identite van de moederklasse herdefinieert of verbergt. In het algemeen geldt: als O een object is en M een methode, dan zoekt het systeem, om de methode O.M uit te voeren, naar een methode M in de volgende volgorde:
- in de klasse van het object O
- in de bovenliggende klasse, indien deze bestaat
- in de bovenliggende klasse van de bovenliggende klasse, indien deze bestaat
- enzovoort…
Erfenis maakt het dus mogelijk om in de dochterklasse methoden/eigenschappen met dezelfde naam als in de bovenliggende klasse opnieuw te definiëren. Hierdoor kan de dochterklasse aan haar eigen behoeften worden aangepast. In combinatie met polymorfisme, dat we straks zullen bespreken, is het opnieuw definiëren van methoden/eigenschappen het belangrijkste voordeel van erfenis.
Laten we hetzelfde testprogramma als eerder bekijken:
using System;
namespace Chap2 {
class Program {
static void Main(string[] args) {
Console.WriteLine(new Enseignant("Jean", "Dupont", 30, 27).Identite);
}
}
}
De verkregen resultaten zijn deze keer als volgt:
4.2.4. Polymorfisme
Laten we eens kijken naar een reeks klassen: C0 ← C1 ← C2 ← … ←Cn
waarbij Ci ← Cj aangeeft dat de klasse Cj is afgeleid van de klasse Ci. Dit houdt in dat de klasse Cj alle kenmerken van de klasse Ci heeft, plus nog andere. Stel dat er objecten Oi van het type Ci zijn. Het is toegestaan om te schrijven:
Door overerving heeft de klasse Cj namelijk alle kenmerken van de klasse Ci plus nog andere. Een object Oj van het type Cj bevat dus een object van het type Ci. De bewerking
zorgt ervoor dat Oi een verwijzing is naar het object van het type Ci dat in het object Oj is opgenomen.
Het feit dat een variabele Oi van de klasse Ci in feite niet alleen naar een object van de klasse Ci kan verwijzen, maar naar elk object dat is afgeleid van de klasse Ci,, wordt polymorfisme genoemd: het vermogen van een variabele om naar verschillende soorten objecten te verwijzen.
Laten we een voorbeeld nemen en de volgende functie bekijken, die onafhankelijk is van elke klasse (static):
We zouden net zo goed kunnen schrijven
als
In het laatste geval krijgt de formele parameter p van het type Personne van de statische methode Affiche een waarde van het type Enseignant. Aangezien het type Enseignant is afgeleid van het type Personne, is dit toegestaan.
4.2.5. Herdefinitie en polymorfisme
Laten we onze methode Affiche aanvullen:
public static void Affiche(Personne p) {
// toont identiteit van p
Console.WriteLine(p.Identite);
}//toont
De eigenschap p.Identite retourneert een tekenreeks die het Personne-object p identificeert. Wat gebeurt er in het vorige voorbeeld als de parameter die aan de methode Affiche wordt doorgegeven een object is van het type Enseignant:
Enseignant e = new Enseignant(...);
Affiche(e);
Laten we eens naar het volgende voorbeeld kijken:
using System;
namespace Chap2 {
class Program2 {
static void Main(string[] args) {
// een docent
Enseignant e = new Enseignant("Lucile", "Dumas", 56, 61);
Affiche(e);
// een persoon
Personne p = new Personne("Jean", "Dupont", 30);
Affiche(p);
}
// toont
public static void Affiche(Personne p) {
// toont identiteit van p
Console.WriteLine(p.Identite);
}//toont
}
}
De verkregen resultaten zijn als volgt:
Uit de uitvoering blijkt dat de instructie p.Identite (regel 17) telkens de eigenschap Identite van een Personne heeft uitgevoerd, eerst (regel 7) de persoon die in de Enseignant e zit, en vervolgens (regel 10) de Personne p zelf. Het heeft zich niet aangepast aan het object dat daadwerkelijk als parameter aan Affiche is doorgegeven. We hadden liever de volledige identiteit van de Enseignant e gehad. Hiervoor had de notatie p.Identite moeten verwijzen naar de eigenschap Identite van het object waarnaar daadwerkelijk wordt verwezen door p, in plaats van naar de eigenschap Identite van het deel "Personne" van het object waarnaar p daadwerkelijk verwijst.
Dit resultaat kan worden bereikt door Identite als een virtuele eigenschap (virtual) te declareren in de basisklasse Personne:
public virtual string Identite {
get { return String.Format("[{0}, {1}, {2}]", prenom, nom, age); }
}
Het sleutelwoord **virtual maakt van Identite een virtuele eigenschap. Dit sleutelwoord kan ook op methoden worden toegepast. Afgeleide klassen die een virtuele eigenschap of methode herdefiniëren, moeten dan het sleutelwoord override gebruiken in plaats van new om hun herdefinieerde eigenschap/methode aan te duiden. Zo wordt in de klasse Enseignant de eigenschap Identite als volgt herdefinieerd:
public override string Identite {
get { return String.Format("Enseignant[{0},{1}]", base.Identite, Section); }
}
Het voorgaande programma levert dan de volgende resultaten op:
Deze keer, in regel 3, hebben we inderdaad de volledige identiteit van de docent gekregen. Laten we nu eens een methode herdefiniëren in plaats van een eigenschap. De klasse object (C#-alias van System.Object) is de „bovenliggende” klasse van alle C#-klassen. Dus wanneer we schrijven:
schrijven we impliciet:
De klasse System.Object definieert een virtuele methode ToString:
![]() |
De methode ToString retourneert de naam van de klasse waartoe het object behoort, zoals in het volgende voorbeeld te zien is:
using System;
namespace Chap2 {
class Program2 {
static void Main(string[] args) {
// een leraar
Console.WriteLine(new Enseignant("Lucile", "Dumas", 56, 61).ToString());
// een persoon
Console.WriteLine(new Personne("Jean", "Dupont", 30).ToString());
}
}
}
De gegenereerde resultaten zijn als volgt:
Opgemerkt moet worden dat, hoewel we de methode ToString niet opnieuw hebben gedefinieerd in de klassen Personne en Enseignant, kunnen we toch vaststellen dat de methode ToString van de klasse Object in staat was om de werkelijke klassenaam van het object weer te geven.
Laten we de methode ToString in de klassen Personne en Enseignant opnieuw definiëren:
// methode ToString
public override string ToString() {
return Identite;
}
De definitie is in beide klassen hetzelfde. Laten we het volgende testprogramma eens bekijken:
using System;
namespace Chap2 {
class Program3 {
public static void Main() {
// een leraar
Enseignant e = new Enseignant("Lucile", "Dumas", 56, 61);
Affiche(e);
// een persoon
Personne p = new Personne("Jean", "Dupont", 30);
Affiche(p);
}
// affiche
public static void Affiche(Personne p) {
// identiteitsposter van p
Console.WriteLine(p);
}//Affiche
}
}
Laten we eens kijken naar de methode Affiche, die als parameter een persoon p accepteert. Op regel 15 heeft de methode WriteLine van de klasse Console geen variant die een parameter van het type Personne accepteert. Van de verschillende varianten van Writeline is er één die een parameter van het type Object accepteert. De compiler zal deze methode, WriteLine(Object o), gebruiken, omdat deze signatuur aangeeft dat de parameter o van het type Object of een afgeleid type kan zijn. Aangezien Object de bovenliggende klasse is van alle klassen, kan elk object als parameter worden doorgegeven aan WriteLine en dus ook een object van het type Personne of Enseignant. De methode WriteLine(Object o) schrijft o.ToString() naar de schrijfstroom Out. Aangezien de methode ToString virtueel is, zal, als het object o (van het type Object of daarvan afgeleid) de methode ToString heeft geherdefinieerd, deze laatste worden gebruikt. Dit is hier het geval bij de klassen Personne en Enseignant.
Dit blijkt uit de uitvoerresultaten:
4.3. De betekenis van een operator voor een klasse opnieuw definiëren
4.3.1. Inleiding
Laten we eens kijken naar de instructie
waarbij op1 en op2 twee operanden zijn. Het is mogelijk om de betekenis van de operator + opnieuw te definiëren. Als de operand op1 een object is van de klasse C1, moet er een statische methode worden gedefinieerd in de klasse C1 met de volgende signatuur:
Wanneer de compiler de instructie
vertaalt hij deze naar C1.operator+(op1,op2). Het type dat door de methode operator wordt geretourneerd, is belangrijk. Laten we namelijk eens kijken naar de bewerking op1+op2+op3. Deze wordt door de compiler omgezet in (op1+op2)+op3. Stel dat res12 het resultaat is van op1+op2. De bewerking die vervolgens wordt uitgevoerd, is res12+op3. Als res12 van het type C1 is, wordt deze ook omgezet in C1.operator+(res12,op3). Hierdoor kunnen de bewerkingen aan elkaar worden gekoppeld.
Ook unaire operatoren met slechts één operand kunnen opnieuw worden gedefinieerd. Als op1 dus een object is van het type C1, kan de bewerking op1++ worden herdefinieerd door een statische methode van de klasse C1:
Wat hier is gezegd, geldt voor de meeste operatoren, met echter enkele uitzonderingen:
- de operatoren == en != moeten tegelijkertijd worden herdefinieerd
- de operatoren &&, ||, [], (), +=, -=, ... kunnen niet opnieuw worden gedefinieerd
4.3.2. Een voorbeeld
We maken een klasse ListeDePersonnes die is afgeleid van de klasse ArrayList. Deze klasse implementeert een dynamische lijst en wordt in het volgende hoofdstuk besproken. Van deze klasse gebruiken we alleen de volgende elementen:
- de methode L.Add(Object o), waarmee een object o aan de lijst L kan worden toegevoegd. Hier zal het object o een object Personne zijn.
- de eigenschap L.Count die het aantal elementen van de lijst L aangeeft
- de notatie L[i] die het i-de element van de lijst L aangeeft
De klasse ListeDePersonnes zal alle attributen, methoden en eigenschappen van de klasse ArrayList overnemen. De definitie ervan is als volgt:
using System;
using System.Collections;
using System.Text;
namespace Chap2 {
class ListeDePersonnes : ArrayList{
// herdefinitie van de operator +, om een persoon aan de lijst toe te voegen
public static ListeDePersonnes operator +(ListeDePersonnes l, Personne p) {
// we voegen de persoon p toe aan de ListeDePersonnes l
l.Add(p);
// de ListeDePersonnes wordt
return l;
}// operator +
// ToString
public override string ToString() {
// geeft (element1, element2, ..., elementn)
// open haakje
StringBuilder listeToString = new StringBuilder("(");
// we doorlopen de lijst met personen (this)
for (int i = 0; i < Count - 1; i++) {
listeToString.Append(this[i]).Append(",");
}//for
// laatste element
if (Count != 0) {
listeToString.Append(this[Count-1]);
}
// sluitend haakje
listeToString.Append(")");
// er moet een tekenreeks worden geretourneerd
return listeToString.ToString();
}//ToString
}
}
- regel 6: de klasse ListeDePersonnes is afgeleid van de klasse ArrayList
- regels 8-13: definitie van de operator + voor de bewerking l + p, waarbij l van het type ListeDePersonnes is en p van het type Personne of daarvan is afgeleid.
- regel 10: de persoon p wordt toegevoegd aan de lijst l. Hier wordt de methode Add van de bovenliggende klasse ArrayList gebruikt.
- regel 12: de verwijzing naar de lijst l wordt teruggegeven om de +-operatoren achter elkaar te kunnen gebruiken, zoals in l + p1 + p2. De bewerking l+p1+p2 wordt (op basis van de prioriteit van de operatoren) geïnterpreteerd als (l+p1)+p2. De bewerking l+p1 levert de referentie l op. De bewerking (l+p1)+p2 wordt dan l+p2, waarmee de persoon p2 wordt toegevoegd aan de lijst met personen l.
- regel 16: we herdefiniëren de methode ToString om een lijst met personen weer te geven in de vorm (persoon1, persoon2, ...) waarbij personnei zelf het resultaat is van de methode ToString van de klasse Personne.
- regel 19: we gebruiken een object van het type StringBuilder. Deze klasse is geschikter dan de klasse string zodra er veel bewerkingen op de tekenreeks moeten worden uitgevoerd, in dit geval toevoegingen. Elke bewerking op een object van het type string levert namelijk een nieuw object van het type string op, terwijl dezelfde bewerkingen op een object van het type StringBuilder het object wijzigen zonder een nieuw object aan te maken. We gebruiken de methode Append om de tekenreeksen aan elkaar te koppelen.
- regel 21: we doorlopen de elementen van de lijst met personen. Deze lijst wordt hier aangeduid met this. Dit is het huidige object waarop de methode ToString wordt uitgevoerd. De eigenschap Count is een eigenschap van de bovenliggende klasse ArrayList.
- regel 22: element nr. i van de huidige lijst this is toegankelijk via de notatie this[i]. Ook hier gaat het om een eigenschap van de klasse ArrayList. Aangezien het gaat om het samenvoegen van tekenreeksen, wordt de methode this[i].ToString() gebruikt. Aangezien deze methode virtueel is, wordt de methode ToString van het object this, van het type Personne of een afgeleid type, gebruikt.
- regel 31: we moeten een object van het type string retourneren (regel 16). De klasse StringBuilder heeft een methode ToString waarmee kan worden overgegaan van een type StringBuilder naar een type string.
Merk op dat de klasse ListeDePersonnes geen constructor heeft. In dit geval weten we dat de constructor
zal worden gebruikt. Deze constructor doet niets anders dan de constructor zonder parameters van de bovenliggende klasse aanroepen:
Een testklasse zou er als volgt uit kunnen zien:
using System;
namespace Chap2 {
class Program1 {
static void Main(string[] args) {
// een lijst met personen
ListeDePersonnes l = new ListeDePersonnes();
// personen toevoegen
l = l + new Personne("jean", "martin",10) + new Personne("pauline", "leduc",12);
// weergave
Console.WriteLine("l=" + l);
l = l + new Enseignant("camille", "germain",27,60);
Console.WriteLine("l=" + l);
}
}
}
- regel 7: aanmaken van een lijst met personen
- regel 9: toevoegen van 2 personen met de operator +
- regel 12: toevoegen van een docent
- regels 11 en 13: gebruik van de opnieuw gedefinieerde methode ListeDePersonnes.ToString().
De resultaten:
4.4. Een indexer voor een klasse definiëren
We blijven hier de klasse ListeDePersonnes gebruiken. Als l een object ListeDePersonnes is, willen we de notatie l[i] kunnen gebruiken om de persoon nr. i uit de lijst l aan te duiden, zowel bij het lezen (Persoon p=l[i]) als bij het schrijven (l[i]=new Persoon(...)).
Om l[i] te kunnen schrijven, waarbij l[i] verwijst naar een object Personne, moeten we in de klasse ListeDePersonnes de volgende this-methode definiëren:
public Personne this[int i] {
get { ... }
set { ... }
}
We noemen de methode this[int i] een indexer, omdat deze betekenis geeft aan de uitdrukking obj[i], die doet denken aan de notatie van arrays, terwijl obj geen array is, maar een object. De methode get van de methode this van hetobject obj wordt aangeroepen wanneer men schrijft variabele = obj[i] en de methode set wanneer men schrijft *obj[i] = waarde*.
De klasse ListeDePersonnes is afgeleid van de klasse ArrayList, die zelf een indexer heeft:
Er is een conflict tussen de methode this van de klasse ListeDePersonnes:
public Personne this[int i]
en de methode this van de klasse ArrayList
public object this[int i]
omdat ze dezelfde naam hebben en hetzelfde type parameter (int) accepteren. Om aan te geven dat de methode this van de klasse ListeDePersonnes de gelijknamige methode van de klasse ArrayList „verbergt”, moet het sleutelwoord new worden toegevoegd aan de declaratie van de indexer van ListeDePersonnes. We schrijven dus:
public new Personne this[int i]{
get { ... }
set { ... }
}
Laten we deze methode aanvullen. De methode this.get wordt aangeroepen wanneer bijvoorbeeld variable=l[i] wordt geschreven, waarbij l van het type ListeDePersonnes is. We moeten dan persoon nr. i uit de lijst l retourneren. Dit gebeurt met de notatie base[i], die het object nr. i van de klasse ArrayList, die ten grondslag ligt aan de klasse ListeDePersonnes, retourneert. Aangezien het geretourneerde object van het type Object is, is een typeconversie naar de klasse Personne nodig.
public new Personne this[int i]{
get { return (Personne) base[i]; }
set { ... }
}
De methode set wordt aangeroepen wanneer l[i]=p wordt geschreven, waarbij p een Personne is. Het gaat er dan om de persoon p toe te wijzen aan element i van de lijst l.
public new Personne this[int i]{
get { ... }
set { base[i]=value; }
}
Hier wordt de persoon p, vertegenwoordigd door het trefwoord value, toegewezen aan element nr. i van de basisklasse ArrayList.
De indexer van de klasse ListeDePersonnes ziet er dan als volgt uit:
public new Personne this[int i]{
get { return (Personne) base[i]; }
set { base[i]=value; }
}
Nu willen we ook Personne p=l["nom"], c.a.d kunnen schrijven en de lijst l niet langer op basis van een elementnummer, maar op basis van een persoonsnaam indexeren. Hiervoor definiëren we een nieuwe indexer:
// zoeken op naam
public int this[string nom] {
get {
// de persoon zoeken
for (int i = 0; i < Count; i++) {
if (((Personne)base[i]).Nom == nom)
return i;
}//for
return -1;
}//ophalen
}
De eerste regel
public int this[string nom]
geeft aan dat de klasse ListeDePersonnes wordt geïndexeerd op basis van de tekenreeks nom en dat het resultaat van l[nom] een geheel getal is. Dit gehele getal is de positie in de lijst van de persoon met de naam nom of -1 als deze persoon niet in de lijst voorkomt. We definiëren alleen de eigenschap get,, waardoor de toewijzing l["nom"]=waarde wordt verhinderd, die de definitie van de eigenschap set zou hebben vereist. Het sleutelwoord new is niet nodig in de declaratie van de indexer, omdat de basisklasse ArrayList geen indexer this[string] definieert.
In de body van get wordt de lijst met personen doorlopen op zoek naar de als parameter doorgegeven naam. Als deze op positie i wordt gevonden, wordt i geretourneerd; anders wordt -1 geretourneerd.
Het vorige testprogramma wordt als volgt aangevuld:
using System;
namespace Chap2 {
class Program2 {
static void Main(string[] args) {
// een lijst met personen
ListeDePersonnes l = new ListeDePersonnes();
// personen toevoegen
l = l + new Personne("jean", "martin",10) + new Personne("pauline", "leduc",12);
// weergave
Console.WriteLine("l=" + l);
l = l + new Enseignant("camille", "germain",27,60);
Console.WriteLine("l=" + l);
// element 1 wijzigen
l[1] = new Personne("franck", "gallon",5);
// element 1 weergeven
Console.WriteLine("l[1]=" + l[1]);
// lijst weergeven l
Console.WriteLine("l=" + l);
// personen zoeken
string[] noms = { "martin", "germain", "xx" };
for (int i = 0; i < noms.Length; i++) {
int inom = l[noms[i]];
if (inom != -1)
Console.WriteLine("Personne(" + noms[i] + ")=" + l[inom]);
else
Console.WriteLine("Personne(" + noms[i] + ") n'existe pas");
}//for
}
}
}
De uitvoering ervan levert de volgende resultaten op:
4.5. Structuren
De C#-structuur is vergelijkbaar met de structuur in de programmeertaal C en lijkt sterk op het concept van een klasse. Een structuur wordt als volgt gedefinieerd:
Ondanks de gelijkenis in de syntaxis zijn er belangrijke verschillen tussen klassen en structuren. Het begrip overerving bestaat bijvoorbeeld niet bij structuren. Als we een klasse schrijven die niet afgeleid mag worden, welke verschillen tussen structuren en klassen helpen ons dan bij de keuze tussen beide? Laten we het volgende voorbeeld gebruiken om dit te ontdekken:
using System;
namespace Chap2 {
class Program1 {
static void Main(string[] args) {
// een structuur sp1
SPersonne sp1;
sp1.Nom = "paul";
sp1.Age = 10;
Console.WriteLine("sp1=SPersonne(" + sp1.Nom + "," + sp1.Age + ")");
// een structuur sp2
SPersonne sp2 = sp1;
Console.WriteLine("sp2=SPersonne(" + sp2.Nom + "," + sp2.Age + ")");
// sp2 is gewijzigd
sp2.Nom = "nicole";
sp2.Age = 30;
// controle van sp1 en sp2
Console.WriteLine("sp1=SPersonne(" + sp1.Nom + "," + sp1.Age + ")");
Console.WriteLine("sp2=SPersonne(" + sp2.Nom + "," + sp2.Age + ")");
// een object op1
CPersonne op1=new CPersonne();
op1.Nom = "paul";
op1.Age = 10;
Console.WriteLine("op1=CPersonne(" + op1.Nom + "," + op1.Age + ")");
// een object op2
CPersonne op2=op1;
Console.WriteLine("op2=CPersonne(" + op2.Nom + "," + op2.Age + ")");
// op2 is gewijzigd
op2.Nom = "nicole";
op2.Age = 30;
// controle van op1 en op2
Console.WriteLine("op1=CPersonne(" + op1.Nom + "," + op1.Age + ")");
Console.WriteLine("op2=CPersonne(" + op2.Nom + "," + op2.Age + ")");
}
}
// structuur SPersonne
struct SPersonne {
public string Nom;
public int Age;
}
// klasse CPersonne
class CPersonne {
public string Nom;
public int Age;
}
}
- regels 38-41: een structuur met twee openbare velden: Nom, Age
- regels 44-47: een klasse met twee openbare velden: Nom, Age
Als we dit programma uitvoeren, krijgen we de volgende resultaten:
Waar we eerder een klasse Personne gebruikten, gebruiken we nu een structuur SPersonne:
struct SPersonne {
public string Nom;
public int Age;
}
De structuur heeft hier geen constructor. Ze zou er wel een kunnen hebben, zoals we later zullen laten zien. Standaard beschikt ze altijd over de constructor zonder parameters, in dit geval SPersonne().
- regel 7 van de code: de declaratie
SPersonne sp1;
is gelijk aan de instructie:
SPersonne sp1=new Spersonne();
Er wordt een structuur (Naam,Leeftijd) aangemaakt en de waarde van sp1 is deze structuur zelf. In het geval van de klasse moet het aanmaken van het object (Naam,Leeftijd) expliciet gebeuren met de operator new (regel 22):
CPersonne op1=new CPersonne();
De vorige instructie maakt een object CPersonne aan (grofweg het equivalent van onze structuur) en de waarde van p1 is dan het adres (de referentie) van dit object.
Laten we het samenvatten
- in het geval van de structuur is de waarde van sp1 de structuur zelf
- in het geval van de klasse is de waarde van op1 het adres van het aangemaakte object
![]() |
Wanneer in het programma op regel 12 wordt geschreven:
SPersonne sp2 = sp1;
wordt een nieuwe structuur sp2(Naam,Leeftijd) aangemaakt en geïnitialiseerd met de waarde van sp1,, dus de structuur zelf.
![]() |
De structuur van sp1 wordt gekopieerd naar sp2 [1]. Dit is een kopie van de waarde. Laten we nu eens kijken naar de instructie op regel 27:
CPersonne op2=op1;
Bij klassen wordt de waarde van op1 gekopieerd naar op2, maar aangezien deze waarde in feite het adres van het object is, wordt het object zelf niet gedupliceerd: [2].
In het geval van de structuur [1]: als we de waarde van sp2 wijzigen, wordt de waarde van sp1 niet gewijzigd, wat het programma laat zien. In het geval van het object [2]: als het object waarnaar op2 verwijst, wordt gewijzigd, wordt het object waarnaar op1 verwijst, wel gewijzigd, aangezien het om hetzelfde object gaat. Dit blijkt ook uit de resultaten van het programma.
Uit deze uitleg kunnen we dus concluderen dat:
- de waarde van een variabele van het type structuur de structuur zelf is
- de waarde van een variabele van het type object is het adres van het object waarnaar wordt verwezen
Zodra dit fundamentele verschil begrepen is, blijkt de structuur sterk op de klasse te lijken, zoals het volgende nieuwe voorbeeld laat zien:
using System;
namespace Chap2 {
// structuur SPersonne
struct SPersonne {
// privé-attributen
private string nom;
private int age;
// eigenschappen
public string Nom {
get { return nom; }
set { nom = value; }
}//naam
public int Age {
get { return age; }
set { age = value; }
}//leeftijd
// fabrikant
public SPersonne(string nom, int age) {
this.nom = nom;
this.age = age;
}//fabrikant
// ToString
public override string ToString() {
return "SPersonne(" + Nom + "," + Age + ")";
}//ToString
}//structuur
}//naamruimte
- regels 8-9: twee privévelden
- regels 12-20: de bijbehorende openbare eigenschappen
- regels 23-26: er wordt een constructor gedefinieerd. Merk op dat de constructor zonder parameters SPersonne() altijd aanwezig is en niet hoeft te worden gedeclareerd. De declaratie ervan wordt door de compiler geweigerd. In de constructor op de regels 23-26 zou men in de verleiding kunnen komen om de privévelden nom en age te initialiseren via hun openbare eigenschappen Nom en Age. Dit wordt door de compiler geweigerd. De methoden van de structuur mogen niet worden gebruikt bij het aanmaken ervan.
- regels 29-31: herdefinitie van de methode ToString.
Een testprogramma zou er als volgt uit kunnen zien:
using System;
namespace Chap2 {
class Program1 {
static void Main(string[] args) {
// een persoon p1
SPersonne p1=new SPersonne();
p1.Nom="paul";
p1.Age= 10;
Console.WriteLine("p1={0}",p1);
// een persoon p2
SPersonne p2 = p1;
Console.WriteLine("p2=" + p2);
// p2 is gewijzigd
p2.Nom = "nicole";
p2.Age = 30;
// controle van p1 en p2
Console.WriteLine("p1=" + p1);
Console.WriteLine("p2=" + p2);
// een persoon p3
SPersonne p3 = new SPersonne("amandin", 18);
Console.WriteLine("p3=" + p3);
// een persoon p4
SPersonne p4 = new SPersonne { Nom = "x", Age = 10 };
Console.WriteLine("p4=" + p4);
}
}
}
- regel 7: we zijn verplicht om expliciet de constructor zonder parameters te gebruiken, omdat er een andere constructor in de structuur bestaat. Als de structuur geen constructor had gehad, zou de instructie
SPersonne p1;
voldoende zijn geweest om een lege structuur aan te maken.
- regels 8-9: de structuur wordt geïnitialiseerd via de openbare eigenschappen
- regel 10: de methode p1.ToString wordt gebruikt in de WriteLine.
- regel 21: aanmaken van een structuur met de constructor SPersonne(string, int)
- regel 24: aanmaken van een structuur met de constructor zonder parameters SPersonne(), waarbij tussen accolades de privévelden worden geïnitialiseerd via hun openbare eigenschappen.
Dit levert de volgende uitvoerresultaten op:
Het enige opvallende verschil tussen een structuur en een klasse is dat bij een klasse de objecten p1 en p2 aan het einde van het programma naar hetzelfde object zouden hebben verwezen.
4.6. Interfaces
Een interface is een verzameling prototypes van methoden of eigenschappen die samen een contract vormen. Een klasse die besluit een interface te implementeren, verbindt zich ertoe een implementatie te leveren van alle methoden die in de interface zijn gedefinieerd. De compiler controleert deze implementatie.
Hier volgt bijvoorbeeld de definitie van de interface System.Collections.IEnumerator:
public interface System.Collections.IEnumerator
{
// Eigenschappen
Object Current { get; }
// Methoden
bool MoveNext();
void Reset();
}
De eigenschappen en methoden van de interface worden alleen gedefinieerd door hun handtekeningen. Ze zijn niet geïmplementeerd (hebben geen code). Het zijn de klassen die de interface implementeren die de methoden en eigenschappen van de interface van code voorzien.
- regel 1: de klasse C implementeert de klasse IEnumerator. Merk op dat het teken : dat wordt gebruikt voor de implementatie van een interface hetzelfde is als het teken dat wordt gebruikt voor het afleiden van een klasse.
- regels 3-5: de implementatie van de methoden en eigenschappen van de interface IEnumerator.
Laten we de volgende interface eens bekijken:
namespace Chap2 {
public interface IStats {
double Moyenne { get; }
double EcartType();
}
}
De interface IStats bevat:
- een alleen-lezen eigenschap Moyenne: om het gemiddelde van een reeks waarden te berekenen
- een methode EcartType: om de standaardafwijking te berekenen
Opgemerkt moet worden dat nergens wordt gespecificeerd om welke reeks waarden het gaat. Het kan gaan om het gemiddelde van de cijfers van een klas, het maandelijkse gemiddelde van de verkoopcijfers van een bepaald product, de gemiddelde temperatuur op een bepaalde locatie, ... Dat is het principe van interfaces: er wordt verondersteld dat er methoden in het object bestaan, maar niet dat er specifieke gegevens zijn.
Een eerste implementatieklasse van de interface IStats zou een klasse kunnen zijn die dient om de cijfers van de leerlingen van een klas voor een bepaald vak op te slaan. Een leerling zou worden gekenmerkt door de volgende structuur Elève:
public struct Elève {
public string Nom { get; set; }
public string Prénom { get; set; }
}//Leerling
De leerling wordt geïdentificeerd aan de hand van zijn voor- en achternaam. In de regels 2-3 staan de automatische eigenschappen voor deze twee attributen.
Een cijfer zou worden gekenmerkt door de volgende structuur: Note:
public struct Note {
public Elève Elève { get; set; }
public double Valeur { get; set; }
}//Cijfer
Het cijfer wordt geïdentificeerd door de beoordeelde leerling en het cijfer zelf. In de regels 2-3 staan de automatische eigenschappen voor deze twee attributen.
De cijfers van alle leerlingen voor een bepaald vak worden gebundeld in de volgende klasse TableauDeNotes:
using System;
using System.Text;
namespace Chap2 {
public class TableauDeNotes : IStats {
// attributen
public string Matière { get; set; }
public Note[] Notes { get; set; }
public double Moyenne { get; private set; }
private double ecartType;
// constructor
public TableauDeNotes(string matière, Note[] notes) {
// opslag via openbare eigenschappen
Matière = matière;
Notes = notes;
// berekening van het gemiddelde van de cijfers
double somme = 0;
for (int i = 0; i < Notes.Length; i++) {
somme += Notes[i].Valeur;
}
if (Notes.Length != 0) Moyenne = somme / Notes.Length;
else Moyenne = -1;
// standaardafwijking
double carrés = 0;
for (int i = 0; i < Notes.Length; i++) {
carrés += Math.Pow((Notes[i].Valeur - Moyenne), 2);
}//for
if (Notes.Length != 0)
ecartType = Math.Sqrt(carrés / Notes.Length);
else ecartType = -1;
}//constructor
public double EcartType() {
return ecartType;
}
// ToString
public override string ToString() {
StringBuilder valeur = new StringBuilder(String.Format("matière={0}, notes=(", Matière));
int i;
// we voegen alle scores samen
for (i = 0; i < Notes.Length-1; i++) {
valeur.Append("[").Append(Notes[i].Elève.Prénom).Append(",").Append(Notes[i].Elève.Nom).Append(",").Append(Notes[i].Valeur).Append("],");
};
//laatste notitie
if (Notes.Length != 0) {
valeur.Append("[").Append(Notes[i].Elève.Prénom).Append(",").Append(Notes[i].Elève.Nom).Append(",").Append(Notes[i].Valeur).Append("]");
}
valeur.Append(")");
// einde
return valeur.ToString();
}//ToString
}//klasse
}
- regel 6: de klasse TableauDeNotes implementeert de interface IStats. Ze moet dus de eigenschap Moyenne en de methode EcartType implementeren. Deze worden geïmplementeerd in regel 10 (Moyenne) en regels 35-37 (EcartType)
- regels 8-10: drie automatische eigenschappen
- regel 8: het vak waarvan het object de cijfers opslaat
- regel 9: de tabel met de cijfers van de leerlingen (Leerling, Cijfer)
- regel 10: het gemiddelde van de cijfers – eigenschap die de eigenschap Moyenne van de interface IStats implementeert.
- regel 11: veld waarin de standaardafwijking van de cijfers wordt opgeslagen - de bijbehorende methode get (EcartType) van de regels 35-37 implementeert de methode EcartType van de interface IStats.
- regel 9: de cijfers worden opgeslagen in een array. Deze wordt bij het aanmaken van de klasse TableauDeNotes doorgegeven aan de constructor in de regels 14-33.
- regels 14-33: de constructor. Hier wordt ervan uitgegaan dat de aan de constructor doorgegeven cijfers daarna niet meer zullen veranderen. Daarom wordt de constructor gebruikt om onmiddellijk het gemiddelde en de standaardafwijking van deze cijfers te berekenen en deze op te slaan in de velden van de regels 10-11. Het gemiddelde wordt opgeslagen in het privéveld dat ten grondslag ligt aan de automatische eigenschap Moyenne van regel 10 en de standaardafwijking in het privéveld van regel 11.
- regel 10: de methode get van de automatische eigenschap Moyenne geeft het onderliggende privéveld weer.
- regels 35-37: de methode EcartType geeft de waarde van het privéveld van regel 11 weer.
Er zitten enkele subtiliteiten in deze code:
- regel 23: de methode set van de eigenschap Moyenne wordt gebruikt om de toewijzing uit te voeren. Deze methode is in regel 10 als privé gedeclareerd, zodat het toewijzen van een waarde aan de eigenschap Moyenne alleen binnen de klasse mogelijk is.
- regels 40-54: maken gebruik van een object StringBuilder om de tekenreeks te construeren die het object TableauDeNotes vertegenwoordigt, met het oog op betere prestaties. Opgemerkt moet worden dat de leesbaarheid van de code hierdoor sterk achteruitgaat. Dat is de keerzijde van de medaille.
In de vorige klasse werden de cijfers in een array opgeslagen. Het was niet mogelijk om een nieuw cijfer toe te voegen nadat het object TableauDeNotes was aangemaakt. We stellen nu een tweede implementatie van de interface IStats voor, genaamd ListeDeNotes, waarbij de cijfers deze keer in een lijst worden opgeslagen, met de mogelijkheid om cijfers toe te voegen na de eerste aanmaak van het object ListeDeNotes.
De code van de klasse ListeDeNotes is als volgt:
using System;
using System.Text;
using System.Collections.Generic;
namespace Chap2 {
public class ListeDeNotes : IStats {
// attributen
public string Matière { get; set; }
public List<Note> Notes { get; set; }
public double moyenne = -1;
public double ecartType = -1;
// constructor
public ListeDeNotes(string matière, List<Note> notes) {
// opslag via openbare eigenschappen
Matière = matière;
Notes = notes;
}//constructor
// een notitie toevoegen
public void Ajouter(Note note) {
// toevoegen van de notitie
Notes.Add(note);
// gemiddelde en standaardafwijking gereset
moyenne = -1;
ecartType = -1;
}
// ToString
public override string ToString() {
StringBuilder valeur = new StringBuilder(String.Format("matière={0}, notes=(", Matière));
int i;
// alle notities worden samengevoegd
for (i = 0; i < Notes.Count - 1; i++) {
valeur.Append("[").Append(Notes[i].Elève.Prénom).Append(",").Append(Notes[i].Elève.Nom).Append(",").Append(Notes[i].Valeur).Append("],");
};
//laatste score
if (Notes.Count != 0) {
valeur.Append("[").Append(Notes[i].Elève.Prénom).Append(",").Append(Notes[i].Elève.Nom).Append(",").Append(Notes[i].Valeur).Append("]");
}
valeur.Append(")");
// einde
return valeur.ToString();
}//ToString
// gemiddelde van de cijfers
public double Moyenne {
get {
if (moyenne != -1) return moyenne;
// berekening van het gemiddelde van de cijfers
double somme = 0;
for (int i = 0; i < Notes.Count; i++) {
somme += Notes[i].Valeur;
}
// het gemiddelde wordt weergegeven
if (Notes.Count != 0) moyenne = somme / Notes.Count;
return moyenne;
}
}
public double EcartType() {
// standaardafwijking
if (ecartType != -1) return ecartType;
// gemiddelde
double moyenne = Moyenne;
double carrés = 0;
for (int i = 0; i < Notes.Count; i++) {
carrés += Math.Pow((Notes[i].Valeur - moyenne), 2);
}//for
// de standaardafwijking weergeven
if (Notes.Count != 0)
ecartType = Math.Sqrt(carrés / Notes.Count);
return ecartType;
}
}//klas
}
- regel 7: de klasse ListeDeNotes implementeert de interface IStats
- regel 10: de notities worden nu in een lijst geplaatst in plaats van in een array
- regel 11: de automatische eigenschap Moyenne van de klasse TableauDeNotes is hier vervangen door een privéveld moyenne, regel 11, gekoppeld aan de openbare, alleen-lezen eigenschap Moyenne van de regels 48-60
- regels 22-28: er kan nu een notitie worden toegevoegd aan de reeds opgeslagen notities, wat voorheen niet mogelijk was.
- regels 15-19: daardoor worden het gemiddelde en de standaardafwijking niet langer berekend in de constructor, maar in de methoden van de interface zelf: Moyenne (regels 48-60) en EcartType (62-76). De herberekening wordt echter alleen opnieuw gestart als het gemiddelde en de standaardafwijking afwijken van -1 (regels 50 en 64).
Een testklasse zou er als volgt uit kunnen zien:
using System;
using System.Collections.Generic;
namespace Chap2 {
class Program1 {
static void Main(string[] args) {
// enkele leerlingen & cijfers voor Engels
Elève[] élèves1 = { new Elève { Prénom = "Paul", Nom = "Martin" }, new Elève { Prénom = "Maxime", Nom = "Germain" }, new Elève { Prénom = "Berthine", Nom = "Samin" } };
Note[] notes1 = { new Note { Elève = élèves1[0], Valeur = 14 }, new Note { Elève = élèves1[1], Valeur = 16 }, new Note { Elève = élèves1[2], Valeur = 18 } };
// die we opslaan in een object TableauDeNotes
TableauDeNotes anglais = new TableauDeNotes("anglais", notes1);
// weergave van gemiddelde en standaardafwijking
Console.WriteLine("{2}, Moyenne={0}, Ecart-type={1}", anglais.Moyenne, anglais.EcartType(), anglais);
// we plaatsen de leerlingen en het vak in een object ListeDeNotes
ListeDeNotes français = new ListeDeNotes("français", new List<Note>(notes1));
// weergave van gemiddelde en standaardafwijking
Console.WriteLine("{2}, Moyenne={0}, Ecart-type={1}", français.Moyenne, français.EcartType(), français);
// er wordt een cijfer toegevoegd
français.Ajouter(new Note { Elève = new Elève { Prénom = "Jérôme", Nom = "Jaric" }, Valeur = 10 });
// weergave van gemiddelde en standaardafwijking
Console.WriteLine("{2}, Moyenne={0}, Ecart-type={1}", français.Moyenne, français.EcartType(), français);
}
}
}
- regel 8: aanmaken van een tabel met leerlingen met behulp van de constructor zonder parameters en initialisatie via de openbare eigenschappen
- regel 9: aanmaken van een array met cijfers volgens dezelfde techniek
- regel 11: een object TableauDeNotes waarvan het gemiddelde en de standaardafwijking worden berekend regel 13
- regel 15: een object ListeDeNotes waarvan het gemiddelde en de standaardafwijking worden berekend in regel 17. De klasse List<Note> heeft een constructor die een object accepteert dat de interface IEnumerable<Note> implementeert. De array notes1 implementeert deze interface en kan worden gebruikt om het object List<Note> te construeren.
- regel 19: toevoeging van een nieuwe opmerking
- regel 21: herberekening van het gemiddelde en de standaardafwijking
De resultaten van de uitvoering zijn als volgt:
In het vorige voorbeeld implementeren twee klassen de interface IStats. Dit gezegd zijnde, laat het voorbeeld niet zien wat het nut is van de interface IStats. Laten we het testprogramma als volgt herschrijven:
using System;
using System.Collections.Generic;
namespace Chap2 {
class Program2 {
static void Main(string[] args) {
// enkele leerlingen & cijfers voor Engels
Elève[] élèves1 = { new Elève { Prénom = "Paul", Nom = "Martin" }, new Elève { Prénom = "Maxime", Nom = "Germain" }, new Elève { Prénom = "Berthine", Nom = "Samin" } };
Note[] notes1 = { new Note { Elève = élèves1[0], Valeur = 14 }, new Note { Elève = élèves1[1], Valeur = 16 }, new Note { Elève = élèves1[2], Valeur = 18 } };
// die worden opgeslagen in een object TableauDeNotes
TableauDeNotes anglais = new TableauDeNotes("anglais", notes1);
// weergave van gemiddelde en standaardafwijking
AfficheStats(anglais);
// we plaatsen de leerlingen en het vak in een object ListeDeNotes
ListeDeNotes français = new ListeDeNotes("français", new List<Note>(notes1));
// weergave van gemiddelde en standaardafwijking
AfficheStats(français);
// er wordt een cijfer toegevoegd
français.Ajouter(new Note { Elève = new Elève { Prénom = "Jérôme", Nom = "Jaric" }, Valeur = 10 });
// weergave van gemiddelde en standaardafwijking
AfficheStats(français);
}
// weergave van gemiddelde en standaardafwijking van een type IStats
static void AfficheStats(IStats valeurs) {
Console.WriteLine("{2}, Moyenne={0}, Ecart-type={1}", valeurs.Moyenne, valeurs.EcartType(), valeurs);
}
}
}
- regels 25-27: de statische methode AfficheStats ontvangt als parameter een type IStats, dus een interfacetype. Dit betekent dat de daadwerkelijke parameter elk object kan zijn dat de interface IStats implementeert. Wanneer men een gegeven gebruikt met het type van een interface, betekent dit dat men alleen de methoden van de interface zal gebruiken die door het gegeven worden geïmplementeerd. De rest wordt buiten beschouwing gelaten. Dit is een eigenschap die lijkt op het polymorfisme dat we bij klassen hebben gezien. Als een verzameling klassen Ci die niet via overerving met elkaar verbonden zijn (waardoor we geen gebruik kunnen maken van overervingspolymorfisme) een verzameling methoden met dezelfde signatuur bevat, kan het interessant zijn om deze methoden te bundelen in een interface I die door alle betrokken klassen zou worden geïmplementeerd. Instanties van deze klassen Ci kunnen dan worden gebruikt als effectieve parameters van functies die een formele parameter van het type I, c.a.d, toelaten. Dit zijn functies die uitsluitend gebruikmaken van de methoden van de objecten Ci die in de interface I zijn gedefinieerd, en niet van de specifieke attributen en methoden van de verschillende klassen Ci.
- regel 13: de methode AfficheStats wordt aangeroepen met een type TableauDeNotes dat de interface IStats implementeert
- regel 17: idem met een type ListeDeNotes
De resultaten van de uitvoering zijn identiek aan die van de vorige.
Een variabele kan het type van een interface hebben. Zo kan men schrijven:
De declaratie op regel 1 geeft aan dat stats1 de instantie is van een klasse die de interface IStats implementeert. Deze declaratie houdt in dat de compiler in stats1 alleen toegang toestaat tot de methoden van de interface: de eigenschap Moyenne en de methode EcartType.
Tot slot merken we op dat interfaces meerdere keren kunnen worden geïmplementeerd, zoals c.a.d. Dit kan als volgt worden geschreven:
waarbij de Ij interfaces zijn.
4.7. Abstracte klassen
Een abstracte klasse is een klasse waarvan geen instantie kan worden gemaakt. Er moeten afgeleide klassen worden gemaakt, waarvan wel instanties kunnen worden gemaakt.
Abstracte klassen kunnen worden gebruikt om de code van een reeks klassen te hergroeperen. Laten we het volgende voorbeeld eens bekijken:
using System;
namespace Chap2 {
abstract class Utilisateur {
// velden
private string login;
private string motDePasse;
private string role;
// fabrikant
public Utilisateur(string login, string motDePasse) {
// de gegevens worden opgeslagen
this.login = login;
this.motDePasse = motDePasse;
// de gebruiker wordt geïdentificeerd
role=identifie();
// geïdentificeerd?
if (role == null) {
throw new ExceptionUtilisateurInconnu(String.Format("[{0},{1}]", login, motDePasse));
}
}
// toString
public override string ToString() {
return String.Format("Utilisateur[{0},{1},{2}]", login, motDePasse, role);
}
// identificeert
abstract public string identifie();
}
}
- regels 11-21: de constructor van de klasse Utilisateur. Deze klasse slaat informatie op over de gebruiker van een webapplicatie. Deze applicatie kent verschillende soorten gebruikers die worden geauthenticeerd via een gebruikersnaam en wachtwoord (regels 6-7). Deze twee gegevens worden voor sommige gebruikers gecontroleerd bij een service LDAP, voor andere bij een SGBD, enzovoort...
- regels 13-14: de authenticatiegegevens worden opgeslagen
- regel 16: deze worden gecontroleerd door een methode identifie. Omdat de identificatiemethode niet bekend is, wordt deze op regel 29 als abstract gedeclareerd met het sleutelwoord abstract. De methode identifie retourneert een tekenreeks die de rol van de gebruiker aangeeft (in grote lijnen wat hij mag doen). Als deze tekenreeks de pointer null is, wordt er op regel 19 een uitzondering gegenereerd.
- regel 4: omdat de klasse een abstracte methode heeft, wordt de klasse zelf als abstract gedeclareerd met het sleutelwoord `abstract`.
- regel 29: de abstracte methode identifie heeft geen definitie. Het zijn de afgeleide klassen die hier een definitie aan zullen geven.
- regels 24-26: de methode ToString, die een instantie van de klasse identificeert.
Hier wordt aangenomen dat de ontwikkelaar controle wil hebben over het aanmaken van instanties van de klasse Utilisateur en de daarvan afgeleide klassen, wellicht omdat hij er zeker van wil zijn dat er een uitzondering van een bepaald type wordt gegenereerd als de gebruiker niet wordt herkend (regel 19). Afgeleide klassen kunnen gebruikmaken van deze constructor. Hiervoor moeten ze de methode **identifie** implementeren.
De klasse ExceptionUtilisateurInconnu ziet er als volgt uit:
using System;
namespace Chap2 {
class ExceptionUtilisateurInconnu : Exception {
public ExceptionUtilisateurInconnu(string message) : base(message){
}
}
}
- regel 3: deze is afgeleid van de klasse Exception
- regels 4-6: deze heeft slechts één constructor die een foutmelding als parameter accepteert. Deze wordt doorgegeven aan de bovenliggende klasse (regel 5), die dezelfde constructor heeft.
We leiden nu de klasse Utilisateur af van de dochterklasse Administrateur:
namespace Chap2 {
class Administrateur : Utilisateur {
// fabrikant
public Administrateur(string login, string motDePasse)
: base(login, motDePasse) {
}
// identificeert
public override string identifie() {
// identificatie LDAP
// ...
return "admin";
}
}
}
- regels 4-6: de constructor geeft de ontvangen parameters gewoon door aan de bovenliggende klasse
- regels 9-12: de methode `identifie` van de klasse Administrateur. We nemen aan dat een beheerder wordt geïdentificeerd door een systeem met de naam LDAP. Deze methode herdefinieert de methode `identifie` van de bovenliggende klasse. Omdat het een abstracte methode herdefinieert, is het niet nodig om het sleutelwoord override. te gebruiken
We leiden nu de klasse Utilisateur af van de onderliggende klasse Observateur:
namespace Chap2 {
class Observateur : Utilisateur{
// fabrikant
public Observateur(string login, string motDePasse)
: base(login, motDePasse) {
}
//identificeert
public override string identifie() {
// identificatie SGBD
// ...
return "observateur";
}
}
}
- regels 4-6: de constructor geeft de ontvangen parameters gewoon door aan de bovenliggende klasse
- regels 9-13: de identificatiemethode van de klasse Observateur. We gaan ervan uit dat een waarnemer wordt geïdentificeerd door zijn identificatiegegevens in een database te controleren.
Uiteindelijk worden de objecten Administrateur en Observateur geïnstantieerd door dezelfde constructor, namelijk die van de bovenliggende klasse Utilisateur.. Deze constructor maakt gebruik van de methode identifie die deze klassen bieden.
Een derde klasse, Inconnu, is eveneens afgeleid van de klasse Utilisateur:
namespace Chap2 {
class Inconnu : Utilisateur{
// fabrikant
public Inconnu(string login, string motDePasse)
: base(login, motDePasse) {
}
//identificatie
public override string identifie() {
// gebruiker onbekend
// ...
return null;
}
}
}
- regel 13: de methode identifie retourneert de pointer null om aan te geven dat de gebruiker niet is herkend.
Een testprogramma zou er als volgt uit kunnen zien:
using System;
namespace Chap2 {
class Program {
static void Main(string[] args) {
Console.WriteLine(new Observateur("observer","mdp1"));
Console.WriteLine(new Administrateur("admin", "mdp2"));
try {
Console.WriteLine(new Inconnu("xx", "yy"));
} catch (ExceptionUtilisateurInconnu e) {
Console.WriteLine("Utilisateur non connu : "+ e.Message);
}
}
}
}
Merk op dat in de regels 6, 7 en 9 de methode [Utilisateur].ToString() wordt gebruikt door de methode WriteLine.
De resultaten van de uitvoering zijn als volgt:
4.8. Klassen, interfaces, generieke methoden
Stel dat we een methode willen schrijven die twee gehele getallen van plaats verwisselt. Deze methode zou er als volgt uit kunnen zien:
public static void Echanger1(ref int value1, ref int value2){
// de referenties value1 en value2 worden omgewisseld
int temp = value2;
value2 = value1;
value1 = temp;
}
Als we nu twee verwijzingen naar objecten Personne zouden willen omwisselen, zouden we het volgende schrijven:
public static void Echanger2(ref Personne value1, ref Personne value2){
// we wisselen de referenties value1 en value2 om
Personne temp = value2;
value2 = value1;
value1 = temp;
}
Wat de twee methoden van elkaar onderscheidt, is het type T van de parameters: int in plaats van Echanger1, Personne in plaats van Echanger2. Generieke klassen en interfaces voorzien in de behoefte aan methoden die alleen verschillen in het type van sommige van hun parameters.
Met een generieke klasse zou de methode Echanger als volgt herschreven kunnen worden:
namespace Chap2 {
class Generic1<T> {
public static void Echanger(ref T value1, ref T value2){
// de referenties value1 en value2 worden omgewisseld
T temp = value2;
value2 = value1;
value1 = temp;
}
}
}
- regel 2: de klasse Generic1 wordt gedefinieerd met een type aangeduid als T. Dit type kan een willekeurige naam krijgen. Dit type T wordt vervolgens hergebruikt in de klasse op regel 3 en 5. We zeggen dat de klasse Generic1 een generieke klasse is.
- regel 3: definieert de twee verwijzingen naar een type T die moeten worden verwisseld
- regel 5: de tijdelijke variabele temp heeft het type T.
Een testprogramma voor de klasse zou er als volgt uit kunnen zien:
using System;
namespace Chap2 {
class Program {
static void Main(string[] args) {
// int
int i1 = 1, i2 = 2;
Generic1<int>.Echanger(ref i1, ref i2);
Console.WriteLine("i1={0},i2={1}", i1, i2);
// string
string s1 = "s1", s2 = "s2";
Generic1<string>.Echanger(ref s1, ref s2);
Console.WriteLine("s1={0},s2={1}", s1, s2);
// Person
Personne p1 = new Personne("jean", "clu", 20), p2 = new Personne("pauline", "dard", 55);
Generic1<Personne>.Echanger(ref p1, ref p2);
Console.WriteLine("p1={0},p2={1}", p1, p2);
}
}
}
- regel 8: wanneer een generieke klasse wordt gebruikt die is geparametriseerd met de typen T1, T2, ... moeten deze worden "geïnstantieerd". In regel 8 wordt de statische methode Echanger van het type Generic1<int> gebruikt om aan te geven dat de verwijzingen die aan de methode Echanger worden doorgegeven, van het type int zijn.
- regel 12: we gebruiken de statische methode Echanger van het type Generic1<string> om aan te geven dat de verwijzingen die aan de methode Echanger worden doorgegeven, van het type string zijn.
- regel 16: de statische methode Echanger van het type Generic1<Personne> wordt gebruikt om aan te geven dat de verwijzingen die aan de methode Echanger worden doorgegeven, van het type Personne zijn.
De resultaten van de uitvoering zijn als volgt:
De methode Echanger had ook als volgt geschreven kunnen worden:
namespace Chap2 {
class Generic2 {
public static void Echanger<T>(ref T value1, ref T value2){
// de verwijzingen value1 en value2 worden verwisseld
T temp = value2;
value2 = value1;
value1 = temp;
}
}
}
- regel 2: de klasse Generic2 is niet langer generiek
- regel 3: de statische methode Echanger is generiek
Het testprogramma ziet er dan als volgt uit:
using System;
namespace Chap2 {
class Program2 {
static void Main(string[] args) {
// int
int i1 = 1, i2 = 2;
Generic2.Echanger<int>(ref i1, ref i2);
Console.WriteLine("i1={0},i2={1}", i1, i2);
// string
string s1 = "s1", s2 = "s2";
Generic2.Echanger<string>(ref s1, ref s2);
Console.WriteLine("s1={0},s2={1}", s1, s2);
// Persoon
Personne p1 = new Personne("jean", "clu", 20), p2 = new Personne("pauline", "dard", 55);
Generic2.Echanger<Personne>(ref p1, ref p2);
Console.WriteLine("p1={0},p2={1}", p1, p2);
}
}
}
- regels 8, 12 en 16: de methode Echanger wordt aangeroepen, waarbij in <> het type van de parameters wordt gespecificeerd. In feite kan de compiler op basis van het type van de daadwerkelijke parameters afleiden welke variant van de methode Echanger moet worden gebruikt. Daarom is de volgende schrijfwijze toegestaan:
Generic2.Echanger(ref i1, ref i2);
...
Generic2.Echanger(ref s1, ref s2);
...
Generic2.Echanger(ref p1, ref p2);
Regels 1, 3 en 5: de aangeroepen variant van de methode Echanger wordt niet meer gespecificeerd. De compiler kan deze afleiden uit de aard van de daadwerkelijk gebruikte parameters.
Er kunnen beperkingen worden opgelegd aan de generieke parameters:

Laten we eens kijken naar de volgende nieuwe generieke methode Echanger:
namespace Chap2 {
class Generic3 {
public static void Echanger<T>(ref T value1, ref T value2) where T : class {
// de verwijzingen value1 en value2 worden verwisseld
T temp = value2;
value2 = value1;
value1 = temp;
}
}
}
- regel 3: er wordt vereist dat het type T een referentie is (klasse, interface)
Laten we het volgende testprogramma eens bekijken:
using System;
namespace Chap2 {
class Program4 {
static void Main(string[] args) {
// int
int i1 = 1, i2 = 2;
Generic3.Echanger<int>(ref i1, ref i2);
Console.WriteLine("i1={0},i2={1}", i1, i2);
// string
string s1 = "s1", s2 = "s2";
Generic3.Echanger(ref s1, ref s2);
Console.WriteLine("s1={0},s2={1}", s1, s2);
// Persoon
Personne p1 = new Personne("jean", "clu", 20), p2 = new Personne("pauline", "dard", 55);
Generic3.Echanger(ref p1, ref p2);
Console.WriteLine("p1={0},p2={1}", p1, p2);
}
}
}
De compiler meldt een fout op regel 8 omdat het type int geen klasse of interface is, maar een structuur:

Laten we de volgende nieuwe generieke methode Echanger eens bekijken:
namespace Chap2 {
class Generic4 {
public static void Echanger<T>(ref T element1, ref T element2) where T : Interface1 {
// de waarden van de 2 elementen worden opgehaald
int value1 = element1.Value();
int value2 = element2.Value();
// als het eerste element > het tweede element is, worden de elementen omgewisseld
if (value1 > value2) {
T temp = element2;
element2 = element1;
element1 = temp;
}
}
}
}
- regel 3: het type T moet de interface Interface1 implementeren. Deze heeft een methode Value, die op regel 5 en 6 wordt gebruikt, en die de waarde van het object van het type T retourneert.
- regels 8-12: de twee verwijzingen element1 en element2 worden alleen uitgewisseld als de waarde van element1 groter is dan de waarde van element2.
De interface Interface1 is als volgt:
namespace Chap2 {
interface Interface1 {
int Value();
}
}
Deze wordt geïmplementeerd door de volgende klasse Class1:
using System;
using System.Threading;
namespace Chap2 {
class Class1 : Interface1 {
// waarde van het object
private int value;
// constructor
public Class1() {
// 1 ms wachten
Thread.Sleep(1);
// willekeurige waarde tussen 0 en 99
value = new Random(DateTime.Now.Millisecond).Next(100);
}
// accessor voor het privéveld ‘value’
public int Value() {
return value;
}
// status van de instantie
public override string ToString() {
return value.ToString();
}
}
}
- regel 5: Class1 implementeert de interface Interface1
- regel 7: de waarde van een instantie van Class1
- regels 10-14: het veld value wordt geïnitialiseerd met een willekeurige waarde tussen 0 en 99
- regels 18-20: de methode Value van de interface Interface1
- regels 23-25: de methode ToString van de klasse
De interface Interface1 wordt ook geïmplementeerd door de klasse Class2:
using System;
namespace Chap2 {
class Class2 : Interface1 {
// waarden van het object
private int value;
private String s;
// constructor
public Class2(String s) {
this.s = s;
value = s.Length;
}
// accessor voor de waarde van een privéveld
public int Value() {
return value;
}
// status van de instantie
public override string ToString() {
return s;
}
}
}
- regel 4: Class2 implementeert de interface Interface1
- regel 6: de waarde van een instantie van Class2
- regels 10-13: het veld value wordt geïnitialiseerd met de lengte van de tekenreeks die aan de constructor is doorgegeven
- regels 16-18: de methode Value van de interface Interface1
- regels 21-22: de methode ToString van de klasse
Een testprogramma zou er als volgt uit kunnen zien:
using System;
namespace Chap2 {
class Program5 {
static void Main(string[] args) {
// instanties van het type Class1 uitwisselen
Class1 c1, c2;
for (int i = 0; i < 5; i++) {
c1 = new Class1();
c2 = new Class1();
Console.WriteLine("Avant échange --> c1={0},c2={1}", c1, c2);
Generic4.Echanger(ref c1, ref c2);
Console.WriteLine("Après échange --> c1={0},c2={1}", c1, c2);
}
// instanties van het type Class2 uitwisselen
Class2 c3, c4;
c3 = new Class2("xxxxxxxxxxxxxx");
c4 = new Class2("xx");
Console.WriteLine("Avant échange --> c3={0},c4={1}", c3, c4);
Generic4.Echanger(ref c3, ref c4);
Console.WriteLine("Avant échange --> c3={0},c4={1}", c3, c4);
}
}
}
- regels 8-14: er worden instanties van het type Class1 uitgewisseld
- regels 16-22: er worden instanties van het type Class2 uitgewisseld
De resultaten van de uitvoering zijn als volgt:
Om het concept van een generieke interface ( ) te illustreren, gaan we een array met personen eerst op naam en vervolgens op leeftijd sorteren. De methode waarmee we een array kunnen sorteren, is de statische methode Sort van de klasse Array:

Ter herinnering: een statische methode wordt gebruikt door de naam van de klasse als voorvoegsel voor de methode te plaatsen, en niet die van een instantie van de klasse. De methode Sort heeft verschillende signaturen (ze is overladen). We zullen de volgende signatuur gebruiken:
Sort is een generieke methode waarbij T een willekeurig type aangeeft. De methode ontvangt twee parameters:
- T[] array: de array met elementen van het type T die gesorteerd moeten worden
- IComparer<T> vergelijkingsfunctie: een objectreferentie die de interface IComparer<T> implementeert.
IComparer<T> is een generieke interface die als volgt is gedefinieerd:
De interface IComparer<T> heeft slechts één methode. De methode Compare:
- ontvangt als parameters twee elementen t1 en t2 van het type T
- retourneert 1 als t1>t2, 0 als t1==t2, -1 als t1<t2. Het is aan de ontwikkelaar om betekenis te geven aan de operatoren <, ==, >. Als p1 en p2 bijvoorbeeld twee objecten van het type Personne zijn, kan men zeggen dat p1 > p2 geldt als de naam van p1 in alfabetische volgorde vóór de naam van p2 komt. We krijgen dan een oplopende sortering op basis van de namen van de personen. Als we een sortering op leeftijd willen, zeggen we dat p1 > p2 is als de leeftijd van p1 hoger is dan die van p2.
- Om een aflopende sortering te krijgen, hoeven we alleen maar de resultaten +1 en -1 om te draaien
We weten nu genoeg om een array met personen te sorteren. Het programma ziet er als volgt uit:
using System;
using System.Collections.Generic;
namespace Chap2 {
class Program6 {
static void Main(string[] args) {
// een array van personen
Personne[] personnes1 = { new Personne("claude", "pollon", 25), new Personne("valentine", "germain", 35), new Personne("paul", "germain", 32) };
// weergave
Affiche("Tableau à trier", personnes1);
// sorteren op naam
Array.Sort(personnes1, new CompareNoms());
// weergave
Affiche("Tableau après le tri selon les nom et prénom", personnes1);
// sorteren op leeftijd
Array.Sort(personnes1, new CompareAges());
// weergave
Affiche("Tableau après le tri selon l'âge", personnes1);
}
static void Affiche(string texte, Personne[] personnes) {
Console.WriteLine(texte.PadRight(50, '-'));
foreach (Personne p in personnes) {
Console.WriteLine(p);
}
}
}
// vergelijkingsklasse van de voor- en achternamen van personen
class CompareNoms : IComparer<Personne> {
public int Compare(Personne p1, Personne p2) {
// vergelijking van achternamen
int i = p1.Nom.CompareTo(p2.Nom);
if (i != 0)
return i;
// overeenstemming van achternamen – de voornamen worden vergeleken
return p1.Prenom.CompareTo(p2.Prenom);
}
}
// vergelijkingsklasse voor de leeftijden van personen
class CompareAges : IComparer<Personne> {
public int Compare(Personne p1, Personne p2) {
// de leeftijden worden vergeleken
if (p1.Age > p2.Age)
return 1;
else if (p1.Age == p2.Age)
return 0;
else
return -1;
}
}
}
- regel 8: de array met personen
- regel 12: de sortering van de tabel met personen op voor- en achternaam. De tweede parameter van de generieke methode Sort is een instantie van een klasse CompareNoms die de generieke interface IComparer<Person> implementeert.
- regels 30-39: de klasse CompareNoms die de generieke interface IComparer<Persoon> implementeert.
- regels 31-38: implementatie van de generieke methode int CompareTo(T,T) van de interface IComparer<T>. De methode maakt gebruik van de methode String.CompareTo, beschreven in paragraaf 3.3.5.4, om twee tekenreeksen te vergelijken.
- regel 16: het sorteren van de tabel met personen op leeftijd. De tweede parameter van de generieke methode Sort is een instantie van de klasse CompareAges, die de generieke interface IComparer<Person> implementeert en gedefinieerd is in de regels 42-51.
De resultaten van de uitvoering zijn als volgt:
4.9. Naamruimten
Om een regel op het scherm weer te geven, gebruiken we de instructie
Als we kijken naar de definitie van de klasse Console
Namespace: System
Assembly: Mscorlib (in Mscorlib.dll)
dan zien we dat deze deel uitmaakt van de naamruimte System. Dit betekent dat de klasse Console aangeduid zou moeten worden met System.Console en dat we in feite het volgende zouden moeten schrijven:
Dit kan worden voorkomen door een using-clausule te gebruiken:
We zeggen dat we de naamruimte System importeren met de clausule using. Wanneer de compiler de naam van een klasse tegenkomt (hier Console), zal hij deze zoeken in de verschillende naamruimten die zijn geïmporteerd via de clausules using. Hier vindt hij de klasse Console in de naamruimte System. Laten we nu eens kijken naar de tweede informatie die aan de klasse Console is gekoppeld:
Deze regel geeft aan in welke „assemblage“ de definitie van de klasse Console zich bevindt. Wanneer men buiten Visual Studio compileert en de verwijzingen moet opgeven van de verschillende dll-bestanden die de te gebruiken klassen bevatten, kan deze informatie nuttig zijn. Om te verwijzen naar de dll-bestanden die nodig zijn voor het compileren van een klasse, schrijft men:
waarbij csc de C#-compiler is. Wanneer je een klasse aanmaakt, kun je deze binnen een naamruimte aanmaken. Het doel van deze naamruimten is om naamconflicten tussen klassen te voorkomen, bijvoorbeeld wanneer deze worden verkocht. Laten we twee bedrijven nemen, E1 en E2, die respectievelijk klassen distribueren die zijn verpakt in de dll, e1.dll en e2.dll. Stel dat een klant C deze twee sets klassen koopt, waarin beide bedrijven een klasse Personne hebben gedefinieerd. Klant C compileert een programma als volgt:
Als de broncode prog.cs gebruikmaakt van de klasse Personne, weet de compiler niet of hij de klasse Personne uit e1.dll of die uit e2.dll moet nemen. Hij zal een foutmelding geven. Als het bedrijf E1 ervoor zorgt dat het zijn klassen aanmaakt in een naamruimte met de naam E1 en het bedrijf E2 in een naamruimte met de naam E2, zullen de twee klassen Personne dan E1.Personne en E2.Personne heten. De klant moet in zijn klassen ofwel E1.Personne, ofwel E2.Personne gebruiken, maar niet Personne. De naamruimte maakt het mogelijk om de dubbelzinnigheid weg te nemen.
Om een klasse in een naamruimte aan te maken, schrijft men:
4.10. Voorbeeldtoepassing - V2
We nemen de belastingberekening die we al in het vorige hoofdstuk, paragraaf 3.6, hebben behandeld, opnieuw onder de loep en passen deze nu toe met behulp van klassen en interfaces. Laten we het probleem nog eens op een rijtje zetten:
We willen een programma schrijven waarmee de belasting van een belastingplichtige kan worden berekend. We gaan uit van het vereenvoudigde geval van een belastingplichtige die alleen zijn salaris hoeft aan te geven (cijfers uit 2004 voor inkomsten uit 2003):
- het aantal aandelen van de werknemer wordt berekend als nbParts = nbEnfants/2 + 1 als hij ongehuwd is, nbEnfants/2+2 als hij getrouwd is, waarbij nbEnfants het aantal kinderen is.
- als hij ten minste drie kinderen heeft, krijgt hij een half deel extra
- zijn belastbaar inkomen wordt berekend als R = 0,72 * S, waarbij S zijn jaarsalaris is
- we berekenen zijn gezinscoëfficiënt QF = R / nbParts
- zijn belasting I wordt berekend. Laten we de volgende tabel bekijken:
4262 | 0 | 0 |
8382 | 0,0683 | 291,09 |
14753 | 0,1914 | 1322,92 |
23888 | 0,2826 | 2668,39 |
38868 | 0,3738 | 4846,98 |
47932 | 0,4262 | 6883,66 |
0 | 0,4809 | 9505,54 |
Elke regel heeft 3 velden. Om belasting I te berekenen, zoeken we de eerste regel waar QF <= veld1. Als bijvoorbeeld QF = 5000 is, vinden we de regel
Belasting I is dan gelijk aan 0,0683*R - 291,09*nbParts. Als QF zodanig is dat de relatie QF<=veld1 nooit geldt, dan worden de coëfficiënten van de laatste regel gebruikt. In dit geval:
wat de belasting I = 0,4809*R - 9505,54*nbParts oplevert.
Allereerst definiëren we een structuur die een rij uit de vorige tabel kan omvatten:
namespace Chap2 {
// een belastingschijf
struct TrancheImpot {
public decimal Limite { get; set; }
public decimal CoeffR { get; set; }
public decimal CoeffN { get; set; }
}
}
Vervolgens definiëren we een interface IImpot waarmee de belasting kan worden berekend:
namespace Chap2 {
interface IImpot {
int calculer(bool marié, int nbEnfants, int salaire);
}
}
- regel 3: de methode voor het berekenen van de belasting op basis van drie gegevens: de burgerlijke staat van de belastingplichtige, het aantal kinderen en het salaris
Vervolgens definiëren we een abstracte klasse die deze interface implementeert:
namespace Chap2 {
abstract class AbstractImpot : IImpot {
// de belastingklassen die nodig zijn voor de berekening van de belasting
// zijn afkomstig van een externe bron
protected TrancheImpot[] tranchesImpot;
// berekening van de belasting
public int calculer(bool marié, int nbEnfants, int salaire) {
// berekening van het aantal aandelen
decimal nbParts;
if (marié) nbParts = (decimal)nbEnfants / 2 + 2;
else nbParts = (decimal)nbEnfants / 2 + 1;
if (nbEnfants >= 3) nbParts += 0.5M;
// berekening van het belastbaar inkomen en het gezinsquotiënt
decimal revenu = 0.72M * salaire;
decimal QF = revenu / nbParts;
// berekening van de belasting
tranchesImpot[tranchesImpot.Length - 1].Limite = QF + 1;
int i = 0;
while (QF > tranchesImpot[i].Limite) i++;
// resultaat weergeven
return (int)(revenu * tranchesImpot[i].CoeffR - nbParts * tranchesImpot[i].CoeffN);
}//berekenen
}//klasse
}
- regel 2: de klasse AbstractImpot implementeert de interface IImpot.
- regel 7: de jaarlijkse gegevens voor de belastingberekening in de vorm van een beschermd veld. De klasse AbstractImpot weet niet hoe dit veld moet worden geïnitialiseerd. Zij laat dit over aan de afgeleide klassen. Daarom wordt zij als abstract gedeclareerd (regel 2) om elke instantiëring ervan te verbieden.
- regels 10-25: de implementatie van de methode calculer van de interface IImpot. De afgeleide klassen hoeven deze methode niet te herschrijven. De klasse AbstractImpot dient dus als factorisatieklasse voor de afgeleide klassen. Hierin wordt alles ondergebracht wat alle afgeleide klassen gemeen hebben.
Een klasse die de interface IImpot implementeert, kan worden geconstrueerd door af te leiden van de klasse AbstractImpot. Dat gaan we nu doen:
using System;
namespace Chap2 {
class HardwiredImpot : AbstractImpot {
// tabel met gegevens die nodig zijn voor de berekening van de belasting
decimal[] limites = { 4962M, 8382M, 14753M, 23888M, 38868M, 47932M, 0M };
decimal[] coeffR = { 0M, 0.068M, 0.191M, 0.283M, 0.374M, 0.426M, 0.481M };
decimal[] coeffN = { 0M, 291.09M, 1322.92M, 2668.39M, 4846.98M, 6883.66M, 9505.54M };
public HardwiredImpot() {
// opstellen van de belastingschijven
tranchesImpot = new TrancheImpot[limites.Length];
// invullen
for (int i = 0; i < tranchesImpot.Length; i++) {
tranchesImpot[i] = new TrancheImpot { Limite = limites[i], CoeffR = coeffR[i], CoeffN = coeffN[i] };
}
}
}// klasse
}// naamruimte
De klasse HardwiredImpot definieert in de regels 7-9 de gegevens die nodig zijn voor de berekening van de belasting. De constructor (regels 11-18) gebruikt deze gegevens om het beschermde veld tranchesImpot van de bovenliggende klasse AbstractImpot te initialiseren.
Een testprogramma zou er als volgt uit kunnen zien:
using System;
namespace Chap2 {
class Program {
static void Main() {
// interactief programma voor belastingberekening
// de gebruiker voert drie gegevens in via het toetsenbord: gehuwd nbEnfants salaris
// het programma geeft vervolgens de te betalen belasting weer
const string syntaxe = "syntaxe : Marié NbEnfants Salaire\n"
+ "Marié : o pour marié, n pour non marié\n"
+ "NbEnfants : nombre d'enfants\n"
+ "Salaire : salaire annuel en F";
// aanmaken van een object IImpot
IImpot impot = new HardwiredImpot();
// oneindige lus
while (true) {
// er wordt gevraagd om de parameters voor de belastingberekening
Console.Write("Paramètres du calcul de l'Impot au format : Marié (o/n) NbEnfants Salaire ou rien pour arrêter :");
string paramètres = Console.ReadLine().Trim();
// moet er iets gebeuren?
if (paramètres == null || paramètres == "") break;
// controle van het aantal argumenten in de ingevoerde regel
string[] args = paramètres.Split(null);
int nbParamètres = args.Length;
if (nbParamètres != 3) {
Console.WriteLine(syntaxe);
continue;
}//if
// controle van de geldigheid van de parameters
// getrouwd
string marié = args[0].ToLower();
if (marié != "o" && marié != "n") {
Console.WriteLine(syntaxe + "\nArgument marié incorrect : tapez o ou n");
continue;
}//if
// nbEnfants
int nbEnfants = 0;
bool dataOk = false;
try {
nbEnfants = int.Parse(args[1]);
dataOk = nbEnfants >= 0;
} catch {
}//if
// gegevens correct?
if (!dataOk) {
Console.WriteLine(syntaxe + "\nArgument NbEnfants incorrect : tapez un entier positif ou nul");
continue;
}
// salaris
int salaire = 0;
dataOk = false;
try {
salaire = int.Parse(args[2]);
dataOk = salaire >= 0;
} catch {
}//try-catch
// zijn de gegevens correct?
if (!dataOk) {
Console.WriteLine(syntaxe + "\nArgument salaire incorrect : tapez un entier positif ou nul");
continue;
}
// de parameters zijn correct – de belasting wordt berekend
Console.WriteLine("Impot=" + impot.calculer(marié == "o", nbEnfants, salaire) + " euros");
// volgende belastingplichtige
}//while
}
}
}
Met het bovenstaande programma kan de gebruiker herhaaldelijk simulaties van belastingberekeningen uitvoeren.
- regel 16: aanmaken van een object impot dat de interface IImpot implementeert. Dit object wordt verkregen door een instantie te maken van het type HardwiredImpot, een type dat de interface IImpot implementeert. Merk op dat aan de variabele impot niet het type HardwiredImpot is toegekend, maar het type IImpot. Hiermee geven we aan dat we alleen geïnteresseerd zijn in de methode calculer van het object impot en niet in de rest.
- regels 19-68: de lus voor de simulaties van de belastingberekening
- regel 22: de drie parameters die nodig zijn voor de methode calculer worden in één regel via het toetsenbord ingevoerd.
- regel 26: met de methode [chaine].Split(null) wordt [chaine] in woorden opgesplitst. Deze worden opgeslagen in een array args.
- regel 66: aanroep van de methode calculer van het object impot, dat de interface IImpot implementeert.
Hier volgt een voorbeeld van de uitvoering van het programma:









