n00b pro

10. classes en properties

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

Youtube: classes & properties

https://www.youtube.com/watch?v=sE48gsnO5KE

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

Een class member (letterlijk vertaald: een lid) is een onderdeel van de class. De belangrijkste types zijn:

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!

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

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}");
customer 1: Miles Davis - rating: 5
resultaat in de console

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:

Aan de andere kant kunnen de meeste applicaties wel overweg met volgende niet-pure technieken, die dan ook in klassen mogen gebruikt worden:

property vs. variabele

Nog eens alle afspraken samengevat:

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!

Klassen inspecteren in debugger

In debugger mode kun je doorklikken in de objectstructuur, bv. Bank → Accounts → Customers →...