Typestærke collections  
Downloads til denne artikel finder du her !
StronglyTypedCollections_vb.zip
VB.NET demokode, der svarer til eksemplerne i denne artikel
StronglyTypedCollections_cs.zip
C# demokode, der svarer til eksemplerne i denne artikel
StronglyTypedCollections_IList_vb.zip
VB.NET demokode med samme funktionalitet, blot implementeres IList interfacet, som beskrevet i afsnittet: "Alternative implementationer".
StronglyTypedCollections_IList_cs.zip
C# demokode med samme funktionalitet, blot implementeres IList interfacet, som beskrevet i afsnittet: "Alternative implementationer".

Når man taler om en "collection", som begreb betragtet, forstår man løst defineret en datastruktur, hvis programmeringsmæssige interface består af metoder og properties, der gør, at man kan tilføje, fjerne, tilgå og tælle objekterne i datastrukturen.

I .NET frameworkets klassebiblioteker er der defineret mange forskellige slags collections. De mest generelt anvendelige er defineret i System.Collections namespacet. Her finder man nyttige collection-typer så som ArrayList, BitArray, HashTable, Queue, SortedList og Stack. Disse collection-typer kan, med BitArray som en oplagt undtagelse, tage imod vilkårlige objekter og er derfor af stor nytte i mange meget forskellige situationer. I nogle situationer er man dog ikke interesseret i, at en collection kan tage i mod vilkårlige objekter - nogle gange ønsker man at lægge restriktioner på, hvilke objekter man kan tilføje til en given collection. Det kan specielt være praktisk, at man kan begrænse, hvilke typer af objekter, der kan ligges ind i en collection, hvis det man efterfølgende har tænkt sig at gøre ved disse objekter forudsætter, at de er af en bestemt type.

Collections, der kun kan tage imod objekter af en bestemt type (en type som ikke blot er object/System.Object), kaldes for typestærke collections. Typestærke collections findes mange forskellige steder i .NET frameworket. I System.Data er System.Data.DataTableCollection eksempelvis en ofte anvendt collection, der kun kan indeholde objekter af typen System.Data.DataTable. På samme måde kan System.Data.DataColumnCollection kun indeholde objekter af typen System.Data.DataColumn og så fremdeles.

Hvor vidt man overhovedet ønsker at lave typestærke collections, eller om man blot vil holde sig til de generelle collection-typer i System.Collections afhænger af mangt og meget, og det kan sagtens være emnet for en hel artikel i sig selv. Men lad os i denne sammenhæng blot antage, at vi ønsker at opnå de fordele en typestærk collection giver os i form af større sikkerhed (vi kan ikke ved et uheld komme til at tilføje objekter af en "forkert" type til vores collection) samt, at vi ønsker at få hjælp af Visual Studio .NETs IntelliSense til at skrive vores kode. Bemærk dog at typestærke collections ikke giver os flere tekniske muligheder end de typeløse collections.

Man kan naturligvis forestille sig mange forskellige typestærke collections. I undertegnedes komponent til brugeradminstration er der eksempelvis en UserCollection, men afhængig af de konkrete behov kunne man også lave en AnsatCollection, en FirmaCollection, en ProduktCollection o.s.v. I det følgende vil vi for eksemplets skyld lave en simpel FormCollection - en collection der kan indeholde objekter af typen System.Windows.Forms.Form.

Valg af members

Definitionen af en collection ovenfor var noget løs i det. Inden vi kan komme igang med at implementere en typestærk collection, skal vi allerførst have fastlagt dens programmeringsmæssige interface - hvilke metoder og properties vi ønsker, den skal have. Interfacet afhænger naturligvis meget af den konkrete collection, vi vil lave, men som oftest har man members så som Add, Contains, Clear, Count, Item og Remove i en collections interface.

For FormCollection har jeg valgt nedenstående members. Men man kan sagtens forestille sig mange andre.

  Public Sub Add(ByVal frm As System.Windows.Forms.Form)

  Public Function Contains(ByVal frm As System.Windows.Forms.Form) As Boolean

  Public Sub Clear()
    
  Public ReadOnly Property Count()As Integer

  Public Sub Insert(ByVal index As Integer, ByVal frm As System.Windows.Forms.Form)
    
  Default Public Property Item(ByVal index As Integer) As System.Windows.Forms.Form
    
  Public Sub Remove(ByVal frm As System.Windows.Forms.Form)

  Public Sub RemoveAt(ByVal index As Integer)

Ofte vil en metode have flere overloadede versioner. F.eks. vil man ofte have en eller flere Add-funktioner, der tager imod en række af parametre, svarende til parametrene på konstruktøren på de objekter, man kan tilføje til den konkrete collection: Add-funktionen instansierer ved hjælp af parametrene et nyt objekt, tilføjer det til collectionen og returnerer det nye objekt som resultatet af Add-funktionen.

Når man implementerer en typestærk collection vil man for simpelhedens skyld ofte vælge, at gøre det på basis af en allerede eksisterende typeløs collection eller et almindeligt array. I det nærværende tilfælde bliver Form-objekter, der tilføjes FormCollection-klassen lagret i en privat variabel af typen System.Collections.ArrayList, men man kunne sagtens have valgt at basere den på en anden type "lager"-variabel.

Den type man vælger, som sin interne lager-variabel har en tendens til også at slå igennem i hvilke members, der er tilgængelige i den typestærke klasses interface. I FormCollection-klassens tilfælde gælder dette eksempelvis for RemoveAt-metoden, som ikke overraskende forudsætter, at man via et index kan udpege det objekt, man ønsker at fjerne fra collectionen. RemoveAt-metoden er derfor specielt oplagt at implementere, hvis man har en underliggende lager-variabel, der kan udpege dets objekter via et index. En anden måde at udtrykke det på er, at man naturligvis skal vælge sin underliggende lager-variabel ud fra de behov, man har i sin typestærke collection. Hvis vi havde behov for at kunne tilgå elementerne i collectionen via en key-værdi, var det oplagt, at vi baserede os på en dictionary type som f.eks. en System.Collections.HashTable.

Implementationen

Selve implementationen af ovenstående members i FormCollection klassen er ganske simpel - der er for de fleste members vedkommende blot tale om simple wrappers omkring ArrayList lager-variablen.

  Public Class FormCollection
    Implements System.Collections.IEnumerable

    Private _collection As New System.Collections.ArrayList()

    Public Sub Add(ByVal frm As System.Windows.Forms.Form)
      _collection.Add(frm)
    End Sub

    Public Function Contains(ByVal frm As System.Windows.Forms.Form) As Boolean
      Return _collection.Contains(frm)
    End Function
      
    Public Sub Clear()
      _collection.Clear()
    End Sub
   
    Public ReadOnly Property Count() As Integer
      Get
        Return _collection.Count
      End Get
    End Property

    Public Sub Insert(ByVal index As Integer, ByVal frm As System.Windows.Forms.Form)
      _collection.Insert(index, frm)
    End Sub
    
    Default Public Property Item(ByVal index As Integer) As System.Windows.Forms.Form
      Get
        Return CType(_collection.Item(index), System.Windows.Forms.Form)
      End Get
      Set(ByVal Value As System.Windows.Forms.Form)
         _collection.Item(index) = Value
      End Set
    End Property
 
    Public Sub Remove(ByVal frm As System.Windows.Forms.Form)
      _collection.Remove(frm)
    End Sub

    Public Sub RemoveAt(ByVal index As Integer)
      _collection.RemoveAt(index)
    End Sub
  End Class

Item propertyen skal dog nok have et par enkelte kommentarer med på vejen. I Visual Basic .NET kan man angive en property som værende default, hvis den vel at mærke tager imod en eller flere parametre. Det betyder, at MineForme.Item(n) er ækvivalent med MineForme(n), hvor MineForme er en variabel af typen FormCollection. I C# kan man ikke lave default properties; til gengæld kan man lave såkaldte indexers. En indexer giver stort set samme resultat, som man i VB får ved at lave en default property: Man kan tilgå elementerne i collectionen på samme måde, som hvis de lå i et almindeligt array: MineForme[n]

C# indexeren svarende til ovenstående default Item-property kunne være implementeret, som følger:

  public System.Windows.Forms.Form this[int index]
  {
    get
    {
      return (System.Windows.Forms.Form)_collection[index];
    }
    set
    {
      _collection[index] =  value;
    }
  }

Enumererbare collections

Når man taler om collections, antager man som regel, at man kan gennemløbe elementerne i collectionen v.h.a. en For Each-løkke.

Denne funktionalitet kræver, at System.Collections.IEnumerator interfacet er implementeret. IEnumerator interfacet består af tre members: Current, MoveNext og Reset. En passende implementation af disse tre members gør, at For Each-løkken i et givet sprog via IEnumerator interfacet kan gennemløbe alle elementerne i collectionen. Men i modsætning til, hvad man umiddelbart måske kunne forledes til at tro, er det ikke ens typestærke collection, der skal implementere dette interface; man laver i stedet en selvstændig klasse (i nærværende eksempel kaldet FormCollectionEnumerator), der implementerer IEnumerator interfacet.

  Private Class FormCollectionEnumerator
    Implements System.Collections.IEnumerator
    Private _index As Integer = -1
    Private _forms As  FormCollection
    
    Public Sub New(ByVal forms As FormCollection)
      _forms = forms
    End Sub
    
    Public ReadOnly Property Current() As Object Implements System.Collections.IEnumerator.Current
      Get
        Return _forms(_index)
      End Get
    End Property
    
    Public Function MoveNext() As Boolean _
      Implements System.Collections.IEnumerator.MoveNext
      _index += 1

      If _index < _forms.Count Then
        Return True
      Else
        Return False
      End If
    End Function
    
    Public Sub Reset() Implements System.Collections.IEnumerator.Reset
       _index = -1
    End Sub
  End Class

FormCollection klassen skal for, at man kan benytte For Each-konstruktionen, implementere et helt andet interface: System.Collections.IEnumerable. IEnumerable interfacet indeholder til gengæld blot en enkelt metode GetEnumerator:

  Private Function GetEnumerator() As System.Collections.IEnumerator _ 
    Implements System.Collections.IEnumerable.GetEnumerator
      
    Return New FormCollectionEnumerator(Me)
  EndFunction

Og det er denne metode, der returnerer et object af typen FormCollectionEnumerator (som implementerer IEnumerator interfacet).

Grunden til denne indirekte måde at få tilgang til de tre metoder i IEnumerator interfacet på er, at man gerne vil adskille For Each-løkken fra det konkrete FormCollection objekt. Hver gang der startes en ny For Each-løkke, instansieres der i GetEnumerator et nyt objekt af typen FormCollectionEnumerator (som implicit typekonverteres til et IEnumerator interface), og hver For Each-løkke får derfor sin egen _index tælle-variabel tilknyttet. Denne implementation er et eksempel på det design pattern, der almindeligvis kaldes Iterator.

Rent faktisk kunne vi i dette eksempel have sluppet markant simplere om ved koden. I stedet for at lave FormCollectionEnumerator-klassen, kunne vi blot have benyttet den underliggende ArrayListes GetEnumerator metode. Så en simplere implementation af GetEnumerator ville være:

  Private Function GetEnumerator() As System.Collections.IEnumerator _
    Implements System.Collections.IEnumerable.GetEnumerator

    Return CType(_collection, IEnumerable).GetEnumerator()
  End Function

Eller da GetEnumerator er en metode defineret direkte på ArrayLists:

  Private Function GetEnumerator() As IEnumerator _
    Implements System.Collections.IEnumerable.GetEnumerator

    Return _collection.GetEnumerator()
  End Function

Og FormCollectionEnumerator klassen kunne så droppes helt.

Når den mere besværlige implementation er valgt, er det for at demonstrere det generelle princip ved implementation af IEnumerable. Der er flere situationer, hvor man er tvunget til selv at implementere IEnumerable: Hvis det underliggende lager-objekt ikke implementerer en GetEnumerator metode, hvis man ønsker en anden gennemløbsrækkefølge af sin collection, end det underliggende lagerobjekt tilbyder eller, hvis man kun ønsker at gennemløbe et udvalg af objekterne i collectionen.

Som en lille sidepointe skal det nok lige bemærkes, at For Each-løkken ikke er typestærk - den returnerer altid typeløse objekter, som kan typekonverteres til hvad som helst. På kørselstidspunktet vil (kan) der så selvfølgelig opstå en fejl, hvis man forventer noget andet fra ens collection end det, der rent faktisk kommer ud af den.

Så kompileren vil med andre ord acceptere følgende kode (hvor _forms er af typen FormCollection):

  Dim frm As System.Windows.Forms.TextBox

  For Each frm In _forms
    frm.BackColor = System.Drawing.Color.Red
  Next

Men der vil opstå en runtime-fejl fordi, der ikke kommer objekter af typen System.Windows.Forms.TextBox ud af _forms collectionen.

Alternative implementationer

Man kan implementere sine typestærke collections på mange måder, jeg har her valgt at gøre det på den simplest mulige måde ved selv at definere min typestærke collections members, og ved kun at implementere IEnumerable. Der findes andre interfaces, man også med rette kunne ønske at implementere, når man laver sine egen collections så som System.Collections.IList. Problemet (hvis man betragter det som et sådan) med IList er dog, at det i dets natur er et typeløst interface. Man kan sagtens lave typestærke members på ens collection-klasse, men hvis collection-klassen implementerer IList, vil der så også være en typeløs adgang til collection-klassen. Ved brug af IList interfacet vil udvikleren derfor ikke få typecheck på compile-tidspunktet - men man kan og bør naturligvis lave implementationen sådan, at den i det mindste kaster en exception, hvis man forsøger at tilføje objekter af en anden type til collectionen.

Endelig findes der en abstrakt klasse System.Collections.CollectionBase, som man kan nedarve fra og på den måde få lidt hjælp til implementationen af ens typestærke klasse, man skal dog stadig lave en stor del af arbejdet selv, og det er spørgsmålet om det egentlig giver noget særligt udbytte. Det giver ihvertfald ikke noget nævneværdigt rent typemæssigt.

Download af eksempler

Du kan downloade et par små demoer i henholdsvis VB.NET og C# både af den ovenfor beskrevne løsning, såvel som af en løsning der implementerer IList. Se download boksen øverst i denne artikel.