Generics  

I version 2 af .NET frameworket er der udsigt til mange spændende nyheder. Både nye features i de enkelte sprog og nye features i det fælles framework. En af de væsentligste nyskabelser er mulighederne for at benytte såkaldte generics på mange niveauer i koden. Generics er en del af .NET frameworket, og det er således muligt at benytte generics i både C# og VB.NET. Når man arbejder med generics benytter man på designtidspunktet type-placeholdere; placeholdere som først på run-time erstattes af en konkret type. Det giver både fleksibilitet og typestærk opførsel - såvel på designtidspunktet som run-time.

En simpel maksimumsfunktion

For at beskrive hvordan generics virker, vil vi tage udgangspunkt i følgende meget simple maksimums-funktion:

Public Function MaxIntegers(ByVal i1 As Integer, ByVal i2 As Integer) As Integer

  If i1 > i2 Then
    Return i1
  Else
    Return i2
  End If

End Function
public int MaxIntegers(int i1, int i2)
{
  if (i1 > i2)
  {
    return i1;
  }
  else
  {
    return i2;
  }
}

Denne funktion er naturligvis typestærk med de fordele, det giver i form af intellisense-support og type-check på kompileringstidspunktet - til gengæld er funktionen også begrænset til kun at kunne arbejde med integers. Hvis vi ønsker også at kunne finde maksimum af for eksempel strenge, kan vi naturligvis lave en ny overloadet version af Max-funktionen specifikt beregnet til strenge. Men skal vi kunne tage maksimum over ret mange andre datatyper, kommer vi hurtigt til at bruge meget tid på at lave nye type-specifikke overloads, og vores software ender op med at indeholde en lang række næsten ens funktioner, med de ulemper det har i forhold til fremtidig vedligeholdelse. Alternativt kan vi lave en fælles, typeløs version, hvor argumenterne til Max-funktionen er defineret som værende af typen System.Object. Selvom denne funktion i princippet kan håndtere alle eksisterende datatyper er den ikke særlig nyttig i praksis eftersom den ikke kan kompileres, da man ikke kan sammenligne instanser af Object-datatypen med > (større end) operatoren.

System.IComparable

Så vi har brug for en anden knap så generel supertype for alle de datatyper, som vi ønsker at kunne sammenligne. I .NET findes der allerede en datatype, System.IComparable, hvis formål det netop er at definere interfacet for sammenlignelige instanser. Vi kan ændre Max-funktionen til en typestærk version baseret på System.IComparable. System.IComparable er et særdeles simpelt interface bestående af kun en enkelt metode:

Function CompareTo(ByVal obj As Object) As Integer
int CompareTo(object obj)

CompareTo-metoden returnerer en negativ værdi, hvis instansen er mindre end obj, 0 hvis instansen og obj er ens, og en positiv værdi hvis instansen er større end obj. Max-Funktionen kan hermed implementeres med følgende kode:

Public Function MaxIComparable(ByVal c1 As System.IComparable, ByVal c2 As System.IComparable) As Object

  If c1.CompareTo(c2) > 0 Then     'Kan give en run-time fejl
    Return c1
  Else
    Return c2
  End If

End Function
public object MaxIComparable(System.IComparable c1, System.IComparable c2)
{
  if (c1.CompareTo(c2) > 0)        //Kan give en run-time fejl
  {
    return c1;
  }
  else
  {
    return c2;
  }
}

Et problem med denne implementation er dog, at kompileren vil acceptere følgende kode, som dog naturligvis vil give en run-time fejl, idet strenge og integers normalt ikke kan sammenlignes på en fornuftig måde – heller ikke ved hjælp af System.IComparable-interfacet:

Dim s As String = "Hello"
Dim t As Integer = 42

MaxIComparable(s, t)
string s = "Hello";
int t = 42;

MaxIComparable(s, t);

Generiske metoder

Med generics kan man opnå det bedste af begge verdener: En generel men samtidig typestærk implementation af Max-funktionen. En version baseret på generics kunne se ud som følger:

Public Function Max(Of T As System.IComparable)(ByVal i1 As T, ByVal i2 As T) As T

  If i1.CompareTo(i2) > 0 Then
    Return i1
  Else
    Return i2
  End If

End Function
public T Max(T i1, T i2) where T : System.IComparable
{
  if (i1.CompareTo(i2) > 0)
  {
    return i1;
  }
  else
  {
    return i2;
  }
}

T er her en placeholder for en konkret type - en placeholder der dels sørger for at i1 og i2 er af samme type dels sikrer, at den konkrete type implementerer System.IComparable. Den konkrete type, som T skal erstattes med, defineres ved brugen af funktionen. Så ønskes en konkret version af Max-funktionen, som kan finde maksimum af to integers, vil kaldet se ud som følger:

Dim i1 As Integer = 17
Dim i2 As Integer = 42

Max(Of Integer)(i1, i2)
int i1 = 17;
int i2 = 42;

Max<int>(i1, i2);

Efter at T er fastlagt til, i dette konkrete tilfælde, at være en placeholder for integer-typen, vil brugen af Max-funktionen opføre sig fuldstændig som MaxIntegers-funktionen - der vil altså være fuld intellisense og typestærke kompileringscheck. Forsøges Max-funktionen kaldt med en streng og en integer som parametre som i følgende eksempel, vil man på kaldsstedet få en kompileringsfejl, idet definitionen af Max foreskriver, at begge de to parametre (såvel som iøvrigt retur-værdien) skal være af samme type - i dette tilfælde integers.

Dim s As String = "Hello"
Dim t As Integer = 42

Max(Of Integer)(s, t) 'Giver kompileringsfejl
string s = "Hello";
int t = 42;

Max<int>(s, t) //Giver kompileringsfejl

Constraints

I definitionen af den generiske Max-funktion er de konkrete typer, som T kan erstattes med pålagt en begrænsning: en såkaldt constraint. I dette tilfælde er begrænsningen at T skal implementere System.IComparable. Uden denne begrænsning ville T kunne erstattes af typer som ikke ville kunne sammenlignes med hverken almindelige sammenligningsoperatorer (< og >) eller med CompareTo. Så begrænsningen har altså til formål på den ene side at sikre at konkrete metoder (baseret på konkrete typer) altid vil være typemæssigt korrekte og på den anden at sørge for, at vi rent faktisk kan implementere den nødvendige funktionalitet.

Constraints kan antage flere forskellige former. Fælles for dem alle er, at de definerer betingelser for en konkret type, der kan erstatte en generisk typeplaceholder.

  • Man kan angive navnet på en given klasse. Den konkrete type skal i så fald være en subtype af den angivne klasse.
  • Man kan angive keywordet ”Class” (VB) / ”class” (C#). Dette keyword angiver, at den konkrete type skal være en referencetype.
  • Man kan angive keywordet ”Structure” (VB) / ”struct” (C#). Dette keyword angiver, at den konkrete type skal være en valuetype.
  • Man kan angive navnet på et interface. Den konkrete type skal i så fald implementere det givne interface
  • Man kan angive keywordet ”New” (VB) / ”new( )” (C#). Dette keyword angiver, at den konkrete type skal have en public default konstruktør – altså en public konstruktør uden parametre. Dette har specielt betydning ved implementation af objekt-factories

Uanset hvilke constraints de konkrete typer pålægges, så angives de i VB som en As-konstruktion og i C# som en where-konstruktion efter følgende mønster:

Sub MetodeNavn(Of U As constraint1, T As {constraint2, constraint3})
    (ByVal a As T, ByVal b As U)
End Sub
void MetodeNavn<T, U>(T a, U b)
    where T : constraint1
    where U : constraint2, constraint3
{ }

Generiske interfaces

En markant fordel ved generics’ typestærke natur er, at man undgår de implicitte og eksplicitte typecasts, som ellers ofte optræder, når man implementerer generel funktionalitet. Ikke mindst med hensyn til performance er dette en væsentlig fordel. Typecasts imellem reference typer har ganske vist ikke specielt alvorlige performanceimplikationer, men når man typecaster imellem value- og referencetyper involverer typecastene boxing henholdsvis unboxing, hvilket har tilstrækkelig stor betydning for performance til, at man bør være opmærksom derpå, også når man gør brug af generics. Hvis man eksempelvis kalder den generiske Max-funktion (med System.IComparable-constrainten) med integer som den konkrete type, vil der rent faktisk først ske et typecast fra integer til object og derefter internt i implementeringen af integer-datatypens CompareTo-metode fra object til integer. I stedet for at pålægge Max-funktionens parametre det sædvanlige System.IComparable-interface som constraint kan man istedet vælge den generiske version System.IComparable(Of T) / System.IComparable.

Function CompareTo(ByVal other As T) As Integer
int CompareTo (T other)

Når der ud fra det generiske IComparable-interface laves en konkret version baseret på eksempelvis integer-datatypen kommer det konkrete interface til at se ud som følger:

Function CompareTo(ByVal other As Integer) As Integer
int CompareTo (int other)

Ved brug af dette konkrete integer-baserede IComparable-interface opnås en 100% typestærk udgave af Max-funktionen og enhver form for boxing og unboxing undgåes. Den endelige version af Max-funktionen får hermed følgende udformning:

Public Function Max(Of T As System.IComparable(Of T))(ByVal i1 As T, ByVal i2 As T) As T

  If i1.CompareTo(i2) > 0 Then
    Return i1
  Else
    Return i2
  End If

End Function
public T Max(T i1, T i2) where T : System.IComparable
{
  if (i1.CompareTo(i2) > 0)
  {
    return i1;
  }
  else
  {
    return i2;
  }
}

Generiske collections

Generics har også en vigtig anvendelse i forbindelse med collection-datatyper. En traditionel og ofte benyttet collection-datatype er System.Collections.ArrayList, som der her ses et simpelt eksempel på brugen af:

Dim typeLessList As New System.Collections.ArrayList

typeLessList.Add(17)
typeLessList.Add(42)
typeLessList.Add("Whidbey")

Dim i As Integer = CInt(typeLessList(0))
System.Collections.ArrayList typeLessList = new System.Collections.ArrayList();

typeLessList.Add(17);
typeLessList.Add(42);
typeLessList.Add("Whidbey");

int i = (int)(typeLessList[0]);

Bemærk at System.Collections.ArrayList er en typeløs collection, der accepterer elementer af alle datatyper. Man vil altså som i ovenstående eksempel kunne tilføje såvel integers som strenge til collectionen. Endvidere vil man ofte skulle typecaste objekterne til en mere konkret datatype ved udtagning af collectionens elementer - medmindre man da har brug for elementerne som rene objekter.

Generiske typer anvendes som sagt ikke kun i forbindelse med generiske metoder, men også ofte i forbindelse med collections, hvor man ønsker at gennemtvinge typestærk opførsel på samme måde som for Max-funktionen, men nu ikke begrænset til en enkelt funktion men for alle collectionens metoder, konstruktører, instansvariable, properties og så videre.

Version 2 af .NET frameworket definerer i System.Collections.Generic-namespacet en række generiske collections, som er klar til brug. Man kan naturligvis definere sine egne generiske collections, men oftest vil de allerede definerede være tilstrækkelige. Et eksempel på en sådan prædefineret generisk collection er System.Collections.Generic.List. Eksemplet med ovenstående arrayliste vil med brug af den generiske List-collection tage sig ud som følger:

Dim genericList As New System.Collections.Generic.List(Of Integer)

genericList.Add(17)
genericList.Add(42)

Dim j As Integer = genericList(0)
System.Collections.Generic.List<int> genericList = new System.Collections.Generic.List<int>();

genericList.Add(17);
genericList.Add(42);

int j = genericList[0];

Der er som tidligere bemærket fuld intellisense-understøttelse i brugen af genericList-collectionen, og den opfører sig lige så typestærkt som en gammeldags typestærk collection: Hvis man søger at tilføje en streng til collectionen fås en kompileringsfejl, og man behøver ikke typecaste elementerne til integers, idet anvendelsen af generiske typer sikrer, at det er integers, der returneres.

Konklusion

Generics er nyttige, hvis man ønsker at lave generel anvendelig kode uden at gå på kompromis med intellisense-understøttelse og typestærke kompileringschecks. I nærværende artikel har vi set på de generelle principper der finder anvendelse i forbindelse med generics – og specifikt har vi set på generiske metoder samt givet en kort introduktion til henholdsvis generiske interfaces og generiske collections. Andre former for generics indbefatter generiske strukturer og generiske delegates. Generics illustreres oftest i form af generiske collections, men generics har mange forskellige anvendelser – anvendelser der alle er med til at hæve abstraktionsniveauet for softwareudviklingen – så det er kun et spørgsmål om at være kreativ...

Til slut skal det understreges, at selv om generics er en smart og fleksibel måde at implementere typestærkt genbrug af algoritmer på, så vil man også, selv om man har adgang til generics, stadig have brug for at implementere typestærke collections på den traditionelle måde. Jeg har tidligere skrevet en artikel der dels gennemgår de generelle principper bag typestærke collections dels beskriver konkrete implementationsteknikker. Artiklen "Typestærke collections" kan læses her.

Download af eksempler

Den første demo indeholder forskellige implementation af bubble sort med og uden brug af generics. Demoen anskueliggør de forskelle i performance, der er ved implementation med og uden brug af generics.

Den anden demo viser et eksempel på, hvordan en generisk collection kan implementeres - i dette eksempel en generisk prioritetskø.