Youtube: classes & properties
- 0:00 Intro
- 2:13 class maken
- 4:08 public vs. private variabelen
- 6:05 getter en setter methodes
- 11:00 underscore prefix van private variabelen
- 12:28 properties toevoegen
- 17:59 automatische properties
- 19:25 standaardwaarden
- 20:50 constructors toevoegen
- 28:42 :this()
- 30:08 compositie van meerdere klassen
- 33:36 methodes toevoegen
- 39:10 gebruik van classes in hoofdprogramma
Class maken
Een class is een sjabloon (template) voor het aanmaken van nieuwe objecten. Stel dat we aan een project WpfBanking bezig zijn (met Console apps kan het ook); een nieuwe class voeg je toe via Add, New Item…
We hebben al een lege class met enkel een naam:
namespace WpfBanking {
class Customer {
}
}
- we zullen de class nu inhoud geven
Een class member (letterlijk vertaald: een lid) is een onderdeel van de class. De belangrijkste types zijn:
- variabelen: bevatten gegevens van een object
- constructors: beschrijven hoe objecten gemaakt worden
- properties: beschrijven de state (toestand) van een object
- methodes: beschrijven de behavior (gedrag) van een object
Variabelen
Laat ons enkele variabelen toevoegen:
class Customer
{
int clientId; // bestaat uit exact 10 cijfers
string firstName;
string lastName;
MailAddress email; // opmerking: class MailAddress zit in System.Net.Mail
int rating = 3; // getal van 1 tot 5; standaardwaarde = 3
DateTime birthDate;
DateTime registerDate;
}
Je kan al instanties aanmaken met new Customer()
, maar de variabelen aanspreken lukt blijkbaar nog niet:
De standaard zichtbaarheid voor members is private, d.w.z. enkel zichtbaar binnen het object zelf.
Je zou dit kunnen oplossen door de variabelen die je nodig hebt public te maken:
class Customer
{
public int clientId;
public string firstName;
public string lastName;
public MailAddress email;
public int rating = 3;
public DateTime birthDate;
private DateTime registerDate; // deze mag private blijven; is enkel voor intern gebruik
}
De variabelen zijn nu wel toegankelijk:
Hoewel de public variabelen nu wel toegankelijk zijn, is dit een heel slechte programmeerstijl: maak nooit variabelen publiek, het is als de sleutels van je huis aan een vreemde geven!
- maar hoe moet het dan wel…?
- we hebben properties nodig
Properties
Het idee is dat je variabelen altijd private houdt, en toegang geeft op een andere, gecontroleerde manier. Een eerste (slechte) oplossing is dat je voor elke variabele een get- en een set methode voorziet. Het nadeel is dat je meteen wel heel veel code hebt:
class Customer
{
private int clientId;
private string firstName;
private string lastName;
private MailAddress email;
private int rating = 3;
private DateTime birthDate;
private DateTime registerDate;
public int GetClientId() { // getter voor clienId
return clientId;
}
public string GetFirstName() { // getter voor firstName
return firstName;
}
public void SetFirstName(string fn) { // setter voor firstName
firstName = fn;
}
public string GetLastName() { // getter voor lastName
return lastName;
}
public void SetLastName(string ln) { // setter voor lastName
lastName = ln;
}
public MailAddress GetEmail() { // getter voor email
return email;
}
public void SetEmail(string em) { // setter voor email, met validatie
try {
MailAddress m = new MailAddress(em);
} catch (FormatException) {
throw new ArgumentException($"ongeldig email");
}
email = em;
}
public int GetRating() { // getter voor rating
return rating;
}
public void SetRating(int rtn) { // setter voor rating, met validatie
if (rtn < 1 || rtn > 5) {
throw new ArgumentOutOfRangeException($"rating moet tussen 1 en 5 liggen");
}
rating = rtn;
}
public DateTime GetBirthDate() { // getter voor birthDate
return birthDate;
}
public void SetBirthDate(DateTime bd) { // setter voor birthDate
birthDate = bd;
}
}
Daarom heeft men verkorte notaties bedacht, de zgn. properties. Dit fragment...
private string firstName;
...
public string GetFirstName() {
return firstName;
}
public void SetFirstName(string fn) {
firstName = fn;
}
...wordt dan verkort tot een property
private string firstName;
...
public string FirstName { // naam begint met een hoofletter, en geen haakjes()
get { return firstName; } // implementatie getter
set { firstName = value; } // implementatie setter (met gereserveerd woord "value")
}
automatische properties
Als je geen validatie nodig hebt, kan je het zelfs nóg korter schrijven tot een automatische property:
public string FirstName { get; set; } // automatische property
read-only properties
Als je de setter weglaat, heb je een read-only property:
public string ClientId { get; } // read-only automatische property
property standaardwaarde
Je kan standaardwaarden opgeven op deze manier:
public string Balance { get; set; } = 0; // automatische property met standaardwaarde
volledig codevoorbeeld
De volledige code van de class tot nu toe is een stuk korter geworden:
class Customer {
private string _email; // conventie: prefix property variabelen met _
private int _rating = 3; // conventie: prefix property variabelen met _
private DateTime registerDate;
public int ClientId { get; } // automatische read-only property
public string FirstName { get; set; } // automatische property
public string LastName { get; set; } // automatische property
public DateTime BirthDate { get; set; } // automatische property
public string Email { // niet-automatische property met validatie
get { return _email; }
set {
try {
MailAddress m = new MailAddress(value);
_email = value;
} catch (FormatException) {
throw new ArgumentException($"ongeldig email");
}
}
}
public int Rating { // niet-automatische property met validatie
get { return _rating; }
set {
if (value < 1 || value > 5) {
throw new ArgumentOutOfRangeException($"rating moet tussen 1 en 5 liggen");
}
_rating = value;
}
}
}
De properties kunnen nu publiek gebruikt worden:
Onze property validatie in actie:
Constructors
Een constructor is een blauwdruk om een object te creëren.
lege constructor
Voor elke klasse bestaat, of je ‘m nu specifieert of niet, een lege constructor, d.i. de constructor zonder parameters:
class Customer {
// variabele(n)
private DateTime registerDate;
// properties
public int ClientId { get; }
public string FirstName { get; set; }
public string LastName { get; set; }
public DateTime BirthDate { get; set; }
...
// lege constructor
public Customer() { }
}
De variabelen en properties worden dan geïnitialiseerd op hun standaardwaarden:
aangepaste constructor
Je kan de constructor aanpassen om allerlei initialisaties te doen:
class Customer {
// variabele(n)
private DateTime registerDate;
// properties
public int ClientId { get; }
public string FirstName { get; set; }
public string LastName { get; set; }
public int Rating { get; set; }
// aangepaste constructor
public Customer() {
registerDate = DateTime.Now;
FirstName = "John";
LastName = "Doe";
Rating = new Random().Next(1, 6);
...
}
}
Dit is trouwens exact wat we al die tijd in MainWindow()
deden, de constructor van de MainWindow
pagina:
public partial class MainWindow : Window {
private DispatcherTimer timer;
// MainWindow standaardconstructor
public MainWindow() {
InitializeComponent();
sldAmount.Maximum = 100;
sldAmount.Value = aantalEllipsen = 20;
timer = new DispatcherTimer();
timer.Interval = TimeSpan.FromMilliseconds(100);
timer.Tick += SpawnEllipse;
}
....
}
De variabelen en properties hebben nu beginwaarden meegekregen in de constructor:
Vaak kan je de constructor leeg te laten, en gewoon property standaardwaarden instellen:
class Customer {
// variabele(n)
private DateTime registerDate = DateTime.Now;
// properties
public int ClientId { get; }
public string FirstName { get; set; } = "John";
public string LastName { get; set; } = "Doe";
public int Rating { get; set; } = new Random().Next(1, 6);
// standaardconstructor
public Customer() { }
}
constructors met parameters
Je kan bijkomende constructors definiëren mét parameters:
// default constructor, zonder parameters
public Customer() {
registerDate = DateTime.Now;
}
// constructor met parameters
public Customer(string fn, string ln) {
registerDate = DateTime.Now;
FirstName = fn;
LastName = ln;
Rating = (new Random()).Next(1, 6);
}
// constructor met parameters
public Customer(string fn, string ln, int rt) {
registerDate = DateTime.Now;
FirstName = fn;
LastName = ln;
Rating = rt;
}
constructor met :this()
Je kan de vorige code nog inkorten door vanuit de ene constructor de andere op te roepen via :this()
constructor overloading
Het definiëren van meerdere constructoren met verschillende parameters heet constructor overloading. Je kan dan kiezen met welke constructor je een nieuw object aanmaakt:
Customer cust1 = new Customer(); // gebruik eerste constructor
Customer cust2 = new Customer("Bobby", "Peru"); // gebruik tweede constructor
Customer cust3 = new Customer("Johnny", "Miles", 4); // gebruik derde constructor
alternatief: object initialisatie
In plaats van alle mogelijke constructoren te definiëren voor alle mogelijke gevallen, kan je ook gewoon de lege constructor gebruiken met de object initialisatie syntax. Beide fragmenten zijn gelijkwaardig (al vind ik de tweede leesbaarder):
// klassieke notatie
Customer cust = new Customer();
cust.FirstName = "Johnny";
cust.LastName = "Miles";
cust.Rating = 4;
// object initializer syntax
Customer cust3 = new Customer() {
FirstName = "Johnny",
LastName = "Miles",
Rating = 4
};
// object initializer syntax zonder ()
Customer cust3 = new Customer {
FirstName = "Johnny",
LastName = "Miles",
Rating = 4
};
Methodes
eigen methodes
Laat we onze klasse uitbreiden met methodes. Voegen we eerst nog een klasse Account
toe...
namespace WpfBanking {
class Account {
// properties
public string AccountNr { get; set; }
public Customer Holder { get; set; } // we gebruiken onze eigen Customer klasse
public decimal Balance { get; set; } = 0; // tip: voor geldbedragen gebruiken we altijd decimal
// constructor
public Account(string nr) {
AccountNr = nr;
}
}
}
...en ook nog een klasse Bank
, uitgebreid met enkele klasse methodes:
namespace WpfBanking {
class Bank {
// properties
public string Name { get; }
public List<Customer> Customers { get; set; } = new List<Customer>(); // we gebruiken eigen Customer klasse
public List<Account> Accounts { get; } = new List<Account>(); // we gebruiken eigen Account klasse
// constructor
public Bank(string name) {
Name = name;
}
// klasse methodes
public bool IsHealthy() {
return CalculateGrandTotal() > 0;
}
private decimal CalculateGrandTotal() {
decimal total = 0;
foreach (Account a in Accounts) {
total += a.Balance;
}
return total;
}
public List<A>ccount> GetAccountsLessThan(decimal amount) {
List<Account> accounts = new List<Account>();
foreach (Account a in Accounts) {
if (a.Balance < amount) accounts.Add(a);
}
return accounts;
}
}
}
- denk goed na of je methode public (buiten de klasse beschikbaar) of private moet zijn
- denk na over parameters en returntype
De nieuwe methodes (of toch de publieke) verschijnen eveneens in de intellisense:
override ToString()
Elk object heeft een standaard ToString()
implementatie, d.w.z. de weergave in een string
context. Voegen we b.v. een paar Customer
instanties toe aan een ListBox
:
Customer cust1 = new Customer() {
FirstName = "Miles",
LastName = "Davis",
Rating = 5
};
Customer cust2 = new Customer() {
FirstName = "Pharoah",
LastName = "Sanders",
Rating = 3
};
Customer cust3 = new Customer() {
FirstName = "John",
LastName = "Coltrane",
Rating = 4
};
lbxCustomers.Items.Add(new ListBoxItem() {
Content = cust1
});
lbxCustomers.Items.Add(new ListBoxItem() {
Content = cust2
});
lbxCustomers.Items.Add(new ListBoxItem() {
Content = cust3
});
De weergave van de customers in de ListBox
is nogal nietszeggend:
Je kan dit oplossen door een eigen implementatie te voorzien van de ToString()
methode (override
betekent "overschrijven"):
class Customer
{
...
public override string ToString()
{
return $"{FirstName} {LastName} - rating: {Rating}";
}
}
Deze methode wordt dan gebruikt in elke string
context:
Nog een ander voorbeeld van zo'n string
context, in een Console toepassing:
Customer cust1 = new Customer() {
FirstName = "Miles",
LastName = "Davis",
Rating = 5
};
Console.WriteLine($"customer 1: {cust1}");
Best practices
(min of meer) pure klassen
Goed geschreven klassen staan los van de applicatie waarin ze gebruikt worden (WPF applicatie, Console toepassing, Class Library...)
Daarom hou je ze best zo puur mogelijk:
- gebruik geen Console methodes als
Console.WriteLine()
,Console.ReadKey()
... (werkt niet in WPF) - gebruik geen WPF controls als
Button
,TextBox
... (werkt niet in Console) - gebruik geen klassen voor afbeeldingen als
BitmapImage
,Image
... (werkt niet in Console) —byte[]
array voor image data kan dan weer wel
Aan de andere kant kunnen de meeste applicaties wel overweg met volgende niet-pure technieken, die dan ook in klassen mogen gebruikt worden:
- werken met databanken
- lezen en schrijven van bestanden
- lezen en schrijven over netwerken
- gebruik van Random()
property vs. variabele
Nog eens alle afspraken samengevat:
- variabelen zijn altijd private
- properties kunnen in principe ook private zijn, maar zijn meestal public
- properties beginnen met een hoofletter, variabelen met een kleine letter
- property variabelen beginnen met een underscore _
- gebruik automatische properties als er geen validatie is
private string _firstName;
...
public string FirstName
{
get { return _firstName; }
set { _firstName = value; }
}
...
public string FirstName
{
get;
set;
}
methode vs. property
Als een methode enkel een waarde teruggeeft, en dus eerder aanvoelt als een "eigenschap" van het object, gebruik je beter een read-only property:
constructor vs. object initializer
In plaats van allerlei constructors met parameters te definiëren, kan je vaak eenvoudig de lege constructor met de object initializer gebruiken:
// lege constructor
public Customer() { }
// extra constructors met parameters
public Customer(string fn, string ln) {
registerDate = DateTime.Now;
FirstName = fn;
LastName = ln;
Rating = (new Random()).Next(1, 6);
}
public Customer(string fn, string ln, int rt) : this(fn, ln) {
Rating = rt;
}
Customer cust1 = new Customer();
Customer cust2 = new Customer("Bobby", "Peru");
Customer cust3 = new Customer("Johnny", "Miles", 4);
// lege constructor
public Customer() { }
Customer cust1 = new Customer();
Customer cust2 = new Customer() {
FirstName = "Bobby",
LastName = "Peru"
};
Customer cust3 = new Customer() {
FirstName = "Johnny",
LastName = "Miles",
Rating = 4
};
associatie / aggregatie / compositie
Als klassen met elkaar gerelateerd zijn ("associatie"), gebruik ze dan ook in elkaars definitie. Er zijn twee soorten: de compositie (A kan niet bestaan zonder B) en de aggregatie (A kan wel bestaan zonder B).
Gebruik klassen in een hoofdprogramma
Een voorbeeldgebruik van onze drie klassen Bank
, Account
en Customer
:
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
Bank jpmorgan = new Bank("JP Morgan");
Customer kayne = new Customer("Kayne", "West");
Customer kim = new Customer("Kim", "Kardashian");
Account accountX = new Account("BE20001097");
accountX.Holder = kim;
jpmorgan.Customers.Add(kayne);
jpmorgan.Customers.Add(kim);
jpmorgan.Accounts.Add(accountX);
if (jpmorgan.IsHealthy()) {
// ...
}
}
}
Merk op dat onze klasse verre van af zijn!
- rekeningnummers blijven leeg
- als een account toegevoegd wordt aan een bank, worden de customers niet toegevoegd
- er is nog geen functionaliteit voor overschrijvingen, afhalingen, interesten...
- er zijn geen limieten op het saldo
- ...
Klassen inspecteren in debugger
In debugger mode kun je doorklikken in de objectstructuur, bv. Bank → Accounts → Customers →...