Interface
I C# er interfaces en kraftfuld og fleksibel funktion, der kan hjælpe med at designe mere modulære, vedligeholdelige og testbare kode.
Information til undervisere
Brug anstrakte klasser som indgang til at snakke om interface, og værd sikker på at kursisterne ved hvad “løs kobling” betyder.
Denne lektion kan med fordel suppleres med en gennemgang af Afkobling.
Her er nogle af fordelene ved at bruge interfaces i C#:
-
Adskillelse af ansvar (Separation of Concerns): Interfaces definerer en kontrakt eller et sæt metoder, som en klasse skal implementere. Dette hjælper med at adskille ansvar mellem forskellige dele af koden og gør det lettere at ændre og vedligeholde kodebasen.
-
Kodegenbrug: Interfaces tillader flere klasser at implementere samme kontrakt, hvilket fører til genbrug af kode og reducerer duplikering. Dette hjælper også med at forbedre konsistensen og gør det lettere at vedligeholde kode over tid.
-
Programmering til interface: Ved at programmere til et interface i stedet for en konkret klasse, bliver din kode mere fleksibel og åben for ændringer. Hvis du senere beslutter at ændre implementeringen, kan du gøre det uden at skulle ændre koden, der bruger interfacet.
-
Testbarhed: Interfaces gør det lettere at teste din kode ved at muliggøre “mocking” af afhængigheder i testscenarier. Dette gør det muligt at isolere den kode, du vil teste, og kontrollere dens opførsel uafhængigt af de faktiske afhængigheder.
-
Styrkelse af kodekontrakter: Interfaces hjælper med at definere klare kontrakter mellem forskellige dele af en applikation. Dette gør det lettere at forstå og arbejde med kodebasen, da det tydeligt angiver, hvilke metoder og egenskaber en klasse skal implementere.
-
Multiple interfaces: I modsætning til arv, hvor en klasse kun kan arve fra én baseklasse, kan en klasse implementere flere interfaces. Dette gør det muligt for en klasse at have flere roller og adfærd uden at skulle bekymre sig om begrænsningerne ved enkel arv.
-
Større fleksibilitet: Interfaces tillader dig at skabe fleksible og udskiftelige komponenter i din applikation. Hvis du har brug for at ændre eller udvide funktionaliteten, kan du nemt implementere et nyt interface eller udvide et eksisterende interface uden at skulle ændre alle de klasser, der bruger det.
abstract / interface
Du har tidligere i bogen lært om aktrakte klasser og aktrakte medlemmer, og ideen med dem er, at runtime er helt sikker på, at der findes et medlem i en klasse med en helt bestemt signatur (navn, returværdi og argumenter). Det kunne eksempelvis se således ud:
public abstract class Terning
{
public int Værdi { get; protected set; }
public abstract void Ryst();
}
Bemærk Ryst-metoden som er abstrakt og dermed uden nogen form for implementation. Den skal tilføjes i nedarvede klasser, og man kan heller ikke skabe en instans af klassen Terning. Den skal nedarves, og underklasser skal implementere Ryst:
public class YatzyTerning : Terning
{
public override void Ryst()
{
this.Værdi = new Random().Next(1, 7);
}
}
Dermed kan klassen YatzyTerning indgå i polymorfi jævnfør forrige kapitel, fordi kompileren ved, at der findes en Ryst:
Du kan skabe samme typesikkerhed ved hjælp af en anden overordnet type kaldet interface. I denne type definerer du, hvilke medlemmer som skal være tilgængelige i de klasser, som implementerer det konkrete interface.
Definition af et interface
Et interface placeres på namespace-niveau ligesom andre overordnede typer (klasser, strukturer med videre) og ser således ud:
Typisk vil du se, at et interface er navngivet med et stort I samt et navn – det er dog helt op til dig.
I et interface kan du definere, hvilke medlemmer der skal være tilgængelige, og du skal angive den fulde signatur, men uden definering af synlighed (skal under alle omstændigheder være offentlige) og uden nogen form for implementering. Eksempelvis som følger:
Du kan tilføje så mange medlemmer (egenskaber, metoder eller hændelser), du ønsker. I eksemplet er der angivet en egenskab og en metode, og denne definition ligger til grund for klasser, som imple-menterer det konkrete interface.
Implementering af et interface
Et interface skal implementeres i en klasse eller struktur og det sker med samme syntaks som nedarvning:
så vil kompileren sørge for, at medlemmer defineret i det konkrete interface implementeres i klassen eller strukturen:
public class YatzyTerning : ITerning
{
public int Værdi { get; set; }
public void Ryst()
{
this.Værdi = new Random().Next(1, 7);
}
public void Skriv()
{
Console.WriteLine($"[{this.Værdi}]");
}
}
Du kan tilføje alle mulige andre medlemmer til en klasse, der imple-menterer et interface. I eksemplet har metoden Skriv ikke noget med ITerning at gøre, men pointen er, at kompileren ved, at der findes medlemmer, som er defineret i et interface.
Da et interface er en referencetype, giver det dermed følgende muligheder:
YatzyTerning a = new YatzyTerning();
a.Ryst();
a.Skriv();
ITerning b = new YatzyTerning();
b.Ryst();
Console.WriteLine(b.Værdi);
Som det fremgår, er YatzyTerning en helt normal klasse, men da den også er en ITerning, må en variabel af denne type gerne indeholde en reference til en YatzyTerning – og så er vi tilbage i polymorfi.
Så det er altså muligt at opbygge en samling af klasser, som implementerer et interface (eller flere), og referere til objekter af disse typer gennem en variabel af interface typen.
IComparable<T>
Som et lidt mere komplekst eksempel på brug af interface kan du se på følgende kode:
using System;
using System.Collections.Generic;
namespace Demo
{
internal class Program
{
private static void Main(string[] args)
{
List<Terning> terninger = new List<Terning>();
for (int i = 0; i < 5; i++)
terninger.Add(new Terning());
terninger.Sort(); // FEJLER!!
foreach (var terning in terninger)
Console.WriteLine(terning.Værdi);
}
}
public class Terning {
public int Værdi { get; private set; }
public void Ryst() {
this.Værdi = new Random().Next(1, 7);
}
public Terning()
{
this.Ryst();
}
}
}
I koden bliver der oprettet en liste af fem terninger baseret på en almindelig klasse Terning, og herefter forsøges listen sorteret. Hvis du prøver koden, vil du opleve, at Sort-metoden fejler, hvilket jo i virkeligheden er logisk nok – hvor skulle den vide fra, hvordan en terning skal sorteres? Men vi kan faktisk godt fortælle Sort-metoden, hvordan den skal sortere terninger ved at lade klassen implementere et indbygget interface kaldet IComparable
using System;
using System.Collections.Generic;
namespace Demo
{
internal class Program
{
private static void Main(string[] args)
{
List<Terning> terninger = new List<Terning>();
for (int i = 0; i < 5; i++)
terninger.Add(new Terning());
terninger.Sort();
foreach (var terning in terninger)
Console.WriteLine(terning.Værdi);
}
}
public class Terning : IComparable<Terning> {
public int Værdi { get; private set; }
public void Ryst() {
this.Værdi = new Random().Next(1, 7);
}
public int CompareTo([AllowNull] Terning other)
{
if (this.Værdi > other.Værdi)
return 1;
if (this.Værdi < other.Værdi)
return -1;
return 0;
}
public Terning()
{
this.Ryst();
}
}
}
Når du afvikler koden, vil du opleve, at terningerne bliver sorteret helt korrekt, og det skyldes at CompareTo-metoden returnerer et tal, der kan bruges til at sammenligne og dermed sortere terningerne. Metoden kan skrives på mange måder, men du skal blot returnere et heltal, som kan benyttes til sammenligning.
IEnumerable<T>
IEnumerable
Mange indbyggede samlingstyper i .NET, såsom List, Array og Dictionary, implementerer IEnumerable. Det betyder, at disse samlinger kan gennemgås ved hjælp af foreach-løkker.
Se det som en overordnet defination af en samling:
// Opret forskellige samlinger og arrays
List<int> listOfNumbers = new List<int> { 1, 2, 3, 4, 5 };
int[] arrayOfNumbers = new int[] { 6, 7, 8, 9, 10 };
HashSet<int> hashSetOfNumbers = new HashSet<int> { 11, 12, 13, 14, 15 };
// Anvend IEnumerable til at holde forskellige samlingstyper
IEnumerable<int> enumerableNumbers;
// Tildel IEnumerable variablen til forskellige samlinger og arrays
enumerableNumbers = listOfNumbers;
foreach (int number in enumerableNumbers)
{
Console.WriteLine(number);
}
enumerableNumbers = arrayOfNumbers;
foreach (int number in enumerableNumbers)
{
Console.WriteLine(number);
}
enumerableNumbers = hashSetOfNumbers;
foreach (int number in enumerableNumbers)
{
Console.WriteLine(number);
}
Brug af IEnumerable
For at bruge IEnumerable, skal en klasse eller et interface implementere det. Herefter kan der anvendes foreach-løkker til at gennemgå samlingen.
IEnumerable<int> numbers = new List<int> { 1, 2, 3, 4, 5 };
foreach (int number in numbers)
{
Console.WriteLine(number);
}
Implementering af IEnumerable
For at gøre en klasse itererbar med IEnumerable
, skal du først inkludere System.Collections
-namespacet. Derefter skal din klasse implementere IEnumerable
-interfacet. Dette kræver, at du implementerer GetEnumerator()
-metoden, som skal returnere en IEnumerator
. Denne IEnumerator
er ansvarlig for at holde styr på den nuværende position i samlingen og at navigere gennem elementerne.
Her er et eksempel på, hvordan man implementerer IEnumerable
:
using System.Collections;
using System.Collections.Generic;
public class MinKlasse : IEnumerable<MinItem>
{
private List<MinItem> items = new List<MinItem>();
public IEnumerator<MinItem> GetEnumerator()
{
return items.GetEnumerator();
}
IEnumerator IEnumerable.GetEnumerator()
{
return this.GetEnumerator();
}
// Yderligere metoder og egenskaber
}
I dette eksempel returnerer GetEnumerator() metoden direkte listen items’s egen iterator, hvilket gør implementeringen mere ligetil og effektiv.
Ved at implementere IEnumerable
, kan du gøre dine egne klasser mere fleksible og nemmere at integrere med andre .NET-funktioner, der arbejder med samlinger.
Info
Se også hvor yield kan bruges i forbindelse med IEnumerable her.
LINQ og IEnumerable
Language Integrated Query (LINQ) er en samling af metoder, der gør det nemt at arbejde med samlinger og sekvenser i C#. De fleste LINQ-metoder returnerer IEnumerable og kan kædes sammen for at udføre komplekse operationer på samlinger.
For eksempel, for at finde alle lige tal i en liste og sortere dem, kan følgende LINQ-metoder anvendes:
IEnumerable<int> evenNumbers = numbers.Where(n => n % 2 == 0).OrderBy(n => n);
foreach (int evenNumber in evenNumbers)
{
Console.WriteLine(evenNumber);
}
Ved at bruge IEnumerable og LINQ kan koden gøres mere læsbar og vedligeholdelsesvenlig, hvilket er vigtigt for at skrive effektiv og robust C#-kode.
Fordelene ved at bruge IEnumerable
Når en metode returnerer et objekt af typen IEnumerable, giver det en række fordele i forhold til at returnere en mere specifik samlingstype. Her er nogle af de vigtigste grunde til at vælge IEnumerable i stedet for en konkret type:
-
Abstraktion: Ved at returnere en IEnumerable tillader man metoden at arbejde med en bred vifte af samlingstyper. Dette betyder, at metoden er mere fleksibel og kan bruges i flere forskellige scenarier, uden at skulle ændre dens interne logik.
-
Uafhængighed: Når en metode returnerer en IEnumerable, er den uafhængig af den underliggende samlingstype. Dette gør det muligt at ændre implementeringen af metoden uden at påvirke den kode, der bruger metoden. Hvis metoden senere ændres til at bruge en anden samlingstype, vil det ikke kræve ændringer i den kode, der anvender metoden.
-
Performance: Nogle gange kan brugen af IEnumerable føre til bedre performance, især når man arbejder med store datasæt. Dette skyldes, at IEnumerable understøtter “lazy evaluation” (forsinket evaluering), hvilket betyder, at data kun bliver hentet og behandlet efter behov. Dette kan reducere hukommelsesforbruget og forbedre responstiden for applikationen.
-
Kompatibilitet med LINQ: IEnumerable er grundlaget for LINQ (Language Integrated Query), et kraftfuldt sæt af funktioner og metoder, der gør det nemt at arbejde med data og samlinger i C#. Ved at returnere en IEnumerable fra en metode, gør man det nemt at anvende LINQ-operationer på resultatet og opnå kraftfulde og fleksible datahåndteringsmuligheder.
-
Læsbarhed: Når en metode returnerer en IEnumerable, signalerer det tydeligt, at den returnerede samling primært er beregnet til at blive itereret over ved hjælp af en foreach-løkke eller lignende konstruktioner. Dette kan gøre koden mere læsbar og forståelig for andre udviklere.
Opgaver
IDisposable
IDisposable er en del af System namespace og bruges til at frigive ressourcer, såsom filhåndtag, netværksforbindelser og uallokeret hukommelse, når de ikke længere er nødvendige.
Hvornår skal IDisposable bruges?
IDisposable bør implementeres, når en klasse arbejder med ressourcer, der kræver eksplicit frigivelse, såsom:
- Uallokeret hukommelse (f.eks. ved brug af Marshal.AllocHGlobal)
- Filhåndtag
- Netværksforbindelser
- Databaseforbindelser
Implementering af IDisposable
For at implementere IDisposable-interface i en klasse, skal klassen definere en Dispose-metode, der frigiver de ressourcer, den bruger. Her er et grundlæggende eksempel på, hvordan man kan implementere IDisposable i en klasse:
class MyResource : IDisposable
{
private Stream _resource;
public MyResource(string path)
{
_resource = File.OpenRead(path);
}
public void Dispose()
{
_resource.Dispose();
}
}
using
En almindelig måde at bruge IDisposable-objekter på er med using-sætningen. Dette sikrer, at Dispose-metoden kaldes automatisk, når objektet går ud af using-sætningens rækkevidde.
Her er et eksempel på brug af using i I/O:
string filePath = "example.txt";
using (StreamReader reader = new StreamReader(filePath))
{
string line;
while ((line = reader.ReadLine()) != null)
{
Console.WriteLine(line);
}
}
Og her er et eksempel på, hvordan man kan bruge using-sætningen med en IDisposable-klasse:
I dette eksempel vil Dispose-metoden blive kaldt automatisk, når using-sætningen er afsluttet, og ressourcerne frigives.
Opgaver
Andre vigtige interface
Der findes mange andre interface’s i C# - her er et par eksempler:
-
IEquatable
: Dette interface bruges til at definere en metode, Equals(T), der bestemmer, om to objekter er lige. Implementering af dette interface kan forbedre ydeevnen, når det bruges sammen med samlinger som Dictionary og HashSet . -
INotifyPropertyChanged: Dette interface bruges ofte i forbindelse med databinding i applikationer med brugergrænseflader. Det giver en mekanisme til at underrette klienter, typisk brugergrænseflade-elementer, når en egenskabsværdi ændres.
-
IQueryable
: Dette interface udvider IEnumerable og er en central del af LINQ til datakilder, der understøtter udtryksbaserede forespørgsler. Det bruges typisk, når der arbejdes med databaser eller andre datakilder, hvor det er vigtigt at udføre forespørgsler på datakilden selv i stedet for at hente alle dataene ind i hukommelsen og derefter udføre forespørgslen. IQueryable er en vigtig del af LINQ-to-Entities og gør det muligt at bygge og udføre forespørgsler, der oversættes til SQL og udføres på databasen, så kun de nødvendige data returneres til programmet.
Der er mange andre interfaces i C#, men de nævnte er nogle af de mest almindelige og grundlæggende i C#-programmering.
Afkobling
Du vil tit finde interface benyttet i forskellige former for software mønstre – herunder afkobling gennem et mønster kaldet dependency injection (forkortet DI). Det vil du falde over mange steder, og det er i øvrigt kun en af mange forskellige måder at skabe kode, som ikke er så hårdt bundet til konkrete klasser. Læs om Afkobling her.