c# : polymorfisme

  1. Inleiding
  2. Case Study: lonen
  3. Delegates
  4. Oefeningen

Inleiding

Polymorfisme is in het naast overerving een belangrijk principe binnen het objectgeoriënteerd programmeren. Met de term polymorfisme wordt aangegeven dat een object verschillende vormen of gedaantes kan aannemen. Algemeen komt het erop neer dat een object van een afgeleide klasse kan gezien worden als een element van een basisklasse, en dus ook de eigenschappen en methoden van die basisklasse kan gebruiken.

Tot polymorfische technieken horen ook:

Het werken met polymorfisme kan vermijden dat je in een programma gebruik moet maken van switch-logica: situaties onderscheiden met een switch-statement. Switch-structuren kunnen voor problemen zorgen doordat je ervoor moet zorgen dat het onderzocht element steeds van hetzelfde type is, en moeten bij elke nieuwe situatie steeds worden aangepast, wat een tijdrovende bezigheid kan zijn.

Case Study: lonen

Principes

In deze toepassing maken we een aantal klassen aan voor het verlonen van een aantal types werknemers:

Abstacte klasse Werknemer

We maken eerst een abstracte klasse Werknemer aan. Alle klassen voor de specifieke soorten werknemers zullen overerven van deze bassisklasse. In deze klasse leggen we vast dat elke werknemer een naam en een voornaam heeft. We voorzien er de methodes ToString (override uit Object - string-representatie van een werknemer) en Verdiensten: voor elke werknemer kan een loon worden berekend. De methode Verdiensten is abstract, daar we enkel voor een specieke soort werknemer kunnen aangeven hoeveel deze verdient, we hebben geen algemene berekeningswijze voor de verdiensten van een werknemer.
using System;

namespace Lonen
{
	/// <summary>
	/// Abstracte klasse Werknemer.
	/// </summary>
	public abstract class Werknemer
	{
		private string naam;
		private string voornaam;

		public Werknemer(string naam, string voornaam)
		{
			Naam = naam;
			Voornaam = voornaam;
		}

		public string Voornaam
		{
			get
			{
				return voornaam;
			}
			set
			{
				voornaam = value;
			}
		}

		public string Naam
		{
			get
			{
				return naam;
			}
			set
			{
				naam = value;
			}
		}

		public override string ToString()
		{
			return Voornaam +" " +Naam;
		}

		public abstract decimal Verdiensten();


	}
}

De klasse Baas

Een baas heeft een vast weekloon.
using System;

namespace Lonen
{
	/// <summary>
	/// Klasse Baas.
	/// </summary>
	public class Baas: Werknemer
	{
		private decimal loon;

		public Baas(string naam, string voornaam, decimal loon )
			: base(naam, voornaam)
		{
			WekelijksLoon = loon;
		}

		public decimal WekelijksLoon
		{
			get
			{
				return loon;
			}
			set
			{
				if(value > 0) loon = value;
			}
		}

		public override decimal Verdiensten()
		{
			return WekelijksLoon;
		}

		public override string ToString()
		{
			return "Baas: " +base.ToString();

		}
	}
}

De klasse CommissieWerker

Deze werknemer krijgt een vast wekelijks loon aangevuld met een commissie per verkocht artikel.
using System;

namespace Lonen
{
	/// <summary>
	/// Klasse CommissieWerker.
	/// </summary>
	public class CommissieWerker: Werknemer
	{
		private decimal loon;
		private decimal commissie;
		private int aantal;

		public CommissieWerker(string naam, string voornaam, decimal loon,
			decimal commissie, int aantal)
			: base(naam, voornaam)
		{
			WekelijksLoon = loon;
			Commissie = commissie;
			Aantal = aantal;
		}

		public decimal WekelijksLoon
		{
			get
			{
				return loon;
			}
			set
			{
				if(value > 0) loon = value;
			}
		}

		public decimal Commissie
		{
			get
			{
				return commissie;
			}
			set
			{
				if(value > 0) commissie = value;
			}
		}

		public int Aantal
		{
			get
			{
				return aantal;
			}
			set
			{
				if(value > 0) aantal = value;
			}
		}

		public override decimal Verdiensten()
		{
			return WekelijksLoon + Commissie * Aantal;
		}

		public override string ToString()
		{
			return "CommissieWerker: " +base.ToString();

		}
	}
}


De klasse StukWerker

Deze werknemer krijgt een loon per geproduceerd stuk.
using System;

namespace Lonen
{
	/// <summary>
	/// Summary description for StukWerker.
	/// </summary>
	public class StukWerker: Werknemer
	{
		private decimal loon;
		private int aantal;

		public StukWerker(string naam, string voornaam,
			decimal loonPerStuk, int aantal)
			: base(naam, voornaam)
		{
			LoonPerStuk = loonPerStuk;
			Aantal = aantal;
		}

		public decimal LoonPerStuk
		{
			get
			{
				return loon;
			}
			set
			{
				if (value > 0) loon = value;
			}
		}

		public int Aantal
		{
			get
			{
				return aantal;
			}
			set
			{
				if (value > 0) aantal = value;
			}
		}

		public override decimal Verdiensten()
		{
			return Aantal * LoonPerStuk;
		}

		public override string ToString()
		{
			return "Stukwerker: " +base.ToString();
		}

	}
}


De klasse UurWerker

Deze werknemer krijgt een loon per gepresteerd uur, met anderhalve betaling voor overuren.
using System;

namespace Lonen
{
	/// <summary>
	/// klasse UurWerker.
	/// </summary>
	public class UurWerker: Werknemer
	{ 
		private decimal loon;
		private double uren; //uren gewerkt tijdens de week

		public UurWerker(string naam, string voornaam, decimal loon, int aantalUren)
			: base(naam, voornaam)
		{
			Loon = loon;
			Uren = aantalUren;
		}

		public decimal Loon
		{
			get
			{
				return loon;
			}
			set
			{
				if(value > 0) loon = value;
			}
		}

		public double Uren
		{
			get
			{
				return uren;
			}
			set
			{
				if(value > 0) uren = value;
			}
		}

		public override decimal Verdiensten()
		{
			decimal loon = Loon * Convert.ToDecimal(Uren);
			if(Uren > 40)
			{
				loon += Convert.ToDecimal(Uren - 40) * Loon * 1.5M;
			}
			return loon;
		}

		public override string ToString()
		{
			return "Uurwerker: " +base.ToString();
		}
	}
}


De GUI-testklasse

Maken we nu nog een testklasse met onder meer volgende code:
private void Form1_Load(object sender, System.EventArgs e)
		{
			string uit = "";
			Werknemer werknemer;

			Baas baas = new Baas("Speckneck","Bill",1000);
			CommissieWerker commissiewerker = new CommissieWerker("Zetter","Af",200M,10M,5);
			StukWerker stukwerker = new StukWerker("Adoor","Stuk",100M,5);
			UurWerker uurwerker = new UurWerker("Overuurs","Fien",10,50);


			uit += baas.ToString() +" verdient " + baas.Verdiensten().ToString("c") +"\n";
			werknemer = baas;
			uit += maakString(werknemer);

			uit += commissiewerker.ToString() +" verdient " + commissiewerker.Verdiensten().ToString("c") +"\n";
			werknemer = commissiewerker;
			uit += maakString(werknemer);

			uit += stukwerker.ToString() +" verdient " + stukwerker.Verdiensten().ToString("c") +"\n";
			werknemer = stukwerker;
			uit += maakString(werknemer);

			uit += uurwerker.ToString() +" verdient " + uurwerker.Verdiensten().ToString("c") +"\n";
			werknemer = uurwerker;
			uit += maakString(werknemer);

			L.Text = uit;
		}
		
		public static string maakString(Werknemer werker)
		{
			return werker.ToString() +" verdient " +werker.Verdiensten().ToString("c") +"\n\n";
		}
Dan zie je volgend resultaat:

Bespreking

In deze toepassing maken we vier objecten aan: een Baas, een CommissieWerker, een StukWerker en een UurWerker. Telkens brengen we met de methoden ToString en Verdiensten van de respectievelijke klassen op het scherm wat de persoonlijke- en loonsgegevens zijn.

Elk object wordt telkens gestopt in een variabele van het type Werknemer. Na deze toekenning wordt de methode maakString van de GUI-klasse uitgevoerd. Deze methode ontvangt een Werknemer en retourneert een string na toepassen van de methoden ToString en Verdiensten van de klasse Werknemer.

Je merkt dat na de toekenning toch voor elk soort werknemer de juiste loonsberekening wordt uitgevoerd!

Dit is de kracht van polymorfisme: de methode maakString hoef je slechts 1 keer aan te maken. Deze methode ontvangt een instantie van de basisklasse Werknemer.

De superklasse Werknemer kan de juiste methoden van de afgeleide klassen aanroepen!

Dit fenomeen kan je handig aanwenden voor het maken van herbruikbare methoden.

Opmerking: dit fenomeen treedt ook op wanneer de methode in de superklasse niet abstract is. Doe bijvoorbeeld volgende aanpassing in de klasse Werknemer:

public virtual decimal Verdiensten()
{
		return 300M;
}
Je zal merken dat de respectievelijke methoden van de afgeleide klassen nog steeds worden uitgevoerd.

Delegates

Inleiding

Stel je voor dat je in een programma een methode wenst aan te roepen, maar je weet slechts run-time welke methode moet worden aangeroepen. Dit kan je als programmeur oplossen met behulp van delegates.

Delegates declareren

Een delegate heeft een uniek signatuur: een unieke naam, parameterlijst en returntype.
public delegate double EenheidsOmzetting(double gegeven);
Een delegate bevat zoals je ziet geen implementatie, maar ze vormen een geraamte voor delegate-handler-methoden tot dewelke ze refereren. De handler-methode bevat een implementatie die wordt uitgevoerd wanneer de refererende delegate wordt uitgevoerd.

Delegates gebruiken

Een delegate gebruiken gebeurt in vier stappen:

In deze toepassing maken we een delegate EenheidsConversie die een double ontvangt en ook een double retourneert. Het is de bedoeling dat de gebruiker een waarde uit een bepaalde eenheid kan omzetten in een andere eenheid.

We maken een functie MijlNaarKM met eenzelfde signatuur als de delegate.

We maken een instantie ec van de delegate EenheidsConversie en geven mee dat we de methode MijlNaarKm willen gebruiken bij het uitvoeren van de delegate.

We kunnen nu de delegate gebruiken precies zoals een gewone methode en hoeven ons niet af te vragen welke de achterliggende methode (MijlNaarKm) is die zal worden uitgevoerd.

namespace Delegates
{
	// 1. definieer Delegate
	public delegate double EenheidsConversie(double gegeven);


	public class Form1 : System.Windows.Forms.Form
	{
	
		...
	
		// 2. definiëer handler-methode
		public static double MijlNaarKm(double mijl)
		{
			return mijl * 1.609344;
		}


		private void btnMijlNaarKM_Click(object sender, System.EventArgs e)
		{
			// 3. Maak een instantie van de delegate
			EenheidsConversie ec = new EenheidsConversie(MijlNaarKm);
			
			// 4. Gebruik de delegate net zoals een methode
			double mijl = ec(double.Parse(txtGegeven.Text));


			lblResultaat.Text = mijl.ToString();
		}
	}

Dynamische delegate-methoden

Zoals eerder vermeld kan je met een delegate meerdere gelijkaardige functies uitvoeren. We maken nog een handler functie CelsiusNaarKelvin die een waarde in graden celsius omzet in Kelvin.

Deze tweede handler-functie heeft opnieuw eenzelfde signatuur als de delegate.

Analoog aan het eerste voorbeeld kunnen we deze handler-functie bereiken door een instantie van de delegate te declareren met als argument de naam van deze handler-functie. Wanneer we nu de delegate uitvoeren wordt de conversie van celsius in Kelvin gedaan.

namespace Delegates
{
	// 1. definieer Delegate
	public delegate double EenheidsConversie(double gegeven);


	public class Form1 : System.Windows.Forms.Form
	{
	
		...
	
		// 2. definiëer handler-methode
		public static double MijlNaarKm(double mijl)
		{
			return mijl * 1.609344;
		}
		
		// 2. een andere handler methode
		public static double CelsiusNaarKelvin(double celsius)
		{
			return celsius + 274.15;
		}


		private void btnMijlNaarKM_Click(object sender, System.EventArgs e)
		{
			// 3. Maak een instantie van de delegate
			EenheidsConversie ec = new EenheidsConversie(MijlNaarKm);
			
			// 4. Gebruik de delegate net zoals een methode
			double mijl = ec(double.Parse(txtGegeven.Text));

			lblResultaat.Text = mijl.ToString();
		}
		
		private void btnCelsiusNaarKelvin_Click(object sender, System.EventArgs e)
		{
			
			EenheidsConversie ec = new EenheidsConversie(CelsiusNaarKelvin);
			
			double kelvin = ec(double.Parse(txtGegeven.Text));

			lblResultaat.Text = kelvin.ToString();
		}
	}

Werken met een factory-methode

We maken nu een methode die intern bijhoudt welke conversie er dient te gebeuren. De methode GetConversieMethode ontvangt een int en retourneert een instantie van de delegate EenheidsConversie. Binnen deze methode wordt de instantie van EenheidsConversie aangemaakt naargelang de meegegeven integer.

We noemen de functie GetConversieMethode een factory-methode daar het niet de bedoeling is dat ze rechtstreeks wordt aangeroepen, ze wordt steeds opgeroepen vanuit een andere methode.

Hier zie je ook een methode maakResultaat: deze methode ontvangt een double die het om te zetten getal bevat en een int die het type van omzetting bepaalt. De functie retourneert een double: het omgezette getal.

Op deze manier wordt het mogelijk een omzettingsfunctie te gebruiken door enkel het meegeven van een integer. Je kan hier natuurlijk ook een enumeratie voor voorzien, dan wordt het nog transparanter.



namespace Delegates
{
	// 1. definieer Delegate
	public delegate double EenheidsConversie(double gegeven);


	public class Form1 : System.Windows.Forms.Form
	{
		...


		// 2. definiëer handler-methode
		public static double MijlNaarKm(double mijl)
		{
			return mijl * 1.609344;
		}

		// 2. een andere handler methode
		public static double CelsiusNaarKelvin(double celsius)
		{
			return celsius + 274.15;
		}
		
		/// <summary>
		/// methode voor het bepalen van de soort conversie
		/// </summary>
		/// <param name="conversionType">integer die het type aangeeft</param>
		/// <returns>Een pointer naar de respectievelijke delegatefunctie</returns>
		public EenheidsConversie GetConversieMethode(int conversieType)
		{
			EenheidsConversie conversie;

			switch (conversieType)
			{
				case 1:
					conversie = new EenheidsConversie(MijlNaarKm);
					break;
				case 2:
					conversie = new EenheidsConversie(CelsiusNaarKelvin);
					break;
				default:
					throw new ArgumentException("Conversietype niet herkend: " + conversieType);
			}

			return conversie;
		}

		/// <summary>
		/// Doet de omzetting volgens het opgegeven conversietype
		/// Eerst maken we een EenheidsConverie object met de methode GetconversieMethode
		///		het type krijgen we als integer binnen
		/// Daarna voeren we de respectievelijke delegatefunctie uit met de meegegeven double-waarde
		/// </summary>
		/// <param name="waarde">waarde die we wensen om te zetten</param>
		/// <param name="type">integer die het type omzetting voorstelt</param>
		/// <returns></returns>
		private double MaakResultaat(double waarde, int type)
		{
			EenheidsConversie ec = GetConversieMethode(type);
			return ec(waarde);
			
		}

		private void btnMijlNaarKM_Click(object sender, System.EventArgs e)
		{
			lblResultaat.Text = MaakResultaat(double.Parse(txtGegeven.Text),1).ToString();
		}

		private void btnCelsiusNaarKelvin_Click(object sender, System.EventArgs e)
		{
			lblResultaat.Text = MaakResultaat(double.Parse(txtGegeven.Text),2).ToString();
		}
	}
}


Meerdere handler-functies voor een delegate-instantie

Je kan meerdere handler-functies toekennen aan een instantie van een delegate.

Wanneer je de delegate uitvoert zullen alle handler-functies uitgevoerd worden.

Het toekennen van een handlermethode gebeurde in de bovenstaande voorbeelden met de operator = .
Wens je echter nog een delegate-handlermethode toe te kennen aan de instantie van de delegate dan kan dat met de operator += .

Wanneer je de instantie nu uitvoert worden alle delegate-handlermethodes die werden toegekend uitgevoerd.


namespace Delegates
{
	// 1. definieer Delegate
	public delegate double EenheidsConversie(double gegeven);


	public class Form1 : System.Windows.Forms.Form
	{
		
		...


		

		// 2. definiëer handler-methode
		public double MijlNaarKm(double mijl)
		{
			double res = mijl * 1.609344;
			lblRapport.Text += "Mijl naar KM: " +res.ToString() +"\n";
			return res;
		}

		// 2. een andere handler methode
		public double CelsiusNaarKelvin(double celsius)
		{
			double res = celsius + 274.15;
			lblRapport.Text += "Celsius naar Kelvin: " +res.ToString() +"\n";
			return res;
		}
		
		/// <summary>
		/// methode voor het bepalen van de soort conversie
		/// </summary>
		/// <param name="conversionType">integer die het type aangeeft</param>
		/// <returns>Een pointer naar de respectievelijke delegatefunctie</returns>
		public EenheidsConversie GetConversieMethode(int conversieType)
		{
			EenheidsConversie conversie;

			switch (conversieType)
			{
				case 1:
					conversie = new EenheidsConversie(MijlNaarKm);
					break;
				case 2:
					conversie = new EenheidsConversie(CelsiusNaarKelvin);
					break;
				default:
					throw new ArgumentException("Conversietype niet herkend: " + conversieType);
			}

			return conversie;
		}

		/// <summary>
		/// Doet de omzetting volgens het opgegeven conversietype
		/// Eerst maken we een EenheidsConverie object met de methode GetconversieMethode
		///		het type krijgen we als integer binnen
		/// Daarna voeren we de respectievelijke delegatefunctie uit met de meegegeven double-waarde
		/// </summary>
		/// <param name="waarde">waarde die we wensen om te zetten</param>
		/// <param name="type">integer die het type omzetting voorstelt</param>
		/// <returns></returns>
		private double MaakResultaat(double waarde, int type)
		{
			EenheidsConversie ec = GetConversieMethode(type);
			return ec(waarde);
			
		}

		private void btnMijlNaarKM_Click(object sender, System.EventArgs e)
		{
			lblResultaat.Text = MaakResultaat(double.Parse(txtGegeven.Text),1).ToString();
		}

		private void btnCelsiusNaarKelvin_Click(object sender, System.EventArgs e)
		{
			lblResultaat.Text = MaakResultaat(double.Parse(txtGegeven.Text),2).ToString();
		}

		private void btnRapport_Click(object sender, System.EventArgs e)
		{
			int aantalOmzettingen = 2; // aantal uit te voeren omzettingen

			EenheidsConversie conversies = null;
            
			for(int type = 1; type <= aantalOmzettingen; type++)
			{
				// delegate methoden toevoegen aan EenheidsConversie-object
				conversies += GetConversieMethode(type);
			}
			
			//  Doordat we met voorgaande lus meerdere delegate-functies hebben toegevoegd worden deze nu allen uitgevoerd !
			conversies(double.Parse(txtGegeven.Text));
			
			

		}
	}
}


Delegatemethodes toevoegen en verwijderen

Een delegatemethode toevoegen aan een delegateinstantie gebeurt met de operator +=, een delegatemethode verwijderen gebeurt met de operator -= .

Wanneer een delegate-instantie geen delegatemethoden meer bezit wordt het object ingesteld op null. Je kan op deze manier controleren of er een methode kan worden uitgevoerd.

if (conversies != null)
{
      conversies(waarde);
}
else
{
      Console.WriteLine("Geen conversies geselecteerd.");
}

Delegates en events

De onderliggende implementatie van een event is in .net eigenlijk een delegate.

Events hebben enkel nog een aantal andere beveiligingen: bij een delegate kan je de handlermethodes onmiddellijk loskoppelen met een toekenning (=), bij events niet: enkel de operatoren += en -= zijn toegestaan.
Event-handlers kunnen niet uitgevoerd worden van buiten de omvattende klasse.

Oefeningen

oefeningen

Meer tutorials:
leer ook: html | xhtml | css | asp | asp.net | c# | ado.net | linq | ajax | java | javascript
Valid HTML 4.01! Valid CSS! © - Cursusweb