© 2006, Flemming Koch Jensen
Alle rettigheder forbeholdt
Klasser

 

 

Når det drejer sig om klasser, nedarvning og polymorfi lægger C# sig tæt op ad C++. Generelt kan man sige at nedarvning i C# er mere kompliceret end i Java, idet det som udgangspunkt er referencens type der bestemmer hvilke metoder der kaldes - dette er formodentlig et forsøg på at undgå dynamisk dispatch. Efter min mening er prisen for høj - det bliver for ulogisk og kompliceret at arbejde med.

 

1. Klasser

I dette afsnit vil vi se på klasser - uden nedarvning.
Klasser i C# ligner meget klasser i C++/Java.
this

Betydningen af this er den samme, både når det gælder som reference til objektet selv og som kald af andre konstruktorer, i samme klasse.

static Ligesom man i C++/Java kan bruge statiske variable og metoder som en slags klasse-variable og klasse-metoder, kan man også gøre det i C#. Betydningen af static i C# er nemlig den samme som i C++/Java.
public og internal En klasse er enten public eller internal. Dette relaterer til den assembly som klassen befinder sig i. Hvis den er public er den tilgængelig udenfor sin assembly. Hvis den er internal, er den kun tilgængelig indenfor.
internal kan også bruges på instansvariable og metoder, der derved bliver public indenfor assembly'en - dette ligner lidt package scope i Java.

 

1.1 Konstruktorer

Konstruktorerne er meget lig dem man kender fra C++/Java. De kan overloades på sædvanlig vis (hvilket også gælder for metoder) og de kan kalde hinanden i stil med C++'s syntaks:
class A {
  int x;
  
  public A(): this( 0 ) {
  }
  
  public A( int x ) {
    this.x = x;
  } 
}
Vi ser her begge anvendelser af det reserverede ord: this. Det skal bemærkes at kaldet af den anden konstruktor ikke kan står som første linie i konstruktoren, som man ellers kender det fra Java.
Destructor Som modstykke til konstruktorer har C# også destruktorer. Disse er helt i stil med Java's finalize-metoder, da C# også arbejder med en garbage collector. I C# bliver finalize-metoder angivet på samme måde som destruktorer i C++ - dvs. som en kontruktor med '~' (tilde) foran klassenavnet. F.eks.:
class A {
  ...
  
  public ~A() {
    ...
  }
}
Ligesom i Java, bliver destructoren først udført når garbage collectoren finder anledning til at fjerne objektet.

 

1.2 public, private og protected

For variable og metoder gælder følgende regler for tilgængeligheden.
protected som i C++ public og private betyder det samme som i C++/Java. Betydningen af protected er den samme som i C++, og dermed ikke den samme som i Java. protected betyder at noget er public i forhold til sine subklasser, men private i forhold til alle andre klasser.
Default er private - dvs. skriver man ikke nogen angivelse bliver det pågældende private (det svarer til C++ hvor private også er default - men syntaksen en anden)
internal eller protected internal er tidligere nævnt, men der findes også en særlig internal protected. internal protected skal ikke læses som 'og', men som 'eller'. Hvis noget er internal protected, er det tilgængeligt hvis det ville være det som internal eller som protected.

 

1.3 Properties

set/get-metoder Alle kender det irriterende i at skulle lave set/get-metoder. De er typisk meget ens, og man bliver fristet til at lave public variable i stedet - hvilket som bekendt er en dødssynd. I C# har man lavet en sproglig konstruktion: properties, der skal gøre det nemmere, eller mere systematisk, at lave set/get-metoder.
For den der bruger en instans af en klasse med properties, optræder disse som tilsyneladende public instans-variable.

Lad os se et eksempel:

using System;

class Tal {
  int tal;
  
  public Tal( int tal ) {
    this.tal = tal;
  } 
  
  public int Nr {
    get {
      return tal;
    }
    set {
      tal = value;
    } 
  } 
} 
  
class Test {
  public static void Main() {
    Tal t = new Tal( 5 );
    
    t.Nr = 8;
    Console.WriteLine( t.Nr );
  } 
}
8
value har en speciel betydning i set - det er den "parameter" som kommer ind med værdien. Den har samme type som propertien.
I eksemplet er der en direkte sammenhæng mellem en property og en instansvariabel - som der ofte er i set/get-metoder - men det behøver der ikke være. En property er blot syntaktisk sukker for set/get-metoder og man har den samme frihed som hvis man havde lavet metoderne "manuelt".
I eksemplet har vi lavet både en set og en get. Man kan udelade den ene; hvis det giver bedre mening - man ser f.eks. ofte properties der kun har en get.
Man kan også lave statiske properties - f.eks.:
using System;

class Matematik {
  public static double PI {
    get {
      return 3.141592653;
    }
  } 
} 
  
class Test {
  public static void Main() {
    Console.WriteLine( Matematik.PI );
  } 
}
3.141592653
Dette er blot et eksempel på en statisk property, man vil naturligvis ikke lave et opslag af pi på denne måde i virkeligheden.

 

1.4 Index'ere

Arraylignende Index'ere bruges til at gøre containerlignende klasser til arraylignende klasser. Index'ere ligner meget properties - man kunne kalde dem indekserede properties. Når man har lavet en index'er for en klasse, kan man arbejde med instanser af den, som om de var arrays - også syntaksen er som ved arrays.
Lad os se et eksempel:
using System;

class ToTal {
  int x1, x2;
  
  public ToTal( int x1, int x2 ) {
    this.x1 = x1;
    this.x2 = x2;
  } 
  
  public int this[ int index ] {
    get {
      switch ( index ) {
        case 0:
          return x1;
        case 1:
          return x2;
        default:
          return -1;
      } 
    }
    set {
      switch ( index ) {
        case 0:
          x1 = value;
          break;
        case 1:
          x2 = value;
          break;
      } 
    } 
  } 
} 
  
class Test {
  public static void Main() {
    ToTal ft = new ToTal( 2, 3 );
    
    for ( int i=0; i<2; i++ )
      Console.Write( "{0} ", ft[i] );
    Console.WriteLine(); 
    
    ft[0] = ft[1] + 5;
    ft[1]++;
    
    for ( int i=0; i<2; i++ )
      Console.Write( "{0} ", ft[i] );
    Console.WriteLine(); 
  } 
}
2 3
8 4
Klassen ToTal er en container med to integer's som vi med en index'er kan tilgå, som om de var to tal i et array.
Man genkender flere træk fra properties:
- At den har en type (int)
- Navnene get og set
- At man bruger return fra get
- Anvendelsen af value i set
Alt dette fungerer på samme måde som for properties. get bruges til at få en værdi ud - dvs. når der læses fra en indgang i array'et, og set bruges til at sætte en værdi - dvs. når der sker et assignment til en indgang i array'et.
Linien med:
... this[ int index ] ...
giver os det index som i begge tilfælde anføres mellem de kantede paranteser. I både set og get optræder den som en slags "parameter".
Selv om ovenstående eksempel er lidt specielt, med kun to tal i en container, er det alligevel meget traditionelt i sin opfattelse af et array. Lad os se et eksempel, der illustrerer mere af den frihed man har at arbejde med. Vi vil udvide det foregående eksempel med endnu en index'er og lave en anden testanvendelse:
using System;

class ToTal {
  ... 
  
  public int this[ int index ] {
    ... 
  }
  
  public int this[ string key ] {
    get {
      switch ( key ) {
        case "den ene":
          return x1;
        case "den anden":
          return x2;
        default:
          return -1;
      } 
    }
    set {
      switch ( key ) {
        case "den ene":
          x1 = value;
          break;
        case "den anden":
          x2 = value;
          break;
      } 
    } 
  } 
} 
  
class Test {
  public static void Main() {
    ToTal ft = new ToTal( 2, 3 );
    
    Console.WriteLine( "{0} {1}", ft[ "den ene" ], ft[ "den anden" ] );
    
    ft[ "den ene" ] = ft[ "den anden" ] + 5;
    ft[ "den anden" ]++;
    
    Console.WriteLine( "{0} {1}", ft[ "den ene" ], ft[ "den anden" ] );
  } 
}
2 3
8 4
Som man ser, kan index være andet end et tal - det kan være af hvilken som helst type - også et objekt.
Overloading Samtidig ser vi, at der kan være flere forskellige index'ere for den samme klasse. Der kræves blot at de kan skelnes på index-typen - dvs. at de overloader hinanden.

 

1.4.1 Flerdimensionelle index'ere

Det er også muligt at gøre index'ere flerdimensionelle. F.eks.:
using System;

class LilleTabel {
  
  public int this[ int index1, int index2 ] {
    get {
      return index1*index2;
    }
  } 
} 
  
class Test {

  public static void Main() {
    LilleTabel tabel = new LilleTabel();
    
    Console.WriteLine( tabel[ 6, 8 ] );
    Console.WriteLine( tabel[ 4, 9 ] );
    Console.WriteLine( tabel[ 1, 3 ] );
    Console.WriteLine( tabel[ 2, 5 ] );
  } 
}
48
36
3
10
Eksemplet laver blot den lille tabel, men illustrerer hvordan man kan bruge flerdimensionelle index'ere. Bemærk, at der anvendes C#'s almindelige syntaks for angivelse af flere index (eg. [...,...,...]).

 

1.5 static

static Statiske instans-variable og metoder fungerer som i Java.
Man kan også gøre en default-konstruktor static, så fungerer den på samme måde som et statisk initialiseringsfelt i Java.

 

1.6 Konstanter

const Mens man i Java ofte bruge static final til at erklære konstanter, bruger man i C# at erklære en variabel const. F.eks.:
using System;

public class Matematik {
  public const double PI = 3.141592653;
  
  public void PrintPI() {
    Console.WriteLine( PI );
  }
}

public class Test {
  public static void Main() {
    Console.WriteLine( Matematik.PI );
    new Matematik().PrintPI();
  } 
}
3.141592653
3.141592653
Bemærk at en const skal initialiseres i erklæringen.
Dette kræver dog at konstantens værdi eksisterer på compilerings-tidspunktet. Er der f.eks. tale om en reference, som man ønsker konstant skal referere til den samme instans, kan const ikke bruges - der eksisterer jo ingen instanser på compilerings-tidspunktet.
readonly I stedet bruger man readonly. En variabel der er readonly kan kun tildeles en værdi ved initialiseringen eller fra en konstruktor. F.eks.:
using System;

public class A {

  public void Print() {
    Console.WriteLine( "Hello World" );
  }
}

public class B {
  public readonly A helloSayer;
  
  public B() {
    helloSayer = new A();
  } 
    
  public void Print() {
    helloSayer.Print();
  }
}

public class Test {
  public static void Main() {
    B b = new B();
    
    b.Print();
    b.helloSayer.Print(); 
  } 
}
Hello World
Hello World

 

2. Nedarvning og polymorfi

Man kalder også superklasser for baseklasser. Da det reserverede ord base optræder i C# i denne betydning, vil vi vælge at kalde dem baseklasser.
I C# kan man, svarende til i Java, kun nedarve fra én (rigtig) klasse, samt et vilkårligt antal interfaces.

Nedarvning fra en klasse angives som i C++ med kolon. Hvis vi f.eks. har:

skrives dette i C# som:
class B: A {
  ...
}
sealed Hvis man ønsker at forhindre nedarvning kan man svarende til Java's final, bruge det reserverede ord sealed. Forsøger man alligevel, får man en compiler-fejl.
Nestede klasser Ligesom i Java kan man have nestede klasser, og betydningen er den samme. Nestede klasser i C# kan være enten: public, protected, private, internal eller internal protected, med samme betydning for tilgængeligheden som for variable og metoder
Properties Properties kan nedarves som andre metoder og man kan angive virtual, abstract eller override for dem (vedrørende betydningen af disse tre - se senere). Dette er ikke overraskende, da properties blot er syntaktisk sukker for set/get-metoder.

 

2.1 Konstruktorer

Ligesom i C++/Java nedarves konstruktorer ikke (rigtig). Man kan kalde en konstruktor i en baseklasse fra en direkte subklasse med følgende syntaks - (lidt) i stil med C++:
public B( ... ): base( ... ) {
  ...
}
base virker på denne måde ligesom super i Java.
Der er ikke mulighed for at kalde længere op i klassehierarkiet, som man ellers kender det fra C++.
Ligesom i Java vil en konstruktor uden et kald til en af baseklassens konstruktorer få et kald af defaultkonstruktoren som det første (compileren indsætter det).
Der laves en automatiske default-konstruktor, hvis man ingen konstruktorer laver i en klasse - som det er kendt fra Java

 

2.2 class object

Ligesom i Java nedarver alle klasse direkte eller indirekte fra en fælles super-klasse object. I C# staves den underlig nok med lille (ligesom string forøvrigt), selvom det bryder med konventionen. Ligesom i Java har klassen en række metoder der er mere eller mindre interessant:

public virtual bool Equals( object obj )
public ~object()
public virtual int GetHashCode()
public Type GetType()
protected object MemberwiseClone()
public string ToString()
Equals, GetHashCode og ToString kender vi tilsvarende fra Java.
Reflection GetType svarer til getClass i Java (og Type svarer tilsvarende til Class) - vi vil se nærmere på dette i kapitlet Reflection.
Kloning MemberwiseClone svarer til clone i Java. Analogt til Java's Cloneable interface, skal man implementere ICloneable for at kunne klone objekter.
Der er også to statiske metoder:
public static bool Equals( object objA, object objB )
public static bool ReferenceEquals( object objA, object objB )
Grunden til at der kan være behov for en metode: ReferenceEquals, der sammenligner referencer, er at == kan være overloaded, og derfor uanvendelig til formålet. Man kan dog alternativt caste begge referencer til object og dernæst sammenligne dem med ==; hvilket vil give samme resultat.

 

2.3 new, virtual og override

 

2.3.1 Eksempel: A-B

Man kan bruge et simpelt eksempel til at demonstrere flere af mulighederne i nedarvning. Eksemplet er som følger:
Generelt eksempel
using System;

class A {

  public A void Print() {
    Console.WriteLine( "A.Print" );
  }
}

class B: A {
  
  public B void Print() {
    Console.WriteLine( "B.Print" );
  }
}

class Test {
  public static void Main() {
    B b = new B();
    A a = (A) b;
    
    a.Print();
    b.Print(); 
  }
}
I kildeteksten er der placeret to tegn: A og B. Vi vil se hvad der sker når vi anvender forskellige reserverede ord på disse pladser (der er altså ikke tale om klasserne A og B).
Det vil være en god idé at kopiere dette eksempel over i en file, og efterprøve de muligheder vi i det følgende gennemgår - det giver en bedre føling med tingene.
Lad os første se hvad der sker hvis der ikke placeres noget ved A og B.
A <ingenting>
... warning CS0108: The keyword new is required
on 'B.Print()' because it hides inherited member
'A.Print()'
B

<ingenting>

Compileren giver os en warning (det skal bemærkes at en warning ikke forhindrer os i at køre programmet - det bliver compileret!). Den vil gerne have at vi skriver new ved B; hvilket er hvad den regner med vi i virkeligheden mener:
A <ingenting>
A.Print
B.Print
B

new

Nu er compileren tilfreds, men hvad betyder new i denne sammenhæng?
Først og fremmest kan vi se at det er referencens type, som bestemmer hvilken af de to Print-metoder der bliver kaldt. Det er ligesom i C++ den grundlæggende tanke; hvis man ikke specificerer, at det skal være anderledes.
Compileren var i tvivl om hvad vi mente, og ville gerne have os til at præcisere det med new. new betyder ikke at vi ved at erklære B.Print overrider/erstatter A.Print. new betyder at vi skjuler den nedarvede metode. Hvis der anvendes en B-reference, vil A.Print være skjult. Idéen er, at hvis man ikke ved, at der findes noget der hedder A, så opdager man det heller ikke ved at kalde Print - det er skjult!
Hvis man kun kender Java, skal man vende sig lidt til tanken, da dette slet ikke er muligt i Java. I Java er alle metoder virtuelle, så lad os prøve at gøre A.Print virtuel:
A virtual
A.Print
B.Print
B

new

Skuffende nok, sker der nøjagtig det samme. Det skyldes, at new stadig vælger at hide/skjule i stedet for at override. Vil vi override må vi ændre new til override:
A virtual
B.Print
B.Print
B

override

Og vi har genfundet polymorfien. Det er nu objektet der bestemmer hvilken metode der udføres - ikke referencen. Hvis vi samtidig ændrer testanvendelsen til:
class Test {
  public static void Main() {
    A a;
    
    a = new A();
    a.Print();

    a = new B();
    a.Print(); 
  }
}
A.Print
B.Print
bliver polymorfien helt tydelig.

 

2.3.1 Eksempel: A-B-C

Vi vil udvide eksemplet ovenfor, med endnu et subklasse C, for at se en speciel egenskab ved virtual:
using System;

class A {

  public A void Print() {
    Console.WriteLine( "A.Print" );
  }
}

class B: A {
  
  public B void Print() {
    Console.WriteLine( "B.Print" );
  }
}

class C: B {
  
  public C void Print() {
    Console.WriteLine( "C.Print" );
  }
} 
  
class Test {
  public static void Main() {
    C c = new C();
    B b = (B) c;
    A a = (A) c;
    
    a.Print();
    b.Print(); 
    c.Print(); 
  }
}
Vi har nu tre steder, hvor vi vil indsætte forskellige ord.
Det vil (igen) være en god idé at kopiere dette eksempel over i en file, og efterprøve de muligheder vi i det følgende gennemgår.
Vi vil starte med
A virtual
A.Print
C.Print
C.Print
B

virtual

C

override

Det skal bemærkes, at ovenstående ved compilering giver en warning. Vi vender tilbage til betydningen af denne warning om lidt.
Med alle de virtuelle metoder skulle man tro, at udskriften ville være:
C.Print
C.Print
C.Print
virtual metode skjuler virtual metode Hvorfor er det Print i A der udføres når den er virtuel? Forklaringen er, at en metode der erklæres virtual ikke kan override en anden virtual metode - den hider/skjuler den i stedet. Hver gang vi erklærer en metode virtual, laver vi en ny "rod" i nedarvnings-hierarkiet mht. dispatching af metode-kaldet. Derfor lever B og C til dels i deres egen verden mht. Print-metoden, i forhold til A (Dette er en fremmed tankegang i forhold til Java)
Som nævnt får vi en warning, fordi compileren netop er usikker på om vi ved hvad vi gør. For at undgå en warning skal man sætte et new foran virtual:
A virtual
A.Print
C.Print
C.Print
B

new virtual

C

override

Hvordan opnår vi så det forventede - at det er den samme virtuelle Print-metode hele vejen ned igennem hierarkiet? Det gøres ved kun at bruge virtual ved den første erklæring, og dernæst override ved alle andre:
A virtual
C.Print
C.Print
C.Print
B

override

C

override

Nu er det objektet, som er afgørende for hvilken metode der kaldes.

 

2.4 Overloading

Overloading i C# fungerer stort set på samme måde som det kendes fra Java. Man kan både overloade indenfor den samme klasse, og ved nedarvning. F.eks.:

 

using System;

public class A {

  public void Print( string s ) {
    Console.WriteLine( s );
  }
}

public class B: A {

  public void Print( int i ) {
    Console.WriteLine( i );
  }
}

public class Test {
  public static void Main() {
    B b = new B();
    
    b.Print( 5 );
    b.Print( "C#" );
  }
}
5
C#
Man ser hvordan instanser af B ikke gør nogen forskel på de to metoder - man kan ikke se, om de begge er erklæret i B, eller ej (det sidste er tilfældet).
Man bemærker at C# adskiller sig fra C++ i forbindelse med dispatching, idet C# ikke stopper sin søgen op i klasse-hierarkiet; hvis den finder en funktion med det rigtige navn, men først når den finder en signatur der kan bruges.
Der er yderligere en detalje i dispatching af kaldet som er lidt speciel. Det specielle er, at søgen allerede stopper når der findes en metode der kan håndtere kaldet ved implicit konvertering af parametrene. Dvs. at den ikke danner sig et overblik over alle mulige funktioner!
Lad os se et eksempel:

 

using System;

public class A {

  public void Print( byte b ) {
    Console.WriteLine( "Print( byte ): " + b );
  }
}

public class B: A {

  public void Print( int i ) {
    Console.WriteLine( "Print( int ): " + i );
  }
}

public class Test {
  public static void Main() {
    B b = new B();
    byte t = 10;
    
    b.Print( t );
  }
}
Print( int ): 10
Her ser man at det er B.Print, der bliver kaldt, selvom A.Print i virkeligheden passer bedre til den aktuelle parameters type.

 

2.5 Operatorerne is og as

 

2.5.1 is

Operatoren is fungerer som instanceof i Java - f.eks.:
if ( t is Tal )
  Console.WriteLine( "En instans af Tal" );
else
  Console.WriteLine( "Ikke en instans af Tal" ); 

 

2.5.2 as

Operatoren as virker som en slags "try-casting". Hvis casting ikke lykkes bliver resultatet null. F.eks.:

Tal tal = obj as Tal;
Hvis obj er en reference til et objekt, hvis klasse er mere eller mindre ukendt for os, og obj ikke refererer til en instans af klassen Tal bliver tal sat til null, ellers refererer den efterfølgende til det samme objekt som obj
Det svarer fuldstændig til følgende:
Tal tal;

if ( obj is Tal )
  tal = (Tal) obj;
else
  tal = null;

 

3. Abstrakte klasser og interfaces

Ligesom i C++/Java kan klasser være abstrakte, og ligesom i Java er et interface en abstrakt klasse, hvor ingen af metoderne er implementeret.

 

3.2 Abstrakte klasser

Ligesom i Java Både klasser og metoder erklæres abstrakte på samme måde som i Java. Ligesom i Java skal en klasse der indholder abstrakte metode, selv erklæres abstrakt - dette gælder også subklasser der undlader at implementere alle abstrakte metoder. Ligeledes behøver der ikke være abstrakte metoder i en abstrakt klasse, man kan ganske enkelt gøre den abstrakt for at forhindre instantiering.
Virtuelle metoder Da ikke alle metoder i C# er virtuelle, er det værd at bemærke, at alle abstrakte metoder automatisk er virtuelle - andet ville heller ikke give mening.

 

3.3 Interfaces

Det er konventionen i C# at navngive alle interfaces, så de begynder med I. Det er grundlæggende en dårlig idé at placere den slags information i et navn, da en senere ændring af interfacet til f.eks. en abstrakt klasse vil kræve en navneændring. Det er dog ikke det store problem, og hvis man aldrig udtaler det, men betragter det som et slags symbol, er det næsten til at holde ud :-)
Når en klasse implementerer et interface angives det på samme måde som ved nedarvning fra en klasse. Som bekendt kan man kun nedarve fra én klasse, mens man kan nedarve fra et vilkårligt antal interfaces. Man bruger, som i C++, komma til at adskille deres navne.
Interfaces kan ligeledes nedarve fra et eller flere interfaces.
Ingen konstanter Metoder i et interface er, i tråd med abstrakte klasser, alle abstract virtual. I modsætning til Java, er det ikke muligt at erklære konstanter i interfaces.
Metoderne har ikke nogen access modifiers (eg. public, private, ...). Dette skyldes at det er subklasserne der bestemmer hvad de skal være. Det er dog ikke tilladt at gøre dem utilgængelige. Det lyder umiddelbart som om man kun kan gøre dem public, men så enkelt er det ikke - se senere!
Lad os først se et eksempel, der bla. viser at nedarvning af interfaces ikke fungerer helt som i Java:
using System;

interface I {
  void Print();
} 

public class A: I {
  public void Print() {
    Console.WriteLine( "A.Print()" );
  } 
} 

public class B: A {
  public new void Print() {
    Console.WriteLine( "B.Print()" );
  } 
}

public class Test {
  public static void Main() {
    B b = new B();
    b.Print();
    I i = (I) b;
    i.Print();
  } 
}
B.Print()
A.Print()
I C# implementerer B ikke I, da den ikke nævner den i sin nedarvningsliste (i Java er det som bekendt tilstrækkeligt, at den blot nævnes i en af baseklasserne)

I C# kan man implementere to forskellige interfaces med fælles signature og samtidig holde dem adskilte. F.eks.:

using System;

interface IA {
  void Print();
} 

interface IB {
  void Print();
} 

class C: IA, IB {
  void IA.Print() {
    Console.WriteLine( "IA.Print()" );
  }
  
  void IB.Print() {
    Console.WriteLine( "IB.Print()" );
  }
}

class Test {
  public static void Main() {
    C c = new C();
    
    IA ia = (IA) c;
    ia.Print(); 
    
    IB ib = (IB) c;
    ib.Print(); 
  } 
}
IA.Print()
IB.Print()
Eksplicit implementation

At angive interfacets navn foran metodens navn i implementationen (sådan som det er gjort her) kaldes eksplicit implementation. En sådan metode vil optræde som private, i forbindelse med en C-reference. Det er kun ved at caste til et interface der passer til metoden, at man kan kalde den. Via f.eks. en IA-reference bliver den ene Print-metode public. Det er af samme grund ikke tilladt at placere en access modifier foran metodens signatur, når der anvendes eksplicit implementation.

Man kan rolig konkludere, at designerne af C# ikke har valgt de enkle løsninger!