Arv
Et af de mest omtalte begreber i objektorienteret programmering er arv, hvilket dækker over muligheden for at genbruge og dermed vedligeholde kode i et hierarki af klasser.
Information til undervisere
“Alt hvad der er i mor er i barnet” er en god huskeregel for arv. Der er bare nogle ting, som barnet ikke kan tilgå, fordi de er markeret som private.
Det er vigtigt at understrege, at arv ikke er en løsning på alle problemer, og at det kan være en god idé at overveje andre muligheder. Det er en del af værkstøjskassen, og det er op til den enkelte udvikler at vurdere, om det er den rigtige løsning.
Det er supersmart og i praksis ret nemt at implementere, men du skal være opmærksom på, at du ikke behøver benytte arv i din kode. Det er et værktøj i værktøjskassen, du kan vælge at trække frem, og nogle applikationer egner sig bedre til at benytte arv end andre. Microsoft har i høj grad benyttet sig af arv ved design af frameworket, men du vælger, i hvilken grad du vil benytte det i din applikation. Det er helt op til dig.
Tip
Der er en kort sætning du skal huske på når du har med arv at gøre: “Alt hvad der er i mor er i barnet”.
En lidt længere sætning er: “Alt hvad der er i mor er i barnet, men det er ikke sikkert at barnet kan tilgå alle medlemmer fra mor fordi de kan være markeret som private”. Det skal forstås således, at hvis mor består af eksempelvis to felter defineret som heltal, så består barnet også af to heltal - også selv de måtte være private i mor.
Hierarki af klasser
Ved hjælp af arv har du mulighed for at skabe en (og i C# kun én) klasse, som betragtes som mor for andre. Det kaldes også en super-klasse. Alle medlemmer (felter, egenskaber, metoder og hændelser) med den korrekte synlighed i denne klasse er tilgængelige i alle underklasser, og det vigtigste budskab i den forbindelse er, at medlemmernes signatur ikke kan ændres i underklasser.
Info
En signatur på en metode i et arvehierarki svarer til metodens navn, returværdi og argumenter.
Hvis eksempelvis en metode er gjort tilgængelig i underklasser, vil den ikke kunne fjernes i hele hierarkiet. Den kan muligvis blive tildelt en anden implementation (andre instruktioner) – men metoden med den samme signatur skal være der.
Det mest simple eksempel på et arvehierarki er følgende:
Det kan du læse som klassen Barn, der arver fra klassen Mor, og alt hvad der findes i mor findes også i barnet. Yderligere kan alle offentlige medlemmer i mor også tilgås i barnet.Derfor kan man altid i arvehierarkier sige, at et barn er en mor med eventuelle tilføjelser.
Hvis klassen Mor har en offentlig metode Skriv, så har barnet det også. Hvordan metoden er implementeret i barnet, kan være anderledes end i mor – men metoden findes og kan ikke fjernes på nogen måde.
Syntaksen for arv i C# er kolon, så ovennævnte hierarki kan skrives som:
Du kan også opfatte et hierarki som en definering af generelle offentlige medlemmer, som i underklasser bliver mere og mere specifikke. Her er et andet og lidt mere komplekst eksempel, der beskriver dette:
I toppen af hierarkiet findes klassen Dyr, og jo længere du kommer ned i hierarkiet, bliver klasserne mere og mere specifikke, men du (og runtime) kan være helt sikker på, at medlemmer, der findes i Dyr, også findes i alle underklasser. Du vil ligeledes altid kunne antage, at en underklasse er en overklasse. Eksempelvis er en torsk en fisk, som er et hvirveldyr, som et er dyr, og det samme gælder en musling, som er et bløddyr, som er et hvirvelløst dyr, som er et dyr.
I kode ser hierarkiet således ud:
class Dyr { }
class Hvirveldyr : Dyr { }
class Hvirvelløse : Dyr { }
class Fisk : Hvirveldyr { }
class Torsk : Fisk { }
class Pattedyr : Hvirveldyr { }
class Elefant : Pattedyr { }
class Bløddyr : Hvirvelløse { }
class Musling : Bløddyr { }
Sluttelig er her et mere applikationsorienteret hierarki.
Forestil dig, at du skal udvikle en applikation relateret til kurser. I så fald kunne følgende måske gøre koden mere genbrugelig:
Alle klasser arver fra Person og består dermed af alle de medlemmer, som en person måtte have (måske egenskaberne navn og cpr-nummer og tilhørende felter, samt metoderne skriv og gem), men klasserne bliver mere og mere specifikke.
Igen vil du altid kunne antage, at både en hjælpeinstruktør (fra venstre gren) og en kursist (fra højre gren) er en person. Her er koden bag hierarkiet:
class Person { }
class Medarbejder : Person { }
class Kursist : Person { }
class Instruktør : Medarbejder { }
class FremmødeKursist : Kursist { }
class OnlineKursist : Kursist { }
class HjælpeInstruktør : Instruktør { }
Rækkefølgen eller placering af klasserne har ingen betydning. Nogle udviklere vil gerne have klasser i et hierarki i samme fil, men de fleste kan bedst lide, at hver fil indeholder én klasse. Men det er helt op til dig.
I C# må en klasse kun have én mor, det kaldes som begreb single inheritance. I andre sprog er det muligt, at en klasse kan have flere mødre. Det kaldes multiple inheritance.
Brug af arv
I et arvehierarki vil medlemmer med offentlige (eller som vi skal se senere – beskyttede) medlemmer i mor altså være tilgængelig i alle børn. Du skal huske på, at et barn er en mor, og kompileren skal nok sørge for, at medlemmer er tilgængelige.
Se følgende eksempel:
internal class Person {
public string Navn { get; set; }
public void Skriv() {
Console.WriteLine($"Mit navn er {this.Navn}");
}
}
Det er en almindelig klasse med en egenskab og en metode som tidligere gennemgået, og den kan benyttes som følger:
Hvis en klasse Elev arver fra Person ser det således ud:
internal class Person {
public string Navn { get; set; }
public void Skriv() {
Console.WriteLine($"Mit navn er {this.Navn}");
}
}
internal class Elev : Person { }
// Da en Elev er en Person er følgende muligt:
Elev e = new Elev();
e.Navn = "Mathias";
e.Skriv();
Alle medlemmer er helt automatisk en del af Elev, og så har du jo ikke opnået særlig meget. Men du kan udvide klassen Elev med de medlemmer, du har lyst til - eksempelvis:
internal class Person
{
public string Navn { get; set; }
public void Skriv()
{
Console.WriteLine($"Mit navn er {this.Navn}");
}
}
internal class Elev : Person
{
public int ElevNummer { get; set; }
public void Gem()
{
Console.WriteLine($"Gemmer {this.Navn}");
}
}
Og nu kan Elev bruges således:
Elev e = new Elev();
e.Navn = "Mathias";
e.ElevNummer = 1;
e.Skriv(); // Mit navn er Mathias
e.Gem(); // Gemmer Mathias
Nu har Elev både medlemmer fra mor (Person) og sine egne medlemmer, og en eventuel tilretning af Person vil også slå igennem i Elev. I et hierarki med få typer har det måske ikke så stor betydning, men i et dybt eller bredt hierarki med mange typer, kan det betyde en meget stor grad af genbrug.
Tilgang til medlemmer
Du har tidligere lært om synlighed i typer. Et medlem kan være enten offentlig (public) eller privat (private):
Her kan metoden Test1 kun tilgås internt i klassen (eksempelvis fra Test2), og metoden Test2 kan tilgås både internt og eksternt:
I forbindelse med arv er der en ny mulighed kaldet beskyttet (protected), og med den åbnes der op for, at medlemmer kan tilgås i underklasser, men ikke udefra. Du kan se det som privat inden for hierarkiet.
Prøv at se følgende:
internal class Person
{
public string Navn { get; set; }
protected int Alder { get; set; }
private string CprNummer { get; set; }
public void Test1()
{
// Navn kan godt tilgås
// Alder kan godt tilgås
// CprNummer kan godt tilgås
}
}
internal class Elev : Person
{
public void Test2()
{
// Navn kan godt tilgås
// Alder kan godt tilgås
// CprNummer kan ikke tilgås
}
}
Klassen Person har tre egenskaber – en privat, en beskyttet og en offentlig. I underklasser (her Elev) kan både Navn og Alder tilgås, men CprNummer kan ikke. Det betyder ikke, at en Elev ikke har et CprNummer – for en Elev er jo en Person – men det betyder, at egenskaben ikke kan tilgås direkte.
Udefra ser det således ud:
Person p = new Person();
// p.Navn kan godt tilgås
// p.Alder kan ikke tilgås
// p.CprNummer kan ikke tilgås
Elev e = new Elev();
// e.Navn kan godt tilgås
// e.Alder kan ikke tilgås
// e.CprNummer kan ikke tilgås
Den eneste egenskab, der kan tilgås, er Navn, for den er offentlig. Hverken den beskyttede eller den private kan tilgås udefra.
Så synligheden kan beskrives således:
Synlighed | Beskrivelse |
---|---|
public (offentligt) | Medlemmer kan både tilgås intent og eksternt |
protected (beskyttet) | Medlemmer kan kun tilgås internt i alle klasser i et hierarki |
private (privat) | Medlemmer kan kun tilgås i klassen selv |
Konstruktører
Som før nævnt er en konstruktør en metode, der afvikles, når der skabes en ny instans, og den fungerer lidt specielt i et arvehierarki. En standard konstruktør (uden argumenter) afvikles i rækkefølge fra mor til barn. Se følgende kode:
class A
{
public A()
{
Console.WriteLine("I A()");
}
}
class B : A
{
public B()
{
Console.WriteLine("I B()");
}
}
Hvis der skabes en instans af B, bliver konstruktør i A kørt først, og herefter i B:
Lidt anderledes er det med en brugerdefineret (custom) konstruktør, fordi den ikke bliver nedarvet til børn – det skal du selv sørge for:
class Person
{
protected string navn;
public Person()
{
this.navn = "";
}
public Person(string navn)
{
this.navn = navn;
}
}
class Elev : Person
{
}
I klassen Elev kan den brugerdefinerede konstruktør fra Person ikke findes:
// Dette er ok
Elev e1 = new Elev();
// Dette er ikke ok fordi konstruktør ikke findes
// Elev e2 = new Elev("Mikkel");
Men du må naturligvis godt skabe en brugerdefineret konstruktør i Elev:
Nu kan den brugerdefinerede konstruktør benyttes:
Brug af base-kodeordet
I et arvehierarki kan man hurtigt komme til at gentage kode i konstruktører, og det går jo imod hele princippet ved nedarvning. Eksempelvis er kode som dette uheldigt:
class Person
{
protected string navn;
public Person(string navn)
{
this.navn = navn;
}
}
class Elev : Person
{
public Elev(string navn)
{
this.navn = navn;
}
}
Bemærk, at koden i konstruktørerne er ens, og det duer ikke. Der kunne jo også være kode, som validerer feltet.
Men det kan undgås ved at lade den ene konstruktør kalde mors konstruktør ved hjælp af base-kodeordet:
class Person
{
protected string navn;
public Person(string navn)
{
this.navn = navn;
}
}
class Elev : Person
{
public Elev(string navn) : base(navn)
{
// Eventuel yderligere kode
}
}
I Elev sendes argumentet navn videre til mor som bruges til at initialisere feltet navn. På den måde slipper man for gentaget kode.
En ludoterning
Det er tid til at finde din terning frem igen. Her er en simpel terning i kode:
class Terning {
public int Værdi { get; protected set; }
public void Ryst() {
System.Random rnd = new System.Random();
this.Værdi = rnd.Next(1, 7);
}
public Terning()
{
this.Ryst();
}
}
Forestil dig nu, at du skal bruge en anden type terning til et spil – eksempelvis en Ludo-terning. Det er jo en terning, men med metoder, som ikke har noget med en almindelig terning at gøre. I Ludo har en 3’er og en 5’er jo en anden værdi. Men ved brug af nedarvning er det meget simpelt at skabe en Ludo-terning:
class LudoTerning : Terning
{
public bool ErStjerne()
{
return this.Værdi == 3;
}
public bool ErGlobus()
{
return this.Værdi == 5;
}
}
Du kan nøjes med at tilføje de medlemmer, som gør terningen til en Ludo-terning – alle de andre medlemmer har den allerede. Nu kan du nemt spille Ludo:
LudoTerning l = new LudoTerning();
Console.WriteLine(l.Værdi);
Console.WriteLine(l.ErStjerne());
Console.WriteLine(l.ErGlobus());
Du kan selv prøve at kode en anden type terning – hvad med en pakkeleg-terning (her er en 6’er jo speciel)?