n00b pro

13. OO samenvatting

Dit hoofdstuk is onderdeel van de cursus C#. Andere cursussen in dezelfde reeks: HTML, CSS, Javascript, Ontwikkelomgeving.

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:

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;
   }
}

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; }
}

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();
ik ben een persoon
ik ben een klant
resultaat in de console

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");
constructor 2 van Persoon...
constructor van Klant...
resultaat in de console
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");
constructor 1 van Persoon...
constructor van Klant...
resultaat in de console
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...).

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

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();
}
ik ben een klant
ik ben een werknemer
merk op hoe voor Werknemer de standaardversie van Persoon genomen wordt

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();
}
ik ben een persoon
ik ben een persoon
ik ben een klant
merk op hoe voor Werknemer de standaardversie van Persoon genomen wordt

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");
    }
}

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();
}
ik ben een persoon
ik ben een persoon
ik ben een persoon
Persoon persoon1 = new Persoon();
persoon1.GeefBeschrijving();
Klant klant1 = new Klant();
klant1.GeefBeschrijving();
Werknemer werknemer1 = new Werknemer();
werknemer1.GeefBeschrijving();
ik ben een persoon
ik ben een klant
ik ben een werknemer

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.