Archive for the ‘Generics’ Category

WinDbg Q&A: Hvorfor er der så mange instanser af string og object[] på heapen?

Wednesday, October 28th, 2009

Når vi debugger .NET applikationer med WinDbg, vil vi almindeligvis have brug for at inspicere heapen med !dumpheap-kommandoen. Den viser antallet af instanser af de forskellige typer på heapen. Listen er sorteret efter summen af størrelsen for instanserne af de enkelte typer på heapen. Dog skal det bemærkes, at der er tale om selve typernes størrelse, så størrelsen af en type med referencer inkluderer altså ikke størrelsen på de refererede objekter.

Det vil typisk sige, at typer med mange instanser vil være at finde mod slutningen af listen, og blandt dem finder vi meget ofte string og object[], men hvorfor er der så mange af disse i stort set alle .NET applikationer?

String

Lad os begynde med string, for her er forklaringen oplangt. For det første opretter CLRen en hel del strings til diverse konstanter, stier og så videre og jo flere assemblies vi refererer, desto flere af disse får vi med i bagagen. Dertil kommer at instanser af string som bekendt er immutable, så hvis applikationen laver en eller anden form for string-behandling, skaber det hurtig mange instanser. Husk at alle konstante tekster samt alle konstruerede tekster havner på heapen, så derfor løber det hurtig op. Med mindre der er tale om meget store instanser, er det sjældent værd at bekymre sig om.

Object[]

Antallet af string-instanser kan virke overvældende, men når vi tænker over det, er det i virkeligheden ikke så underligt, men hvad med object[]?

Var vi ikke lige blevet enige om, at generics og typestærke collections er sådan en god ide? Hvorfor skulle vi så bruge object[]? Og hvis det ikke er vores applikation, der bruger alle disse forældede collections, er det så Microsofts klasser, der endnu ikke er blevet opdateret?

Forklaringen er faktisk mere subtil end som så. Den underliggende type af List<T> er faktisk object[]. Hvordan kan det passe? Når vi fyrer op under Reflector, kan vi jo tydelig se, at List<T> benytter T[] til opbevaring af elementerne. Så hvis vi har List<string>, får vi vel string[] som den underliggende type. Ja, og nej.

Den underliggende type af List<string> er ganske rigtig string[], men måden CLRen implementerer arrays for referencetyper er ved hjælp af object[]. Lad os se et eksempel. Hvis vi har følgende i Main(), kan vi let finde den aktuelle List<string>-instans ved hjælp af !dso.

var Simpsons = new List<string> {
   "Homer", "Marge", "Bart", "Lisa", "Maggie"
};
0:000> !dso
OS Thread Id: 0x1390 (0)
ESP/REG  Object   Name
0029ed28 01ea2b78 Microsoft.Win32.SafeHandles.SafeFileHandle
0029ed38 01ea2b78 Microsoft.Win32.SafeHandles.SafeFileHandle
0029ed6c 01ea42ac System.Byte[]
0029ed70 01ea2b8c System.IO.__ConsoleStream
0029ed90 01ea4034 System.IO.StreamReader
0029ed94 01ea4034 System.IO.StreamReader
0029edac 01ea4034 System.IO.StreamReader
0029edb0 01ea45c4 System.IO.TextReader+SyncTextReader
0029edd0 01ea45c4 System.IO.TextReader+SyncTextReader
0029ede0 01ea2af4 System.Collections.Generic.List`1[[System.String, mscorlib]] <=== HER!
0029eea4 01ea2a38 System.Object[]    (System.String[])
0029f050 01ea2a38 System.Object[]    (System.String[])
0029f078 01ea2a38 System.Object[]    (System.String[])

Vores instans findes på adressen 01ea2af4, så lad os se nærmere på den.

0:000> !do 01ea2af4
Name: System.Collections.Generic.List`1[[System.String, mscorlib]]
MethodTable: 5f5f58c8
EEClass: 5f3abf0c
Size: 24(0x18) bytes
 (C:\Windows\assembly\GAC_32\mscorlib\2.0.0.0__b77a5c561934e089\mscorlib.dll)
Fields:
      MT    Field   Offset                 Type VT     Attr    Value Name
5f5c4eec  40009d8        4      System.Object[]  0 instance 01ea2b3c _items
5f5eab0c  40009d9        c         System.Int32  1 instance        5 _size
5f5eab0c  40009da       10         System.Int32  1 instance        5 _version
5f5e84dc  40009db        8        System.Object  0 instance 00000000 _syncRoot
5f5c4eec  40009dc        0      System.Object[]  0   shared   static _emptyArray

WinDbg reporterer korrekt, at vi har fat i List<string>, men læg også mærke til, at vi blandt Fields finder _items af typen Object[]. Er det vores data? Lad os se nærmere på indholdet af _items.

0:000> !dumparray -details -nofields 01ea2b3c
Name: System.String[]
MethodTable: 5f5c4eec
EEClass: 5f3aa8a0
Size: 48(0x30) bytes
Array: Rank 1, Number of elements 8, Type CLASS
Element Methodtable: 5f5e88c0
[0] 01ea2a64
    Name: System.String
    MethodTable: 5f5e88c0
    EEClass: 5f3aa498
    Size: 28(0x1c) bytes
     (C:\Windows\assembly\GAC_32\mscorlib\2.0.0.0__b77a5c561934e089\mscorlib.dll)
    String:     Homer
[1] 01ea2a80
    Name: System.String
    MethodTable: 5f5e88c0
    EEClass: 5f3aa498
    Size: 28(0x1c) bytes
     (C:\Windows\assembly\GAC_32\mscorlib\2.0.0.0__b77a5c561934e089\mscorlib.dll)
    String:     Marge
[2] 01ea2a9c
    Name: System.String
    MethodTable: 5f5e88c0
    EEClass: 5f3aa498
    Size: 26(0x1a) bytes
     (C:\Windows\assembly\GAC_32\mscorlib\2.0.0.0__b77a5c561934e089\mscorlib.dll)
    String:     Bart
[3] 01ea2ab8
    Name: System.String
    MethodTable: 5f5e88c0
    EEClass: 5f3aa498
    Size: 26(0x1a) bytes
     (C:\Windows\assembly\GAC_32\mscorlib\2.0.0.0__b77a5c561934e089\mscorlib.dll)
    String:     Lisa
[4] 01ea2ad4
    Name: System.String
    MethodTable: 5f5e88c0
    EEClass: 5f3aa498
    Size: 30(0x1e) bytes
     (C:\Windows\assembly\GAC_32\mscorlib\2.0.0.0__b77a5c561934e089\mscorlib.dll)
    String:     Maggie
[5] null
[6] null
[7] null

Der er vist ingen tvivl om, at det object[] indeholder navnene på vores gule familie, men hvor er sammenhængen mellem List<string> og det underliggende object[]? Hvordan ved CLRen, at vores array indeholder instanser af string?

Lad os først få en ting på det rene. Referencer er referencer, så der er ingen forskel på en reference til en instans af object og en reference til en instans af string. Det er blot referencer, så der er intet galt i, at CLRen bruger et object[] til at gemme referencerne, men hvordan holder den styr på det faktiske indhold?

Ovenstående dump giver ikke nogen svar på dette spørgsmål. SOS kan give indtrykket af, at vi ser det hele, men faktisk pakker SOS et par implementeringsdetaljer ind og viser os dem på anden vis. Vi er altså nødt til at gå uden om SOS, for at få svar på det spørgsmål.

Dumper vi hukommelsen for vores object[], får vi de manglende brikker.

0:000> dd 01c62b3c -4 01c62b3c -4 +0x30 -1
01c62b38  00000000 5f5c4eec 00000008 5f5e88c0
01c62b48  01c62a64 01c62a80 01c62a9c 01c62ab8
01c62b58  01c62ad4 00000000 00000000 00000000

Den noget kryptiske kommando kalder vist på en forklaring. Den instansadresse SOS reporterer, er faktisk forskudt med et enkelt word. Det vil sige, at data for en instans begynder 4 bytes før den viste adresse (på 32 bit forstås). Så for at udskrive indholdet af instansen er vi nødt til at gå 4 bytes tilbage og udskrive til og med instansadressen, minus de fire bytes, plus størrelsen af instansen minus en. Det giver os data for vores object[].

Det første word er instansens SyncBlock, derefter følger instansen egen Method Table, så kommer længden at arrayet (8 i dette tilfælde), og herefter kommer det interessante: Hvad gemmer der sig bag 5f5e88c0? At dømme på værdien kunne det være en Method Table, så lad os se om det skulle være tilfældet.

0:000> !dumpmt 5f5e88c0
EEClass: 5f3aa498
Module: 5f381000
Name: System.String
mdToken: 02000024  (C:\Windows\assembly\GAC_32\mscorlib\2.0.0.0__b77a5c561934e089\mscorlib.dll)
BaseSize: 0x10
ComponentSize: 0x2
Number of IFaces in IFaceMap: 7
Slots in VTable: 196

Det er en Method Table for string og dermed har vi kædet vores object[] sammen med den aktuelle type. De efterfølgende otte words er referencerne til vores fem string-instanser, samt tre tommer pladser til yderligere string-referencer.

Det efterlader blot et spørgsmål: Hvis object[] i realiteten er T[], hvad indeholder det mystiske felt så for et rigtig object array? Method Table for object naturligvis og så passer pengene.

Bag om generiske typer

Wednesday, January 14th, 2009

Generiske typer tillader, at vi kan definere en type med et eller flere typeparametre. En sådan type kaldes en generisk typedefinition. Den er en type i sig selv, men den er ikke så interessant, for vi kan ikke oprette instanser af den. For at kunne det, er vi nødt til at specificere det korrekte antal konkrete typeparametre. Herefter kan vi konstruere konkrete typer baseret på den generiske typedefinition. 

For at illustrere dette med et eksempel, så er List<T> en generisk typedefinition, mens List<string> er en konkret type. 

Forudsætninger

Formålet med dette indlæg er at vise, hvordan generiske typer opfører sig på kørselstidspunktet, men inden vi kommer så langt, er der lige et par basale kendsgerninger, der skal være på plads. De er forhåbentlig kendte af de fleste, men for at sikre et fælles udgangspunkt, følger her en kort gennemgang. 

En type er kendetegnet ved at have en mængde data samt et antal metoder, der som minimum inkluderer en constructor og de metoder, typen arver fra typen Object. Der er altså en direkte forbindelse mellem typer, data og metoder. Det vigtige i denne sammenhæng er forbindelsen mellem typer og metoder. 

Som bekendt opererer .NET og dermed C# med to forskellige slags typer: referencetyper og værdityper. Referencetyper allokeres altid på heapen, og en variabel af en referencetype er således altid en reference til den konkrete instans. Værdityper allokeres som udgangspunkt på stakken, med mindre de er en del af en referencetype, i hvilket fald de allokeres på heapen sammen med den omsluttende type. I alle tilfælde er værdien af en værditype, den faktiske værdi af den konkrete instans og ikke en reference, som tilfældet er for referencetyper. 

Med disse grundbegreber på plads er det tid til at se lidt på generiske typer, som de tager sig ud i afviklingsmiljøet.

Generisk<værditype>

Hvad sker der, når vi opretter en List<int>? Før generiske typer blev indført i CLR 2.0, ville vi få en samling af boxed ints, men det er jo netop dette problem, generiske typer adresserer, så det er ikke tilfældet længere. Vi forventer således, at få en type, der arbejder direkte på ints. Men vi forventer også, at List<T> kan håndtere andre værdityper som f.eks. DateTime. Hvordan håndterer den underliggende generiske type dette?

Vi kan lave en lille applikation, der opretter en liste af hver slags og bruge WinDbg til at undersøge, hvordan disse repræsenteres i CLRen. Så givet to lister med værdityper:

var numbers = new List<int>() { 1, 2, 3 };
var dates = new List<DateTime> { DateTime.Now, DateTime.Now.AddDays(1) };

 
Hvordan ser de ud under afviklingen? Uanset hvordan generiske typer end må være skruet sammen, ved vi, at List<T> er en referencetype, og da vi har to instanser i spil, forventer vi at kunne lokalisere disse på heapen.  Det kan vi gøre ved hjælp af !dumpheap

      MT    Count    TotalSize Class Name
0238c808        1           24 System.Collections.Generic.List`1[[System.DateTime, mscorlib]]
0238c12c        1           24 System.Collections.Generic.List`1[[System.Int32, mscorlib]]

Jeg har forkortet resultatet af kommandoen en del, men ganske som forventet finder vi to instanser. Læg mærke til, hvordan generiske typer repræsenteres. Først har vi selve navnet. Derefter følger `1 – der betyder, at vi i dette tilfælde har en generisk type med et typeparameter. Typen af dette optræder umiddelbart efter. Til sammenligning vil en Dictionary<int, string> være repræsenteret som System.Collections.Generic.Dictionary`2[[System.Int32, mscorlib],[System.String, mscorlib]]. ILDasm bruger samme notation for generiske typer. 

Indtil videre er der intet ud over den spøjse syntaks, der indikerer, at vi har med generiske typer at gøre, men vi ved, at List<T> erklærer et antal metoder, der opererer på elementer af typen T – f.eks. er Add()-metoden defineret som Add(T).  Det interessante er derfor, om det er den samme metode, der håndterer Add(int) og Add(DateTime). Det kan vi se nærmere på ved hjælp af kommandoen !dumpmt og det tilhørende -md flag, der lister alle metoder for en given type. 

Det resulterer i en ret lang liste for hver type, så jeg har forkortet en del, men læg mærke til, hvordan det ser ud for List<int>:

Name: System.Collections.Generic.List`1[[System.Int32, mscorlib]]
--------------------------------------
MethodDesc Table
   Entry MethodDesc      JIT Name
02358658   0238bf14      JIT System.Collections.Generic.List`1[[System.Int32, mscorlib]].Add(Int32)
02358628   0238be54      JIT System.Collections.Generic.List`1[[System.Int32, mscorlib]]..ctor()

Og for List<DateTime>:

Name: System.Collections.Generic.List`1[[System.Int32, mscorlib]]
--------------------------------------
MethodDesc Table
   Entry MethodDesc      JIT Name
0235a730   0238c5f0      JIT System.Collections.Generic.List`1[[System.DateTime, mscorlib]].Add(System.DateTime)
02358778   0238c530      JIT System.Collections.Generic.List`1[[System.DateTime, mscorlib]]..ctor()

Ikke alene bruger metoderne, de specifikke typer, de ligger på forskellige adresser i hukommelsen. Ergo, har de to varianter hver deres implementering af Add(T). Det samme er sågar tilfældet for deres respektive default constructors. List<int> og List<DateTime> ser altså ikke ud til at dele noget på afviklingstidspunktet. 

Det skyldes, at CLRen af hensyn til performance opretter nye, komplette typer for generiske typer, når de initialiseres med forskellige værdityper. Opretter vi flere instanser af f.eks. List<int>, benytter de dog samme kode helt som forventet, men generiske typer med konstrueret med forskellige værdityper resulterer i nye, selvstændige typer. 

Generisk<referencetype>

Anderledes forholder det sig for referencetyper, hvilket vi kan konstatere, hvis vi laver samme øvelse for nedenstående lister:

var strings = new List<string> { "hello", "world" };
var objects = new List<SomeType> { new SomeType(42), new SomeType(1337) };

 
Også her forventer vi to instanser på heapen, og tag mit ord for, at de er der. Lad os i stedet se på, om hvordan Add(T) er håndteret for referencetyper.

Her er et udsnit af metoderne for List<string>:

Name: System.Collections.Generic.List`1[[System.Int32, mscorlib]]
--------------------------------------
MethodDesc Table
   Entry MethodDesc      JIT Name
0235a960   0238cc8c      JIT System.Collections.Generic.List`1[[System.__Canon, mscorlib]].Add(System.__Canon)
0235a858   0238cbcc      JIT System.Collections.Generic.List`1[[System.__Canon, mscorlib]]..ctor()

Og her er det tilsvarende udsnit for List<SomeType>:

Name: System.Collections.Generic.List`1[[System.Int32, mscorlib]]
--------------------------------------
MethodDesc Table
   Entry MethodDesc      JIT Name
023651c9   0238cc8c      JIT System.Collections.Generic.List`1[[System.__Canon, mscorlib]].Add(System.__Canon)
0235a858   0238cbcc      JIT System.Collections.Generic.List`1[[System.__Canon, mscorlib]]..ctor()

Bemærk, at her er adresserne på de listede metoder identiske mellem de to instanser. List<string> kalder altså den samme Add(T) som List<SomeType>. Selv default constructoren er delt mellem de to varianter. 

Læg også mærke til, at de to lister, står anført til at arbejde på en type ved navn System.__Canon. Hvad er nu det for en type? Søger vi efter den på heapen, finder vi ingen instanser, og hvis vi inspicerer den med !name2ee eller i Reflector, ser vi, at den ligner Object til forveksling. 

Navnet kommet af canonical, men hvad bruges typen til? Da vi jo ikke kan oprette instanser af generiske typedefinitioner uden at binde dem til en eller flere konkrete typer, har vi brug for et sted at placere koden for generiske typer, så de kan deles mellem forskellige varianter af den generiske type. Det er her System.__Canon kommer ind i billedet. Koden til List<string> og List<SomeType> er således delt gennem den konkrete instans baseret på __Canon. På afviklingstidspunktet finder CLRen ud af hvilken specifik type __Canon er stand-in for, så forekomster af T kan udskiftes med den konkrete type. 

Med andre ord opfører generiske typer sig altså anderledes, når de bruges med referencetyper, end når de anvendes med værdityper. Generiske typer konstrueret med en referencetype deler kode. Generiske typer konstrueret med en værditype deler ikke kode.

List<T> under overfladen

Wednesday, December 5th, 2007

Med introduktionen af .NET 2.0 fandt ArrayList en afløser i List<T>. Skønt ordet ”array” ikke længere indgår i navnet, er List<T> faktisk stadig baseret på Array, hvilket vi kan konstatere ved hjælp af Reflector.

De største fordele ved List<T> i forhold til ArrayList er typefastheden og en ganske betydelig performanceforbedring i forbindelse med håndtering af værdityper (value types). Typefastheden leder til simplere kode, da vi undgår at skulle undersøge type på og om nødvendig caste elementer, når vi piller dem ud af listen. Performanceforbedringen kommer af, at vi undgår at box/unboxe i forbindelse med værdityper.

Så for at gøre en lang historie kort: List<T> er langt at foretrække frem for ArrayList, så for at få mest ud af den, er det en god ide at se på, hvordan den er implementeret. Som nævnt er List<T> internt baseret på en Array. Det betyder, at skønt List<T> præsenterer en datastruktur uden fast størrelse, er dette blot en illusion. Når man opretter en List<T>, har den en størrelse, og når der er behov for mere plads, er typen således nødsaget til at oprette en ny, større struktur og flytte alle elementer til denne. Det er selvsagt ikke en billig operation.

Som udgangspunkt afsættes der faktisk ikke plads i en List<T>, hvilket man kan verificere ved at konstatere at Capacity indledningsvis sættes til 0. Så snart der tilføjes et element, afsættes der plads til 4 elementer, og herefter fordobles kapaciteten, hver gang behovet opstår. Det vil sige, at det femte element (og her tænker jeg ikke på filmen) resulterer i at Capacity stiger til 8, det niende element får Capacity til at stige til 16 og så fremdeles. Det betyder, at der for små lister kan spildes meget tid på disse interne omrokeringer, og derfor er det en god ide, at oprette sine List<T>-instanser med en passende kapacitet via den tilhørende constructor.

For meget store lister er dette overhead mindre betydende, men her kan man optimere pladsforbruget ved at sætte en passende kapacitet, så man kan undgå, at der afsættes for meget plads til referencer. Har man f.eks. en liste med 1,1 million navne, vil standardopførelsen gøre, at der afsættes plads til 2^21 eller 2.097.152 elementer. Så uanset hvad er det en god ide at sætte en indledende kapacitet, hvis man på nogen måde har mulighed for at komme med et kvalificeret gæt på den nødvendige kapacitet.

Mange implementeringer af en List-type er baseret på det, man kalder en hægtet listet (linked list), i hvilken brugeren har en reference til det første element i listen, og hvert element desuden har en reference til det efterfølgende element (eller null for sidste element i listen). En sådan implementering vil typisk tilbyde en Insert()- og en Add()-metode ganske som tilfældet er for List<T>. I dette tilfælde vil Insert() almindeligvis være særdeles effektiv, idet det kun kræver justering af to referencer at indsætte et element forrest i listen. Med mindre listen også gemmer en reference til det sidste element, vil det til gengæld være dyrt at indsætte elementer sidst i listen, da dette kræver, at vi løber hele listen igennem via referencerne i de enkelte elementer.

Sådan er det ikke med List<T>. Her forholder det sig faktisk lige omvendt. At tilføje et element via Add() er særdeles effektivt, men vil dog medføre et overhead, hvis kapaciteten skal udvides. Faktisk fremgår det af dokumentationen, at Add() som udgangspunkt er en O(1) operation. Skal listen udvides bliver det dog en O(n) operation.

Insert() er derimod en O(n) operation ifølge dokumentationen, men hvis vi tager et kig på implementeringen ved hjælp af Reflector (se nedenfor), ser vi, at dette faktisk ikke gælder, hvis Insert() resulterer i at elementet indsættes i slutningen af listen. I det tilfælde er Insert() stort set lige så hurtig som Add() – og i realiteten er der tale om O(1).

public void Insert(int index, T item)
{
   if (index > this._size) {
      ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.index,
         ExceptionResource.ArgumentOutOfRange_ListInsert);
   }
   if (this._size == this._items.Length) {
      this.EnsureCapacity(this._size + 1);
   }
   if (index < this._size) {
      Array.Copy(this._items, index, this._items, index + 1, this._size - index);
   }
   this._items[index] = item;
   this._size++;
   this._version++;
}

Som det også fremgår af ovenstående, ligger Insert() naturligvis under for de samme restriktioner som Add() i det tilfælde, at kapaciteten skal udvides.

Så hvad kan vi lære af at se på implementeringen af List<T>? For det første, vil det som sagt være en god ide at vælge en passende kapacitet ved oprettelsen af sin liste for at undgå de tunge omallokeringer og det voldsomme overhead ved store lister. Dernæst kan vi konstatere, at i og med at List<T> ikke er implementeret som en hægtet liste, er Add() langt at foretrække frem for Insert(). Faktisk er omkostningerne ved sidstnævnte så store, at man virkelig kun skal bruge den som sidste udvej. Og sidst men ikke mindst, er der ingen grund til fortsat at bruge ArrayList.

Readonly og typer

Friday, July 13th, 2007

C# giver mulighed for, at felter kan erklæres som readonly, så i nedenstående eksempel erklæres og sættes ReadOnlyNumber til værdien 1, hvorefter værdien ikke længere kan ændres. Forsøg på at ændre værdien vil blive fanget af compileren som en fejl.

public class SomeType {
   public readonly static int ReadOnlyNumber = 1;
}

Console.WriteLine(SomeType.ReadOnlyNumber);
SomeType.ReadOnlyNumber = 2; // Oversætter ikke

Læg mærke til at jeg skrev, at værdien ikke kan ændres. int er en værditype, så det vil sige, at vi ikke kan ændre indholdet af ReadOnlyNumber, hvilket er præcis det, vi ønsker her.

Med referencetyper forholder det sig anderledes. Indholdet af en referencetype er altid en pointer til den faktiske instans på heapen. Ergo er værdien af en referencetype selve pointeren, hvilket kan give lidt overraskende resultater i forbindelse med readonly som i eksemplet nedenfor.

public class SomeType {
   public readonly static int[] OneTwoThreeFour = { 1, 2 , 3 , 4 };
}

SomeType.OneTwoThreeFour[3] = 5; // Dette er tilladt!

Det eneste, der er readonly, er jo referencen – ikke det, den peger på. Ergo, kan vi ændre indholdet af den array, vores readonly reference peger på! Det eneste, vi ikke kan, er at sætte referencen til at pege på en anden array. Nedenstående oversætter således ikke.

// Oversætter ikke
SomeType.OneTwoThreeFour = new int[] { 4, 3, 2, 1 };

Med referencetyper er readonly således ikke særlig effektiv, idet indholdet af det referencen peger på kan ændres efter forgodtbefindende.

Vil vi have reelle readonly-referencetyper, er vi derfor nødt til at tage lidt mere drastiske midler i brug. Ved at indkapsle en type i en type, der kun tilbyder readonly-semantik, kan vi opnå det ønskede resultat. For collections er der i .NET 2.0 tilføjet en type til formålet ReadOnlyCollection<T>, som vi kan bruge i vores eksempel.

public class SomeType {
   public static ReadOnlyCollection<int> OneTwoThreeFourReadOnly =
      new ReadOnlyCollection<int>(new int[] { 1, 2, 3, 4 });
}

SomeType.OneTwoThreeFourReadOnly[3] = 5; // Oversætter ikke

Så kan vi ikke længere sætte indholdet af vores array, men vi er endnu ikke helt i mål, for nedenstående er stadig tilladt.

SomeType.OneTwoThreeFourReadOnly =
   new ReadOnlyCollection<int>(new int[] { 4, 3, 2, 1 });

Og det var jo ikke meningen! Til alt held ved vi, hvordan vi adresserer det problem: readonly på selve referencen. Derfor ender vi med en implementering, der ser således ud:

public class SomeType {
   public readonly static ReadOnlyCollection<int> OneTwoThreeFourReadOnlyForGood
      = new ReadOnlyCollection<int>(new int[] { 1, 2, 3, 4 });
}

// Begge dele fanges nu af compileren
SomeType.OneTwoThreeFourReadOnlyForGood[3] = 5;
SomeType.OneTwoThreeFourReadOnlyForGood =
   new ReadOnlyCollection<int>(new int[] { 4, 3, 2, 1 });

For at implementere en brugbar ReadOnly-indkapsling, er de to typer naturligvis nødt til at dele et antal interfaces. ReadOnlyCollection<T> implementerer IList<T>, ICollection<T>, IEnumerable<T>, IList, ICollection og IEnumerable, hvilket gør, at den kan bruges i de sammenhænge, man ellers ville bruge en collection.

Den kvikke læser vil sikkert bemærke, at for at implementere disse interfaces, skal ReadOnlyCollection<T> også implementere metoder, der modificerer indholdet af den aktuelle collection, og det gør den også. Disse er dog eksplicitte interface implementeringer, hvilket vil sige, at de ikke dukker op i Visual Studios intellisense. For at få adgang til de metoder, kræves en interface-reference til typen, men selv ikke her er der noget at hente, for metoderne er som forventelig implementeret ved, at de blot kaster en NotSupportedException.

Skal man lave en readonly-variant af en af sine egne referencetyper, er det altså fremgangsmåden.

Fleksible grænseflader

Wednesday, May 23rd, 2007

Med introduktionen af generics i .NET 2.0 har vi fået langt bedre muligheder for at lave genbrugelig kode, men som så meget andet kommer det naturligvis ikke af sig selv. Genbrugelig kode kræver, at vi tænker over kodens grænseflader og i den sammenhæng er følgende gode råd at følge:

  1. Parametre til metoder skal være så generelle som mulige
  2. Returparametre fra metoder skal være så specifikke som mulige

I begge tilfælde gælder det naturligvis, at det skal være i forhold til den aktuelle situation og ikke det teoretisk mulige, men lad os se på et par eksempler.

Lad os forestille os, at vi har en metode, der som input tager en liste af vores SomeType-instanser. Den mest nærliggende implementering er:

public static void MostSpecificParameter(List<SomeType> l) {
      foreach (SomeType st in l) {
            st.DoSomething();
         }
      }

Den fungerer, men den er ikke særlig fleksibel, da den kræver at vores samling af SomeType-objekter er samlet i en List<T>. Hvis vi ikke har brug for List<T>s funktionalitet, kan vi gøre metoden lidt mere fleksibel, ved at specificere at input skal være af en type, der implementerer IList<T>.

public static void SpecificParameter(IList<SomeType> l) {
   foreach (SomeType st in l) {
      st.DoSomething();
   }
}

På den måde kan vi nu anvende metoden, uanset om vores SomeType-objeter er opbevaret i en List<T> eller i en array (SomeType[]), men vi kan faktisk gøre det endnu bedre:

public static void LeastSpecificParameter(IEnumerable<SomeType> l) {
   foreach (SomeType st in l) {
      st.DoSomething();
   }
}

Ved at baserer metodens input på typer, der implementerer IEnumerable, har vi gjort det særdeles frit for brugeren, hvordan inputdata skal opbevares. Ovenstående fungerer uændret med blandt andet List<SomeType>, SomeType[] og Stack<SomeType>. Så hvis List<T>s funktionalitet ikke er en nødvendighed for metoden, er alternativerne langt mere anvendelige. Selvfølgelig ville en metode med object som inputargument være endnu mindre specifik, men som nævnt er vi ikke interesseret i den teoretisk mest fleksible grænseflade, men den mest fleksible grænseflade, der passer til den opgave vi forsøger at løse. Da koden ovenfor løber en samling af SomeType-objekter igennem vil det kun give problemer at lave en variant, der tager object som input. Reglen er at input skal være så generel som mulig i forhold til den givne opgave.

Med returtyper forholder det sige lige omvendt. Her er det bedst at anvende en så specifik type som mulig, og derfor er det som udgangspunkt også bedre at returnere typer frem for interfaces, så hvis vi har:

public static SomeDerivedType MostSpecificReturn() {
   return new SomeDerivedType();
}

public static SomeType NotSoSpecificReturn() {
   return new SomeDerivedType();
}

public static object LeastSpecificReturn() {
   return new SomeDerivedType();
}

er førstnævnte langt mere fleksibel for brugerne end de efterfølgende. Førstnævnte understøtter følgende brugsmønstre:

SomeDerivedType SpecificReturn1 = MostSpecificReturn();
SomeType        SpecificReturn2 = MostSpecificReturn();
object          SpecificReturn3 = MostSpecificReturn();

De to andre implementeringer gør det gradvist mere bøvlet for brugerne som nedenstående illustrerer:

SomeDerivedType NotSoSpecificReturn1 = (SomeDerivedType)NotSoSpecificReturn();
SomeType        NotSoSpecificReturn2 = NotSoSpecificReturn();
object          NotSoSpecificReturn3 = NotSoSpecificReturn();

SomeDerivedType LeastSpecificReturn1 = (SomeDerivedType)LeastSpecificReturn();
SomeType        LeastSpecificReturn2 = (SomeDerivedType)LeastSpecificReturn();
object          LeastSpecificReturn3 = LeastSpecificReturn();

Da metoder, der returnerer referencetyper afleverer en reference til en instans af et eller andet, er der som regel ikke noget at vinde ved at returnere et interface i stedet for den faktiske instans.

public static IList<string> ThisIsNotOptimal() {
   return new List<string>();
}

IList<string> l1 = ThisIsNotOptimal ();

Skønt ovenstående fungerer fint, har vi begrænset brugerens anvendelse af den returnerede liste til det interface IList<T> specificerer. Det vil f.eks. sige, at brugeren ikke efterfølgende kan kalde Sort() på listen, da denne metode ikke er en del af IList<T>.

Den eneste fordel ovenstående giver os i forhold til at returnere en List<T> er, at vi på et senere tidspunkt kan ændre implementeringen til f.eks. at returnere en string[]. Da string[] også implementerer IList<T> er det blot en implementeringsdetalje, brugeren ikke behøver at kende. Har man derimod ikke behov for at kunne ændre implementeringen på den måde, giver man brugeren en langt mere fleksibel grænseflade ved at returnere en så specifik type som mulig – i dette tilfælde en List<string>.

List, SortedList og en sorteret liste

Friday, May 11th, 2007

Med .NET 2.0 introducerede Microsoft en helt ny samling generic collections. Blandt disse er den meget anvendelige List<T> – en ordnet, men ikke sorteret liste af elementer. List<T> tilbyder en Sort()-metode, der kan sortere indholdet af listen, men det er både bøvlet og ikke særlig effektivt at sortere hele listen hver gang et element indsættes. Har man brug for en liste med sorterede elementer, er List<T> derfor ikke altid det oplagte valg.

Blandt de mange collections er også SortedList<T>, men det er ikke som navnet ellers antyder en liste men derimod et dictionary. Ergo kræver typen, at data er ordnet i et nøgle/værdi-forhold. Derudover kræver SortedList<T>, at alle nøgler er unikke, så alt i alt er den ikke særlig tæt på at være en sorteret udgave af List<T>. Derfor er der ikke umiddelbart noget godt bud på en collection til repræsentation af en sorteret liste, men inden vi fortvivler, er det faktisk en god ide at kaste endnu et blik på List<T>.

List<T> implementerer nemlig BinarySearch(). Metoden returnerer indeks på det søgte element, hvis det findes i listen eller den binære komplementærværdi til det indeks element ville findes på, hvis det var i listen. Den opførsel kan vi bruge til at tilføje elementer til listen, så listen altid er sorteret. BinarySearch() kræver naturligvis, at listen er sorteret, men hvis alle elementer indsættes på deres korrekte plads, er listen sorteret uden, at vi behøver at kalde Sort(). Add() kunne se ud som følger:

public class OrderedList<T> : IList<T> {
   public void Add(T item) {
      int index = list.BinarySearch(item);
      if (index >= 0) {
         list.Insert(index, item);
      } else {
         list.Insert(~index, item);
      }
   }
   ...
}

OrderedList<T> implementerer en Add()-metode, der indsætter elementet på den korrekte plads i listen ved hjælp af BinarySearch(). Det betyder naturligvis, at Add() bliver noget langsommere end List<T>.Add(), men meget hurtigere end List<T>.Add() efterfulgt af List<T>.Sort() og i og med at listen er sorteret, kan vi implementere særdeles effektive udgaver af IndexOf() og Contains().

public int IndexOf(T item) {
   int index = list.BinarySearch(item);
   return index < 0 ? -1 : index;
}

public bool Contains(T item) {
   return list.BinarySearch(item) >= 0;
}

Hvor List<T> er tvunget til at lave en lineær søgning for IndexOf() og Contains(), kan vi nu gøre det samme som en O(log n)-operation. Der er altså store gevinster at hente, hvis man har brug for at slå op i listen.

Hvornår man skal bruge List<T> eller den sorterede variant afhænger meget af omstændighederne. List<T>s Sort()-metode bliver hurtigt meget dyr, så hvis man har brug for at kalde den ofte (i værste fald efter hver tilføjelse af et element), bliver OrderedList<T> hurtig meget mere effektiv. Har man på den anden side en applikation, der ikke har brug for at listen er sorteret, mens elementer bliver tilføjet, er List<T> at foretrække. I så fald kan man indsætte alle elementer og derefter kalde Sort(), for selvom Sort() tager tid opvejes det af at Add() er langt hurtigere for List<T> end den tilsvarende udgave baseret på BinarySearch().

Jeg har lavet en implementering af en sorteret liste, der kan hentes her til fri afbenyttelse.