Gå til indholdet

Polymorfi

Polymorfi er et fint ord for flerformethed (mange former), og er måske det begreb som begyndere kæmper mest med i objektorienteret programmering. Det benyttes blandt andet i arvehierarkier og er i virkeligheden ikke så komplekst, hvis det underbygges med nogle eksempler.

Information til undervisere

Det er et af de sværeste emner inden for objektorienteret programmering, og det er vigtigt at bruge tid på at forklare og vise eksempler. Det er også vigtigt at forklare, at det er en af de vigtigste egenskaber ved objektorienteret programmering, og at det er en af de ting, der gør objektorienteret programmering så effektivt.

Brug gerne eksemplet med en samling af dyr, hvor du kan tilføje både hunde og fugle, og hvor du kan afvikle en metode på alle dyr. Det er et godt eksempel på polymorfi, og det er noget, som de fleste kan forholde sig til.

Du kan finde det under Summering nederst på denne side.

Selve ordet polymorfi passer meget fint til den funktionalitet, der stilles til rådighed, fordi det dækker over muligheden for, at runtime kan arbejde med et objekt på flere måder. Ved at benytte typer i et arvehierarki kan et objekt således have mange grænseflader og dermed forskellig funktionalitet. Rent praktisk betyder det, at en variabel kan indeholde en reference til mange forskellige objekter af typer i hierarki, og det er jo meget anderledes, end jeg tidligere har beskrevet, hvor en variabel af typen String, Random, Terning og eller andre kun kan indeholde en reference til objekter af denne type.

Polymorfi kommer grundlæggende i to former i C#:

  • Underklasser (børn) kan have deres egen implementation af metoder og egenskaber defineret i en overklasse (mor)
  • En variabel defineret af en overklasse (mor) kan altid pege på objekter af underklasser (børn).

Begge former kigger vi på i dette kapitel.

Børn har sin egen implementation

System.Object

Du skal ikke særlig langt ned i frameworkets store klassehierarki, før du falder over de to former for polymorfi. Alle klasser arver nemlig helt automatisk fra klassen System.Object. Det gælder både andre klasser i frameworket samt dine egne.

System.Object har seks metoder, som dermed er tilgængelig i alle andre klasser, men i grundlæggende C# er det kun ToString-metoden, der er interessant.

System.Object

ToString-metoden har til formål at returnere en streng, der repræsenterer objektet bedst muligt, men som udgangspunkt returnerer den blot navnet på klassen. Som eksempel kan du kode en klasse Terning som følger:

class Terning { }

Af klassen er det muligt at oprette instanser og kalde ToString:

Terning t = new Terning();
Console.WriteLine(t.ToString());    // [NameSpace].Terning

// Eller andre klasser
System.Random r = new Random();
Console.WriteLine(r.ToString());    // System.Random

Nogle klasser har dog deres egen implementation af ToString(), så metoden giver bedre mening:

string b = "x";
Console.WriteLine(b.ToString());    // x

Her returnerer ToString() ikke længere navnet på klassen, men værdien, og det indikerer jo, at metoden er ændret fra det oprindelige format i System.Object til koden i System.String. Man kalder dette en overskrivning (overwrite) af metoden, og det kræver at metoden, der ønskes overskrevet, er markeret med virtual-kodeordet og dermed virtuel. Den anden form for polymorfi (en mor kan altid pege på sine børn) er også meget tydelig, når man arbejder med System.Object. Forudsat at klassen Terning er defineret:

class Terning
{
    public int Værdi { get; private set; }
    public void Ryst()
    {
        System.Random rnd = new System.Random();
        this.Værdi = rnd.Next(1, 7);
    }
    public Terning()
    {
        this.Ryst();
    }
}

kan følgende kode kompilere uden problemer:

object o1 = new Terning();

Det giver jo umiddelbart ingen mening, før du ser arvehierarkiet:

Alt arver fra System.Object

Fordi Terning arver fra Object, har en terning alt, hvad Object har, og dermed kan en variabel af typen Object godt indeholde en reference til en Terning. Men variablen kan ikke tilgå medlemmer andre end medlemmer defineret på Object:

object t1 = new Terning();
Terning t2 = new Terning();

// Dette er ok
Console.WriteLine(t1.ToString());
Console.WriteLine(t2.ToString());
Console.WriteLine(t2.Værdi);

// Dette kan ikke kompilere
// Console.WriteLine(t1.Værdi);

At skabe variabler af superklasser giver en del muligheder – især i kombination med den forrige form for polymorfi. Blandt andet kan denne samling indeholde hvad som helst, fordi alt arver fra objekt:

List<Object> objekter = new List<object>();

Det skal vi også se nærmere på i dette kapitel.

Virtuelle metoder

Hvis du ønsker, at børn skal have sin egen implementation af et medlem, skal metoden i mor være virtuel. Det sker ved at markere metoden med virtual-kodeordet. Når det er sket, kan børn vælge, om de vil overskrive den eller lade være. Overskrivning af en metode sker med override-kodeordet:

class Mor {
    public virtual void Metode() { }
}

// Vælger ikke at overskrive metoden 
class Barn1 : Mor { }

// Vælger at overskrive metoden 
class Barn2 : Mor {
    public override void Metode() { }
}

I eksemplet vil kald til Metode i instanser af Barn1 afvikle Metode i Mor, medens kald til Metode i instanser af Barn2 vil afvikle Barn2’s egen implementation. Et praktisk eksempel er den virtuelle ToString-metode i System.Object-klassen, der som nævnt er mor for alt. Hvis du ikke overskriver metoden, vil den blot returnere en streng svarende til navnet på klassen:

using System;

namespace Demo
{
    internal class Program
    {
        private static void Main(string[] args)
        {
            Terning terning = new Terning();
            Console.WriteLine(terning.ToString());  // Demo.Terning
        }

    }

    class Terning
    {
        public int Værdi { get; private set; }
        public void Ryst()
        {
            System.Random rnd = new System.Random();
            this.Værdi = rnd.Next(1, 7);
        }
        public Terning()
        {
            this.Ryst();
        }
    }
}

Læg mærke til, at ToString-metoden blot returnerer navnet. Men du kan vælge at overskrive metoden, så den returnerer en mere logisk tekst:

using System;

namespace Demo
{
    internal class Program
    {
        private static void Main(string[] args)
        {
            Terning terning = new Terning();
            Console.WriteLine(terning.ToString());
            // Jeg er en terning med værdien [2]
            // (eller andet tilfældigt tal)

        }

    }

    class Terning
    {
        public int Værdi { get; private set; }
        public void Ryst()
        {
            System.Random rnd = new System.Random();
            this.Værdi = rnd.Next(1, 7);
        }
        public Terning()
        {
            this.Ryst();
        }

        public override string ToString()
        {
            return $"Jeg er en terning med værdien [{this.Værdi}]";
        }
    }
}

Bemærk overskrivning af ToString som betyder, at Terning har sin helt egen ToString.

Info

Kodeordene virtual og override hører sammen.

Du kan også vælge at skabe dine egne virtuelle metoder ved brug af virtual-kodeordet. Her er et eksempel på et arvehierarki, som definerer en terning med en almindelig Ryst-metode, og en speciel terning, der har sin helt egen implementation og henter en tilfældig værdi fra random.org:

class Terning
{
    public int Værdi { get; protected set; }
    public virtual void Ryst()
    {
        System.Random rnd = new System.Random();
        this.Værdi = rnd.Next(1, 7);
    }
    public Terning()
    {
        this.Ryst();
    }

    public override string ToString()
    {
        return $"Jeg er en terning med værdien [{this.Værdi}]";
    }
}

class SpecielTerning : Terning {
    public override void Ryst()
    {
        using (WebClient w = new WebClient())
        {
            string s = 
              w.DownloadString("https://www.random.org/integers/" +   
              "?num=1&min=1&max=6&col=1&base=10&format=plain&rnd=new");
            this.Værdi = Convert.ToInt32(s);
        }
    }
}

Nu vil den specielle terning afvikle sin egen version af Ryst og finde et tilfældigt tal fra nettet i stedet for fra Random-klassen.

Base

Hvis du gerne vil afvikle en metode i mor fra en metode i et barn, kan du vælge at benytte base-kodeordet:

class Mor {
    public override string ToString()
    {
        return "mor";
    }
}

class Barn : Mor {
    public override string ToString()
    {
        return "Jeg er et barn af " + base.ToString();
    }
}

Nu vil et kald til barnets ToString-metode returnere ”Jeg er et barn af mor”.

Opgaver

Abstrakte metoder

I nogle situationer ønsker du at skrive klasser, der ligger til grund for nedarvning, som ikke har sin egen implementation, men som kræver, at børn overskriver metoden. Det kaldes en abstrakt metode og er helt speciel på den måde, at mor ikke har nogen implementation overhovedet. Men hvis en klasse ikke har nogen implementation af en metode, må man heller ikke kunne skabe en instans af klassen. Derfor skal abstrakte metoder placeres i abstrakte klasser.

Abstrakte klasser og metoder kan defineres ved hjælp af abstract-kodeordet, og overskrives ved hjælp af override-kodeordet:

abstract class Mor
{
    public abstract void Metode1();
    public void Metode2() { 
        // Må gerne have sin egen metode
    }
    public virtual void Metode3() { 
        // Må også gerne være virtual
    }

}

class Barn : Mor
{
    public override void Metode1()
    {
        // kode...
    }
}

Bemærk, at der ikke er angivet nogen implementation i Metode1 i modsætning til Metode2 og Metode3. Kompileren vil sørge for, at underklasser skal overskrive Metode1, og underklasser kan vælge om de vil overskrive Metode3, og den vil også smide en fejl, såfremt du forsøger at skabe en instans af Mor. Det må du ikke, fordi klassen er abstrakt. Mor kan således kun benyttes til nedarvning.

Her er et andet eksempel på en abstrakt klasse Terning, som dermed skal ligge til grund for en nedarvet klasse. Klassen har en abstrakt metode Ryst, hvilket betyder, at underklasser skal implementere sin egen version af metoden. Men runtime kan være 100 % sikker på, at metoden eksisterer i alle underklasser – det skal kompileren nok sørge for:

abstract class Terning
{
    public int Værdi { get; protected set; }
    public abstract void Ryst();
    public Terning()
    {
        this.Ryst();
    }
}

class YatzyTerning : Terning
{
    public override void Ryst()
    {
        System.Random rnd = new Random();
        this.Værdi = rnd.Next(1, 7);
    }
}

Brugen af abstrakte og virtuelle metoder giver dig altså mulighed for at skabe et hierarki af klasser med forskellige former for implementering.

Opgaver

Mor kan altid pege på sine børn

De regler som kompileren sørger for, at du overholder i et arvehierarki, giver en anden mulighed for polymorfi, som mange kæmper med at forstå i starten – nemlig det faktum at en variabel af en overklasse altid kan indeholde en reference til instanser af sig selv og af instanser af underklasser. Det kan være svært at forstå, men i virkeligheden er det meget simpelt: et barn er jo en mor!

Eller sagt på en anden måde. De medlemmer, der findes i mor, findes altid i barnet og kan ikke fjernes. De kan overskrives, men aldrig fjernes. Derfor kan runtime være sikker på, at en variabel af typen mor altid vil have sine egne medlemmer tilgængelig i sine børn.

Se følgende simple eksempel:

class Mor
{
    public void Metode1()
    {
        // kode...
    }
}

class Barn : Mor
{
    public void Metode2()
    {
        // kode...
    }
}

Der er tale om et simpelt arvehierarki med et barn, der arver fra mor, og følgende kode er derfor fuldt lovlig:

// Dette er helt logisk 
Mor a = new Mor();
Barn b = new Barn();

// og ved nærmere eftertanke er dette 
// også logisk fordi er barn ER jo en mor
Mor c = new Barn();

// Dette duer til gengæld ikke
// Barn d = new Mor();

Det er variablen c, som er interessant. Den er af typen Mor, men må gerne indeholde en reference til et barn i hukommelsen (heap) fordi runtime kan være helt sikker på, at de medlemmer, som er i Mor med 100 % sikkerhed også er i Barn – herunder Metode1:

c.Metode1();

I Barn findes jo også Metode2, men denne kan ikke tilgås af en variabel af type Mor, for her er kun Metode1 synlig. Der er et objekt (en instans) af Barn i hukommelsen, men medlemmer fra Barn kan blot ikke ses af en variabel af typen Mor.

Diagrammet for variabel c ser således ud:

Det er vigtigt at forstå, at objektet af Barn ikke på nogen måde er ændret til noget andet – det bliver blot refereret af en variabel af en højere type i samme arvehierarki.

Typecheck og typekonvertering

Så – endnu engang – følgende kode er fuldt lovlig:

Mor a = new Barn();
a.Metode1();

og du ved, at objektet i hukommelsen i virkeligheden er af typen Barn og alt, hvad den nu består af, udover det den får fra Mor. Men da Mor kun kender til Metode1 – hvordan kan du så afvikle Metode2:

Mor a = new Barn();
// a.Metode2();    
// "Mor does not contain a definition for Metode2" exception

Det kræver en typekonvertering, og dette kræver kode fra din side – for denne kode fejler:

// Barn b = a; // Cannot implicit convert Barn to Mor

Men du har forskellige muligheder:

Barn b = a as Barn;
Barn c = (Barn)a;

Begge typekonverteringer vil kunne kompileres, men forskellen skal findes i eventuelle fejl. Ved brug af as-kodeordet vil variablen blive tildelt værdien null ved en eventuel fejl. Ved brug af () konverteringen vil der blive kastet en Exception, som du kan vælge at fange i en try/catch struktur.

Så hvis du er i tvivl, om en konvertering går godt, bør du kontrollere for en null-værdi:

Barn b = a as Barn;
if (b != null) { 

}

Der findes ligeledes en is-operator, du kan benytte til at kontrollere, om et objekt er af den type, du forventer. Følgende kode beviser i øvrigt også, at et Barn reelt er en Mor (og alt arver fra Object):

Mor a = new Barn();
Console.WriteLine(a is Object);  // true
Console.WriteLine(a is Mor);     // true
Console.WriteLine(a is Barn);    // true

Console.WriteLine(a is int);     // false
Console.WriteLine(a is string);  // false

Virtuelle metoder (igen)

Du har tidligere set, hvordan virtuelle (eller abstrakte) metoder kan give underklasser sin egen implementation, og kombinationen af dette samt det faktum at mor altid kan pege på sine børn, kan bruges til mange ting:

class Mor
{
    public virtual void Metode1()
    {
        Console.WriteLine("Jeg er mor");
    }
}

class Barn : Mor
{
    public override void Metode1()
    {
        Console.WriteLine("Jeg er barn");
    }
}

Med udgangspunkt i disse klasser kan følgende kode afvikles:

Mor a = new Mor();
a.Metode1();            // Jeg er mor
Barn b = new Barn();
b.Metode1();            // Jeg er barn
Mor c = new Barn();
c.Metode1();            // Jeg er barn

Det er variabel c, du skal fokusere på. Den er af typen Mor, men indeholder en reference til et objekt af typen Barn. Når den virtuelle og overskrevne Metode1 afvikles, er det barnets metode, der afvikles. Det er derfor denne feature i objektorienteret programmering hedder polymorfi (mange former), og den giver en masse fordele.

Polymorfi og typestærke samlinger

Hvis du kan skabe et arvehierarki med virtuelle eller abstrakte metoder, har du også mulighed for at udnytte polymorfi i typestærke samlinger:

class Mor
{
    public virtual void Metode1()
    {
        Console.WriteLine("Jeg er mor");
    }
}

class Barn1 : Mor
{
    public override void Metode1()
    {
        Console.WriteLine("Jeg er barn1");
    }
}

class Barn2 : Mor
{
    public override void Metode1()
    {
        Console.WriteLine("Jeg er barn2");
    }
}

I stedet for en samling (array, liste, stak, kø med videre) af konkrete typer kan du skabe en samling af en overklasse og tilføje objekter af underklasser – de er jo alle sammen en mor:

List<Mor> lst = new List<Mor>();
lst.Add(new Barn1());
lst.Add(new Barn2());
lst.Add(new Mor());
lst.Add(new Barn1());
lst.Add(new Barn2());

foreach (Mor i in lst)
    i.Metode1();

/*
    Jeg er barn1
    Jeg er barn2
    Jeg er mor
    Jeg er barn1
    Jeg er barn2 
*/

Da metoden Metode1 er overskrevet i alle børn, vil den rigtige metode blive afviklet helt automatisk.

Med klasserne Mor, Barn1 og Barn2 er det hele lidt abstrakt, men tænk over hvordan det kan udnyttes i en rigtig applikation. I et ERP-system (Enterprise Resource Planning), med et hierarki med klassen Virksomhed som mor, kan en samling af denne type indeholde alle andre typer i hierarkiet (debitor, kreditor, partner med videre). Hvis disse typer har overskrevne metoder som Hent, Gem, Print eller SendFaktura giver det en effektiv mulighed for at afvikle metoder på alle objekter. Hvis metoderne i Virksomhed er virtuelle, kan de overskrives, hvis det er nødvendigt, men Virksomhed kunne også være en abstrakt klasse, så man ikke kan skabe en instans af denne, men kun af underklasser. Så kunne metoderne også være abstrakte og dermed skabes et krav om, at underklasser skal have deres egen implementation af metoderne. Men runtime kan stadig være sikker på, at metoderne findes – og det er hele essensen i polymorfi.

Opgaver

Boxing og Unboxing i C#

Du vil uden tvivl falde over begreberne “boxing” og “unboxing” - her er en kort forklaring.

Boxing

Boxing er processen med at konvertere en værditype (som int, float eller bool) til en referencetype, nemlig System.Object eller en af dens afledte klasser. Når boxing forekommer, allokeres et nyt objekt på heapen, og værditypen kopieres til dette objekt.

int i = 42;
object boxed = i; // Boxing

Unboxing Unboxing er processen med at konvertere et objekt af en referencetype tilbage til en værditype. Dette kræver, at referencetypen faktisk indeholder en værdi af den ønskede værditype. Unboxing er en eksplicit konvertering og kan resultere i en InvalidCastException, hvis den underliggende værditype ikke stemmer overens med den ønskede type.

object boxed = 42; // Boxing
int i = (int)boxed; // Unboxing

Ydeevne og Anvendelse

Boxing og unboxing kan have en negativ indvirkning på ydeevnen, da de medfører ekstra omkostninger ved hukommelsesallokering og kopiering af værdier. Derfor er det en god praksis at undgå unødig boxing og unboxing, når det er muligt.

For at minimere unødig boxing og unboxing kan der bruges generiske klasser og metoder, som giver mulighed for at arbejde med værdityper uden at skulle konvertere dem til referencetyper.

List<int> intList = new List<int>(); // Ingen boxing nødvendig, da List<T> er generisk

Ved at forstå boxing og unboxing kan der skrives mere effektiv kode og undgås potentielle fejl ved at arbejde med værdi- og referencetyper.

Info

En af de bedste metoder til at undgå boxing og unboxing er at minimere brugen af object-typen. Ved at bruge generiske typer og undgå at omdanne værdityper til referencetyper (såsom object), kan man reducere de potentielle ydelsesomkostninger ved boxing og unboxing.

Overordnet eksempel

For at gøre teorien så nem at forstå som overhovedet mulig kan du også tage udgangspunkt i dette eksempel:

Kæledyr
internal class Kæledyr {
    public string Navn { get; set; }
    public string Kendelyd() {
        return "?";
    }
}

internal class Fugl : Kæledyr {
    public bool HåndTam { get; set; }
}

internal class Hund : Kæledyr {
    public string HundeRegisterId { get; set; }
}

Der kan skabes en instans af alle tre klasse og KendeLyd() kan kaldes:

Kæledyr k = new Kæledyr() { Navn = "a" };
Fugl f = new Fugl() { Navn = "Hans", HåndTam = true };
Hund h = new Hund() { Navn = "Mille", HundeRegisterId = "1" };
Console.WriteLine(k.Kendelyd());    // ?
Console.WriteLine(f.Kendelyd());    // ?
Console.WriteLine(h.Kendelyd());    // ?

Virtuelle metoder

Det var bedre hvis både Fugl og Hund havde deres egen implementation af KendeLyd():

internal class Kæledyr
{
    public string Navn { get; set; }
    public virtual string Kendelyd()    // bemærk - virtual
    {
        return "?";
    }
}

internal class Fugl : Kæledyr
{
    public bool HåndTam { get; set; }
    public override string Kendelyd()   // bemærk override
    {
        return "Pip";
    }
}

internal class Hund : Kæledyr
{
    public string HundeRegisterId { get; set; }
    public override string Kendelyd()   // bemærk override
    {
        return "Vov";
    }
}

Nu giver samme kode fra før et noget anderledes resultat:

Kæledyr k = new Kæledyr() { Navn = "a" };
Fugl f = new Fugl() { Navn = "Hand", HåndTam = true };
Hund h = new Hund() { Navn = "Mille", HundeRegisterId = "1" };
Console.WriteLine(k.Kendelyd());    // ?
Console.WriteLine(f.Kendelyd());    // Pip
Console.WriteLine(h.Kendelyd());    // Vov

Bemærk at:

  • KendeLyd() i Dyr er markeret med virtual
  • KendeLyd() i underklasser er markeret med override
  • Runtime finder selv den korrekte metode at afvikle.

Abstrakte metoder

Nogle gange giver det mening at sikre, at man ikke skabe en instans af en klasse - eksempelvis klassen Kæledyr. Ved at gøre klasse abstrakt kan man ikke skabe en instans men udelukkende arve fra den:

internal abstract class Kæledyr
{
    public string Navn { get; set; }
    public virtual string Kendelyd()
    {
        return "?";
    }
}

internal class Fugl : Kæledyr
{
    public bool HåndTam { get; set; }
    public override string Kendelyd()
    {
        return "Pip";
    }
    public override string ToString()
    {
        return $"Jeg er en fugl der hedder {Navn} og siger {Kendelyd()}";
    }
}

internal class Hund : Kæledyr
{
    public string HundeRegisterId { get; set; }
    public override string Kendelyd()
    {
        return "Vov";
    }
    public override string ToString()
    {
        return $"Jeg er en hund der hedder {Navn} og siger {Kendelyd()}";
    }

}

Nu vil compileren smide en fejl hvis man forsøger at skabe en instans af Kæledyr.

Ved at gøre klassen abstrakt kan man også skabe abstrakte metoder. Det gør det muligt (som virtual) at overskrive en metode, men fordi klassen er abstrakt (og man dermed ikke kan skabe en instans), er der ingen grund til implementering i den abstrakte klasse. Til gengæld skal metodens signatur beskrives:

internal abstract class Kæledyr
{
    public string Navn { get; set; }
    public abstract string Kendelyd();  // bemærk - ingen implementering
}

internal class Fugl : Kæledyr
{
    public bool HåndTam { get; set; }
    public override string Kendelyd()
    {
        return "Pip";
    }
    public override string ToString()
    {
        return $"Jeg er en fugl der hedder {Navn} og siger {Kendelyd()}";
    }
}

internal class Hund : Kæledyr
{
    public string HundeRegisterId { get; set; }
    public override string Kendelyd()
    {
        return "Vov";
    }
    public override string ToString()
    {
        return $"Jeg er en hund der hedder {Navn} og siger {Kendelyd()}";
    }

}

På den måde slipper vi også af med den tåbelige KendeLyd() i Kæledyr som jo blot returnerede “?” - det gav jo ikke rigtig nogen mening.

Referencer

Der kan altid skabes en reference til et barn gennem mors type (også - eller måske især - hvis den er abstrakt). Med udgangspunkt i følgende:

internal abstract class Kæledyr
{
    public string Navn { get; set; }
    public abstract string Kendelyd();
}

internal class Fugl : Kæledyr
{
    public bool HåndTam { get; set; }
    public override string Kendelyd()
    {
        return "Pip";
    }
    public override string ToString()
    {
        return $"Jeg er en fugl der hedder {Navn} og siger {Kendelyd()}";
    }
}

internal class Hund : Kæledyr
{
    public string HundeRegisterId { get; set; }
    public override string Kendelyd()
    {
        return "Vov";
    }
    public override string ToString()
    {
        return $"Jeg er en hund der hedder {Navn} og siger {Kendelyd()}";
    }
}

kan man godt skrive følgende kode:

Fugl f = new Fugl() { Navn = "Hans", HåndTam = true };
Hund h = new Hund() { Navn = "Mille", HundeRegisterId = "1" };
// Bemærk - En variabel der kan pege på "mor" kan også pege på dens "børn"
Kæledyr k1 = new Fugl() { Navn = "Svend", HåndTam = false  };
Kæledyr k2 = new Hund() { Navn = "Brutus", HundeRegisterId = "2" };

Console.WriteLine(f.ToString());     // Jeg er en fugl der hedder Hans og siger Pip
Console.WriteLine(h.ToString());     // Jeg er en hund der hedder Mille og siger Vov
Console.WriteLine(k1.ToString());    // Jeg er en fugl der hedder Svend og siger Pip
Console.WriteLine(k2.ToString());    // Jeg er en hund der hedder Brutus og siger Vov

Kigger man nærmere på k1 og k2 er det jo variabler som kan pege på et Kæledyr, men da både Hund og Fugl jo ER et kæledyr kan det lade sig gøre. Og på heap’en ligger der objekter af både Hund og Fugl - vi kan blot ikke se medlemmer relateret til Hund og Fugl for vores variabel jo er af typen Kæledyr.

Brug af virtuelle/abstrakte medlemmer, samt det faktum at “mor” altid kan pege på “børn”, kan blandt udnyttes i samlinger.

Her er en liste af kæledyr, og da både en Hund og en Fugl ER kæledyr kan de tildeles samme liste/array/kø/stack mv:

List<Kæledyr> lst = new List<Kæledyr>();
lst.Add(new Fugl() { Navn = "Hans", HåndTam = true });
lst.Add(new Hund() { Navn = "Mille", HundeRegisterId = "1" });
lst.Add(new Fugl() { Navn = "Svend", HåndTam = false });
lst.Add(new Hund() { Navn = "Brutus", HundeRegisterId = "2" });
foreach (var item in lst)
    Console.WriteLine(item.ToString());

/*
Jeg er en fugl der hedder Hans og siger Pip
Jeg er en hund der hedder Mille og siger Vov
Jeg er en fugl der hedder Svend og siger Pip
Jeg er en hund der hedder Brutus og siger Vov
*/

Shadowing

Man kan gemme (ikke fjerne) en metode for runtime ved ikke at bruge override (brug new i stedet). Det er dog sjældent det giver mening - men for en god ordens skyld er her et eksempel. Med udgangspunkt i følgende:

internal abstract class Kæledyr
{
    public string Navn { get; set; }
    public virtual string Kendelyd() {
        return "?";
    }
}

internal class Fugl : Kæledyr
{
    public bool HåndTam { get; set; }
    public new string Kendelyd()        // bemærk new og ingen override
    {
        return "Pip";
    }
}

Nu “gemmes” KendeLyd() væk når referencevariablen er af mor’s type:

Fugl fugl = new Fugl() { Navn = "Hans", HåndTam = true };
Console.WriteLine(fugl.Kendelyd());     // Pip

Kæledyr kæledyr = new Fugl() { Navn = "Hans", HåndTam = true };
// Bemærk, at fuglens KendeLyd "gemmes væk" og mors KendeLyd afvikles i stedet
Console.WriteLine(kæledyr.Kendelyd());  // ?

Bemærk - det er sjældent det giver reel mening i en applikation.