Overzicht modifiers
Je kent ondertussen al heel wat modifiers voor classes en members. Een overzicht van de belangrijkste:
modifier | betekenis | |
---|---|---|
beschikbaarheid | public |
overal beschikbaar |
internal |
enkel beschikbaar binnen de namespace | |
protected |
enkel beschikbaar binnen de class en overgeërfde classes | |
private |
enkel beschikbaar binnen de class zelf | |
overerving | abstract |
moet overschreven worden in overgeërfde klasse om gebruikt te kunnen worden |
virtual |
mag overschreven worden in overgeërfde klasse | |
sealed |
kan niet meer overschreven worden in overgeërfde klasse | |
override |
overschrijft een member van een hogere klasse | |
instantialisatie / initialisatie |
static |
class: aanmaken objecten niet mogelijk; member: opgeroepen vanop de class |
const |
krijgt zijn definitieve waarde bij het begin van het programma | |
readonly |
krijgt zijn definitieve waarde in de loop van het programma |
Klassen
Klasse definitie
Een class is een blauwdruk voor objecten; het bevat class members, i.e. variabelen, properties, constructors en methods. Schematisch voorbeeld:
public class VeilingItem {
// private variables
private decimal minimumBod;
private List<decimal> Biedingen = new List<decimal>();
// public properties
public string Naam { get; set; }
public string Beschrijving { get; set; }
public decimal HoogsteBod { get { return Biedingen.Max(); } }
// public constructors
public VeilingItem() { }
public VeilingItem(string vn, string an) {
this.Naam = vn;
this.Beschrijving = an;
}
// public ToString() implementation
public override string ToString() {
return $"{Naam} (hoogste bod: {HoogsteBod})";
}
// public and private methods
public bool VerwerkBod(decimal bod) {
if (bod < minimumBod) return false;
Biedingen.Add(bod);
return true;
}
}
Associatie (compositie & aggregatie)
Bij associatie gebruik je bestaande klassen in je nieuwe klasse ("heeft een..."):
class Room {
public string Name { get; set; }
public string Description { get; set; }
public List<Item> Items { get; set; } = new List<Item>(); // aggregatie: item kan bestaan zonder room
public List<Door> Doors { get; set; } = new List<Door>(); // compositie: door kan niet bestaan zonder room
public string Image { get; }
}
Er zijn twee soorten associaties:
- aggregatie: A kan wel bestaan zonder B, b.v. Items in Rooms is een aggregatie (item kan bestaan zonder ruimte)
- compositie: A kan niet bestaan zonder B, b.v. Doors in Rooms is een compositie (deur kan niet bestaan zonder ruimte)
Properties
Een property is als een variabele, maar met meer controle over hoe waarden ingesteld (get) of gelezen (set) worden. Enkele variaties:
class Customer {
// automatische get/set properties
public string FirstName { get; set; }
public string LastName { get; set; }
// property met enkel getter: read-only
public string NameAndEmail {
get { return $"{FirstName} {LastName} <{Email}>"; }
}
// publieke get, private set (waarde instellen kan enkel binnen de class)
public int ClientId { get; private set; }
// property met validatie
private MailAddress _email;
public string Email {
get { return _email.Address; }
set {
try { MailAddress email = new MailAddress(value); }
catch (FormatException) { throw new ArgumentException($"ongeldige email"); }
}
}
}
Static
static members
class Account {
// static members
private static string rexAccount = @"^\d\d\d-\d\d\d\d\d\d\d-\d\d$";
public static int NumCreated { get; private set; } = 0;
public static bool IsValidNumber(string nr) {
return Regex.Match(nr, rexAccount).Success;
}
// non-static members
public decimal Balance { get; set; } = 0;
public string Number { get; set; }
public void Deposit(decimal amount) {
Balance += amount;
}
public Account(string number) {
NumCreated++;
Number = number;
}
}
- static: opgeroepen vanop de klasse
Gebruikt in een programma:
// create some accounts
Account acc1 = new Account("123-456789-012");
Account acc2 = new Account("234-567890-123");
Account acc3 = new Account("345-678901-234");
// static examples
string accNr = "456-789012";
Console.WriteLine($"{accNr} is {(Account.IsValidNumber(accNr) ? "valid" : "invalid")}");
Console.WriteLine($"{Account.NumCreated} accounts created");
// non-static examples
acc1.Deposit(100);
acc2.Deposit(300);
Console.WriteLine($"Account {acc1.Number} has balance {acc1.Balance}");
static classes
Een static class is een klasse met alleen static members.
Ze groeperen doorgaans functionaliteit die thematisch bij elkaar hoort, zonder echt de blauwdruk van een object voor te stellen.
Zo zou je bijvoorbeeld functionaliteit voor het werken met HTML code kunnen onderbrengen in een statische class MyHtmlFunctions
:
static class MyHtmlFunctions {
public static Regex RexHtmlTag { get; } = new Regex(@"<[^>]+>");
public static string[] TagsList { get; } = { "A", "ABBR", ... };
public static List<string> GetLayoutErrors(string htmlCode) { ... }
public static List<string> GetValidationErrors(string htmlCode) { ... }
public static string ReplaceTags(string htmlCode, string tag1, string tag2) { ... }
public static string HtmlEntities(string htmlCode) { ... }
...
}
string code = "<!DOCTYPE html><html...";
List<string> errs = MyHtmlFunctions.GetValidationErrors(html);
html = MyHtmlFunctions.ReplaceTags(html, "B", "STRONG");
string safeText = MyHtmlFunctions.HtmlEntities("...unsafe text here...");
bool tagExists = MyHtmlFunctions.TagsList.Contains("BIG");
...
Overerving
Afgeleide klassen
Bij overerving breid je een bestaande klasse uit en/of pas je het aan tot een nieuwe klasse ("is een..."). Nemen we volgende basisklasse:
// basisklasse Persoon
internal class Persoon {
public string Name { get; set; }
public string Adres { get; set; }
}
Twee voorbeelden van afgeleide (overgeërfde) klassen:
// overgeërfde klasse Klant; heeft de property Loon en de overgeërfde properties Naam en Adres
internal class Klant : Persoon {
public decimal Loon { get; set; }
}
// overgeërfde klasse Werknemer; heeft de property KlantNr en de overgeërfde properties Naam en Adres
internal class Werknemer : Persoon {
public string KlantNr { get; set; }
}
- overerving beschrijft een “is een” relatie, bv. een
Klant
is eenPersoon
is
en as
is controleert of een object tot een subklasse behoort, as cast het naar een subklasse; samenvattend voorbeeld:
// basisklasse Persoon
internal class Persoon {
public string Name { get; set; }
public string Adres { get; set; }
}
// overgeërfde klasse Klant
internal class Klant : Persoon {
public decimal Loon { get; set; }
}
// overgeërfde klasse Werknemer
internal class Werknemer : Persoon {
public string KlantNr { get; set; }
}
// maak lijst personen aan
List<Persoon> personen = new List<Persoon>();
// voeg klanten en werknemers toe
personen.Add(new Werknemer() { Name = "Amir", BadgeNr = 2387 });
personen.Add(new Klant() { Name = "Bernard", KlantNr = 466123 });
personen.Add(new Klant() { Name = "Chloë", KlantNr = 466123 });
// verloop personen en druk gegevens af
foreach (Persoon p in personen) {
if (p is Werknemer) { // controleer of p Werknemer is
Werknemer w = p as Werknemer; // zoja, cast naar Werknemer
Console.WriteLine($"Werknemer met badge #{w.BadgeNr}: {w.Name}"); // nu kan je BadgeNr gebruiken
}
if (p is Klant) { // controleer of p Klant is
Klant k = p as Klant; // zoja, cast naar Klant
Console.WriteLine($"Klant met nummer #{k.KlantNr}: {k.Name}"); // nu kan je KlantNr gebruiken
}
}
base
Het sleutelwoord base verwijst expliciet naar een member uit de superklasse:
class Persoon {
public virtual void GeefBeschrijving() {
Console.WriteLine("ik ben een persoon");
}
}
class Klant : Persoon {
public override void GeefBeschrijving() {
base.GeefBeschrijving(); // voer eerst GeefBeschrijving() uit Persoon uit
Console.WriteLine("ik ben een klant"); // voer dan deze regel uit
}
}
Met base kan je vanuit Klant
de methode GeefBeschrijving()
van de basisklasse Persoon
oproepen. Gebruikt in een programma:
Klant k = new Klant();
k.GeefBeschrijving();
Constructors
Een constructor zegt hoe een object uit een klasse kan gecreëerd worden.
this()
Met this() kan je daarbij eerst een andere constructor uitvoeren in dezelfde klasse.
Customer cust1 = new Customer();
Customer cust2 = new Customer("Johnny","Miles");
Customer cust3 = new Customer(
"Johnny",
"Miles",
new DateTime(2024, 03, 22)
);
// default constructor
public Customer() {
registerDate = DateTime.Now;
}
// constructor waarnaar verwezen wordt
public Customer(string fn, string ln) {
registerDate = DateTime.Now;
FirstName = fn;
LastName = ln;
Rating = (new Random()).Next(1, 6);
}
// constructor met parameters; this(fn, ln) verwijst naar de constructor hierboven
public Customer(string fn, string ln, int rt) : this(fn, ln) {
Rating = rt;
}
standaardconstructor
Als in een klasse geen constructor gedefinieerd is, wordt impliciet een lege parameterloze standaardconstructor toegevoegd. Beide fragmenten zijn dus equivalent:
class Persoon {
}
class Persoon {
public Persoon() { }
}
Gebruik van de lege constructor:
Persoon p2 = new Persoon(); // OK: impliciete lege standaardconstructor gebruikt
Als een klasse wél een constructor bevat, dan vervalt deze standaardconstructor:
class Persoon {
public string Naam { get; set; }
public Persoon(string nm) {
Naam = nm;
}
}
Persoon p1 = new Persoon("Bob"); // OK
Persoon p2 = new Persoon(); // FOUT: lege constructor bestaat niet
Je kan uiteraard wel zelf een constructor zonder parameters voorzien:
class Persoon {
public string Naam { get; set; }
public Persoon(string nm) {
Naam = nm;
}
// voorzie zelf een lege constructor
public Persoon() {
Naam = "Onbekend";
}
}
Persoon p1 = new Persoon("Bob"); // OK
Persoon p2 = new Persoon(); // OK: lege constructor bestaat
constructors bij overerving
Bij overerving moet altijd eerst een basisconstructor opgeroepen worden. Dit kan handmatig met base()
, waarna de afgeleide constructor uitgevoerd wordt:
// hoofdprogramma
Klant k1 = new Klant("Bob", "U0066540");
class Persoon {
public string Name { get; set; }
// lege constructor
public Persoon() {
Name = "onbekend";
Console.WriteLine("constructor 1 van Persoon...");
}
// deze niet-lege constructor wordt eerst uitgevoerd
public Persoon(string nm) {
Name = nm;
Console.WriteLine("constructor 2 van Persoon...");
}
}
class Klant : Persoon {
public string KlantNr { get; set; }
// voer eerst base constructor van Persoon uit
public Klant(string nm, string nr) : base(nm) {
// voer daarna de rest uit
KlantNr = nr;
Console.WriteLine("constructor van Klant...");
}
}
Als je base()
weglaat, zal de basisconstructor zonder parameters uitgevoerd worden, waarna de afgeleide constructor uitgevoerd wordt:
// hoofdprogramma
Klant k1 = new Klant("Bob", "U0066540");
class Persoon {
public string Name { get; set; }
// deze lege constructor wordt eerst uitgevoerd
public Persoon() {
Name = "onbekend";
Console.WriteLine("constructor 1 van Persoon...");
}
// niet-lege constructor
public Persoon(string nm) {
Name = nm;
Console.WriteLine("constructor 2 van Persoon...");
}
}
class Klant : Persoon {
public string KlantNr { get; set; }
// geen base, voer eerst lege constructor van Persoon uit
public Klant(string nm, string nr) {
// voer daarna de rest uit
KlantNr = nr;
Console.WriteLine("constructor van Klant...");
}
}
Als je base()
weglaat, en er is geen basisconstructor zonder parameters in de basisklasse, dan krijg je een — nogal cryptisch geformuleerde — foutmelding:
class Persoon {
public string Name { get; set; }
// geen lege constructor
public Persoon(string nm) {
Name = nm;
Console.WriteLine("constructor 2 van Persoon...");
}
}
class Klant : Persoon {
public string KlantNr { get; set; }
// fout: geen base, lege constructor van Persoon moet eerst worden uitgevoerd
public Klant(string nm, string nr) {
KlantNr = nr;
Console.WriteLine("constructor van Klant...");
}
}
Access modifiers
Access modifiers bepalen de zichtbaarheid van een class member (variabele, property, method...).
-
public: in het hele programma bruikbaar
— vergelijk met een openbare Facebook post, voor iedereen zichtbaar -
internal: enkel zichtbaar binnen de assembly (voor jullie komt dit neer op een project in Visual Studio)
— vergelijk met een Facebook post die enkel voor vrienden en vrienden van vrienden zichtbaar is -
protected: enkel zichtbaar binnen de klasse en afgeleide klassen
— vergelijk met een Facebook post die enkel voor vrienden zichtbaar is -
private: enkel zichtbaar binnen deze klasse
— vergelijk met een post die privé is, enkel voor jou zichtbaar
standaard: private
De standaard zichtbaarheid is altijd private, dus nergens zichtbaar buiten de klasse, zelfs niet in afgeleide klassen:
class Account {
int Deposit { get; set; } = 5000;
}
class SavingAccount : Account {
public void PrintDeposit() {
Console.WriteLine($"The current deposit is {Deposit}"); // fout in afgeleide klasse: Deposit is private
}
}
Account acc = new Account();
Console.WriteLine($"Account created with depost {acc.Deposit}"); // fout elders: Deposit is private
sa.PrintDeposit();
protected
Als je de zichtbaarheid wil uitbreiden tot de klasse en alle afgeleide klassen, dan markeer je het protected:
class Account {
int Deposit { get; set; } = 5000;
}
class SavingAccount : Account {
public void PrintDeposit() {
Console.WriteLine($"The current deposit is {Deposit}"); // OK in afgeleide klasse: Deposit is protected
}
}
Account acc = new Account();
Console.WriteLine($"Account created with depost {acc.Deposit}"); // fout elders: Deposit is protected
sa.PrintDeposit();
public
Als je de zichtbaarheid wil uitbreiden tot overal, dan markeer je het public:
class Account {
int Deposit { get; set; } = 5000;
}
class SavingAccount : Account {
public void PrintDeposit() {
Console.WriteLine($"The current deposit is {Deposit}"); // OK in afgeleide klasse: Deposit is public
}
}
Account acc = new Account();
Console.WriteLine($"Account created with depost {acc.Deposit}"); // OK elders: Deposit is public
sa.PrintDeposit();
abstract
, virtual
, override
, new
, sealed
abstract class
Een abstracte class moet overgeërfd worden om te kunnen gebruiken; het is “nog niet af”
// Meubel is abstract en kan niet rechtstreeks gebruikt worden
abstract class Meubel {
...
}
// Tafel erft over van Meubel en kan wel gebruikt worden
class Tafel : Meubel {
...
}
// Stoel erft over van Meubel en kan wel gebruikt worden
class Stoel : Meubel {
...
}
Tafel tafel1 = new Tafel(); // OK
Stoel stoel1 = new Stoel(); // OK
Meubel meubel1 = new Meubel(); // FOUT: Meubel is abstract
- voordeel: een abstracte class is misschien niet direct bruikbaar, maar kan wel al veel functionaliteit bevatten voor afgeleide klassen.
abstract, override
Een abstracte methode of property moet overschreven worden in afgeleide klassen met override
.
abstract class Persoon {
...
// merk op: geen body want is abstract en moet overschreven worden
public abstract void GeefBeschrijving();
}
// FOUT: afgeleide klasse Werknemer moet implementatie van GeefBeschrijving() voorzien
class Werknemer : Persoon {
...
}
// OK: afgeleide klasse Klant voorziet implementatie van GeefBeschrijving()
class Klant : Persoon {
...
// markeer de overschrijvende member met override
public override void GeefBeschrijving() {
Console.WriteLine("ik ben een klant");
}
}
Het voordeel is dat je zeker bent dat alle afgeleide klassen deze implementeren.
In volgend voorbeeld b.v. ben je zeker dat GeefBeschrijving()
kan gebruikt worden, of de Persoon nu Klant
of Werknemer
is:
List<Persoon> personen = new List<Persoon>();
personen.Add(new Klant());
personen.Add(new Werknemer());
foreach (Persoon p in personen) {
p.GeefBeschrijving();
}
-
merk op: de interne implementatie en het resultaat van
GeefBeschrijving()
verschilt voorKlant
enWerknemer
, en toch is er naar de buitenwereld toe één uniforme methode - dit noemt men weer polymorfisme: meerdere implementaties intern, uniform gebruik extern
abstracte methode? dan abstracte class
Merk op dat als een class abstracte methodes bevat, het per definitie “niet af” is en zelf dus ook abstract moet zijn:
// FOUT: klasse bevat abstracte members, dus moet zelf ook abstract zijn
class Persoon {
...
public abstract void GeefBeschrijving();
}
// OK, methode en class abstract
abstract class Persoon {
...
public abstract void GeefBeschrijving();
}
virtual
Een class member virtual declareren is als abstract, behalve dat het niet moet maar mag overschreven worden in afgeleide klassen (weer met override
).
// basisklasse Persoon voorziet een algemene implementatie van GeefBeschrijving()
class Persoon {
...
public virtual void GeefBeschrijving() {
Console.WriteLine("ik ben een persoon");
}
}
// afgeleide klasse Werknemer voorziet geen eigen versie, geen probleem
class Werknemer : Persoon {
...
}
// afgeleide klasse Klant voorziet wel een eigen versie, ook goed
class Klant : Persoon {
...
// markeer weer met override
public override void GeefBeschrijving() {
Console.WriteLine("ik ben een klant");
}
}
Net zoals bij abstract ben je bij virtual zeker dat elke afgeleide klasse de gemarkeerde methode heeft, terwijl — in tegenstelling tot abstract — een standaard implementatie voorzien is voor alle afgeleide klassen.
List<Persoon> personen = new List<Persoon>();
personen.Add(new Persoon());
personen.Add(new Werknemer());
personen.Add(new Klant ());
foreach (Persoon p in personen) {
p.GeefBeschrijving();
}
sealed
Als je een class member override sealed declareert, kunnen ze niet verder overschreven worden in afgeleide klassen
class Persoon {
...
// methode mag overschreven worden want is marked virtual
public virtual void GeefBeschrijving() {
Console.WriteLine("ik ben een persoon");
}
}
class Klant : Persoon {
...
// methode overschreven met override sealed
public override sealed void GeefBeschrijving() {
Console.WriteLine("ik ben een klant");
}
}
// OK: afgeleide klasse Klant voorziet implementatie van GeefBeschrijving()
class BevoorrechteKlant : Klant {
...
// FOUT: kan niet overschreven worden want is sealed door Klant
public override void GeefBeschrijving() {
Console.WriteLine("ik ben een bevoorrechte klant");
}
}
new
Je kan in theorie ook members die niet abstract of virtual gemarkeerd zijn, toch overschrijven met new:
class Persoon {
...
// niet abstract of virtual
public void GeefBeschrijving() {
Console.WriteLine("ik ben een persoon");
}
}
class Klant : Persoon {
...
// toch overschreven met new
public new void GeefBeschrijving() {
Console.WriteLine("ik ben een klant");
}
}
-
overschrijven met
new
is een slecht idee want het verbreekt de hele polymorfisme gedachte; gebruik het dus best niet - de enige situatie waarin het eventueel bruikbaar kan zijn is als je een library gebruikt en absoluut een methode wil overschrijven met jouw eigen versie
Eigenlijk overschrijf je geen methode, maar maak je effectief een nieuwe methode aan, zij het met dezelfde signatuur. Er is geen verband meer met de methode in de superklasse, het polymorfisme gaat verloren:
List<Persoon> personen = new List<Persoon>();
personen.Add(new Persoon());
personen.Add(new Werknemer());
personen.Add(new Klant());
foreach (Persoon p in personen) {
p.GeefBeschrijving();
}
Persoon persoon1 = new Persoon();
persoon1.GeefBeschrijving();
Klant klant1 = new Klant();
klant1.GeefBeschrijving();
Werknemer werknemer1 = new Werknemer();
werknemer1.GeefBeschrijving();
Merk op hoe er nu effectief een verschil is tussen b.v. Klant:GeefBeschrijving() en Persoon:GeefBeschrijving(). Er is geen verband meer met de methode in de superklasse, het polymorfisme is verloren gegaan.