Synkronisering

Med asynkron programmering dukker ofte behovet opp for kontrollere hvilken tråd som bruker bestemte ressurser på et gitt tidspunkt.

Det er ganske vanlig å kun tillate èn tråd, i stedet for flere likt. Har man ingen kontroll risikerer man at oppdaterte/endrede data blir mangelfulle, at ting ikke blir gjort i riktig rekkefølge, o.s.v. Her kommer lock, Monitor, [Synchronization] med flere, inn i bildet.

Lock og Monitor

Vanligvis kommer man langt med lock som i praksis fungerer slik:

lock (this) // hvis dette er inni en privat metode
{
   ...
}

.. eller slik:

private object låsObjekt = new object(); // utenfor metoden
...
lock (låsObjekt) // hvis dette er inni en åpen metode
{
   ...
}

Og siden lock bare er en kortform for Monitor kan man visst i følge boka også kjøre på med følgende:

private object låsObjekt = new object(); // utenfor metoden
...
Monitor.Enter(låsObjekt) // inni åpen eller stengt metode
try
{
   ...
}
finally
{
   Monitor.Exit(låsObjekt)
}

Fordelen er visst at dette muliggjør større kontroll fordi Monitor blant annet har støtte for å kontakte ventende tråder.

[Synchronization]

Vil man ordne låsing for en hel klasse trenger man bare å plassere [Synchronization] fremfor klassedefinisjonen sin. Ulempen er at dette vil gjelde for absolutt alt innhold – til og med de metodene som ikke trenger det.

Asynkron programmering

Etter å ha brukt ca èn uke har jeg nå fått god oversikt over de forskjellige måtene man kan bedrive asynkron programmering på i NET. Og ikke bare har jeg fått repetert gammelt stoff, jeg har også fått lært mye nytt.

Hvorfor jeg ikke har brukt tid på dette før vet jeg ikke helt. Jeg har alltid hatt for vane å opprette egne tråder og ikke tenkt mer på det, det er jo så enkelt og intuitivt:

new Thread(() =>
{
   ...
}).Start();

Delegater

Først, for å i det hele tatt kunne komme i gang med å lære hvilke alternativer som fantes måtte jeg lære hvordan delegater fungerer: En delegat er simpelthen bare en egen type for å holde på referanser til funksjoner/metoder. Disse brukes fordi man ikke alltid kan benytte de aktuelle funksjonene/metodene direkte. En delegat har samme signatur som de funksjonene/metodene den skal kunne referere til.

Om man først tar utgangspunkt i to enkle mattefunksjoner:

static double Pluss(double tall1, double tall2)
{
   return tall1 + tall2;
}
static double Minus(double tall1, double tall2)
{
   return tall1 - tall2;
}

.. så gir dette utgangspunktet for hvordan definisjonen til delegaten må se ut:

delegate double MatteFunksjon(double tall1, double tall2);

Deretter kan man opprette en stk. for hver av de opprinnelige funksjonene:

MatteFunksjon PlussDelegat = new MatteFunksjon(Pluss);
MatteFunksjon MinusDelegat = Minus; // en kortere form

Disse delegatene, altså PlussDelegat og MinusDelegat, kan nå brukes hver gang man ønsker å benytte Pluss og eller Minus i en eller annen asynkron sammenheng.

IAsyncResult

Boka jeg bruker begynte dette temaet med å ta i bruk delegater og (I)AsyncResult.

Et eksempel på hvordan dette fungerer i praksis, med Pluss og PlussDelegat:

IAsyncResult plussAsyncResultat = PlussDelegat.BeginInvoke(2, 2, null, null);
while (!plussAsyncResultat.IsCompleted)
{
   ...
}
double svar = PlussDelegat.EndInvoke(plussAsyncResultat);

Her sendes tallene 2 og 2 til Pluss (som skal kjøre asynkront) via PlussDelegat, og det returneres en instans av AsyncResult via en IAsyncResult-referanse. Denne brukes helt til slutt for å hente ut svaret.

I mellomtiden venter man på at plussAsyncResultat skal bli ferdig.

En alternativ løsning kan være å benytte en egen funksjon/metode som tar seg av det som skal gjøres etter Pluss er ferdig.

I praksis kan man sause alt sammen inn i BeginInvokePlussDelegat:

IAsyncResult plussAsyncResultat = PlussDelegat.BeginInvoke(2, 2, new AsyncCallback(PlussResultat), new double[] {2,2});

.. hvor PlussResultat får ansvaret for å avslutte:

void PlussResultat(IAsyncResult iar)
{
   double[] tall = (double[]) iar.AsyncState;
   double svar = PlussDelegat.EndInvoke(iar);
   ...
}

Dermed blir det aldri noe venting i den opprinnelige tråden.

Thread

Min favoritt som er egne tråder er nok fortsatt det alternativet jeg liker best:

Thread tråd = new Thread(() =>
{
   double svar = Pluss(2, 2);
   ...
});
tråd.Start();

I stedet for anonyme funksjoner kan man selvsagt også bruke en funksjon/metode som allerede eksisterer, hvis den har riktig signatur:

Thread tråd = new Thread(FunksjonMedRiktigSignatur); // gitt funksjon må returnere void uten å ta inn parametere

.. eller:

Thread tråd = new Thread(FunksjonMedRiktigSignatur); // gitt funksjon må returnere void og akseptere èn stk. object inn
...
tråd.Start(new double[] {2, 2});

Det som kan være et problem her er at ikke noe returneres. En mulig løsning er da å lagre resultatet i en variabel opprettet utenfor tråden, men siden dette åpner for å få en «race condition» må man være påpasselig med hvordan man gjør det.

ThreadPool

Mye omtalte ThreadPool som ofte er anbefalt, men som jeg aldri har lært å like:

WaitCallback plussDelegatForTrådSamling = new WaitCallback((data) =>
{
   double svar = Pluss(2, 2);
   ...
});
ThreadPool.QueueUserWorkItem(plussDelegatForTrådSamling);

Også her kan man bruke en eksisterende funksjon/metode hvis noen passer:

WaitCallback minusDelegatForTrådSamling = new WaitCallback(FunksjonMedRiktigSignatur); // gitt funksjon må returnere void og ta inn èn stk. object
ThreadPool.QueueUserWorkItem(minusDelegatForTrådSamling, new double[] {2, 2});

Task

Et alternativ jeg har begynt å like er Task siden det gir lite kode:

Task.Factory.StartNew(() =>
{
   double svar = Pluss(2, 2);
   ...
});

Også her kan man angi en eksisterende funksjon/metode hvis signaturen passer:

using (Task plussOppgave = Task.Factory.StartNew(FunksjonMedRiktigSignatur)) // gitt funksjon må returnere void uten å ta inn parametere
{
   while (!plussOppgave.IsCompleted)
   {
      ...
   }
}

Task er støttet fra og med versjon 4.0 av NET.

async og await

I motsetning til (I)AsyncResult, Thread, ThreadPool og Task så er async og await litt vanskelig å komme inn i til å begynne med.

Først et eksempel på hvordan funksjonen/metoden kan se ut her for at man skal kunne bruke den asynkront:

async void AsyncPlussResultat(double tall1, double tall2)
{
   await Task.Run(() =>
   {
      double svar = Pluss(tall1, tall2);
      ...
   });
}

.. som kan brukes med:

AsyncPlussResultat(2, 2);

(Man trenger riktignok ikke alltid mate inn data slik som her, man kan ha funksjoner/metoder som ikke tar parametere i det hele tatt.)

Som med Thread og ThreadPool er det også her litt kronglete å få fraktet data helt tilbake dit det opprinnelige kallet begynner. Men ved å bruke async og await med Task er det fullt mulig:

async Task<double> AsyncPluss(double tall1, double tall2)
{
   return await Task.Run(() =>
   {
      double svar = Pluss(tall1, tall2);
      return svar;
   });
}

.. hvor denne kalles ved å f.eks. bruke følgende:

Task<double> oppgave = AsyncPluss(2, 2);
while (!oppgave.IsCompleted)
{
   ...
}
double svar = oppgave.Result;

Altså får man også her returnert et svar, om man er villig til å vente litt.

Parallel

Siste alternativet (akkurat nå) er Parallel, her med bruk av Invoke:

Parallel.Invoke(() =>
{
   double svar = Pluss(2, 2);
   ...
},
() =>
{
   double svar = Minus(2, 2);
   ...
});

Parallel har også For og ForEach for å trygt kunne kjøre gjennom «collections», og er støttet fra og med 4.6 av NET.

Mal for klasse med egen ressursfrigjøring

Har man en klasse som benytter mye «managed» minne kan man selv implementere og sende kall til Dispose() for å frigjøre minnet med en gang man er ferdig.

Om man bruker «unmanaged» minne bør også dette frigjøres her, i tillegg må man implementere metoden ~NavnPåKlasse() som GC kan bruke i tillfelle man glemmer å sende kall til Dispose() på egenhånd.

Her er en ferdig mal som inneholder alt dette:

class Klasse : IDisposable
{
   private bool brukerFortsattRessurser = true;

   ~Klasse()
   {
      // Frigjør bare 'unmanaged' ressurser:
      FrigjørRessurser(false);

      Console.WriteLine("~Klasse() kalt");
   }

   public void Dispose()
   {
      // Frigjør både 'managed' og 'unmanaged' ressurser:
      FrigjørRessurser(true);

      // Fortell GC at ~Klasse() ikke trengs å kjøres:
      GC.SuppressFinalize(this);

      Console.WriteLine("Dispose() kalt");
   }

   private void FrigjørRessurser(bool beggeTyper)
   {
      // Frigjør både 'managed' og 'unmanaged' ressurser hvis dette ikke er gjort:
      if (brukerFortsattRessurser)
      {
         // Frigjør 'managed' ressurser her:
         if (beggeTyper)
         {
            // ..
         }

         // Frigjør 'unmanaged' ressurser her
         // ..

         brukerFortsattRessurser = false;
      }

      Console.WriteLine("FrigjørRessurser(" + beggeTyper + ") kalt");
   }
}

Her bør hver nye metode man lager først kontrollere at brukerFortsattRessurser er sann. Er Dispose() allerede kjørt burde nye metodekall gi unntak (altså «exception»).

using

Noen gang lurt på hva som er greia med using (Klasse objekt = new Klasse())?

Vel, dette er enkelt og greit en kortform for try med finally hvor man i sistnevnte sender et kall til Dispose() for å gjøre seg ferdig med brukte ressurser i det dynamiske minnet (altså på «heap»-en).

using (RessursKrevendeKlasse k = new RessursKrevendeKlasse())
{
   // .. bruk av k.metode(..) her
}

.. kan derfor sidestilles med:

RessursKrevendeKlasse k = new RessursKrevendeKlasse();
try
{
   // .. bruk av k.metode(..) her
}
finally
{
   k.Dispose();
}

LINQ

Etter å ha brukt noen timer på LINQ de siste dagene viser det seg at dette er utrolig enkelt å komme i gang med.

Et svært konkret og enkelt eksempel:

string[] tekst = { "Dette", "er", "en", "test!" };
var ordFraTekstMedBokstavenT = from ord in tekst where ord.Contains("t") select ord;

Her henter man ut alle ord som inneholder bokstaven t.

Disse kan man enkelt ta i bruk for videre bruk:

foreach (var ord in tekst) Console.Write(ord + " "); 

Som dermed skriver ut Dette test!.

Man kan selvsagt behandle andre ting enn bare tekst også:

Bil[] mineBiler = new Bil[]
{
   new Bil("Toyota", "Corolla", 1993),
   new Bil("Toyota", "Corolla Verso", 2003),
};
var bilNyereEnn2000 = from bil in mineBiler where bil.Årgang > 2000 select bil;
Console.Write("Biler fra år 2000 eller senere: ");
foreach (var bil in bilNyereEnn2000) Console.Write(bil);

Her blir resultatet Biler fra år 2000 eller senere: Toyota Corolla Verso 2003.

(Her har klassen Bil en «overloaded» ToString() som forenkler utskrifter.)

TBC

operator overloading

Som i C++ og en rekke andre språk (bortsett fra f.eks. Java) kan man i C# «overloade» operatorer i klassene man lager. Dette gjør koden mer lettleselig samt kortere.

Et helt konkret eksempel som de fleste vil forstå:

Tall t1 = new Tall(1);
Tall t2 = new Tall(2);
Tall sum = t1.Pluss(t2);

vs:

Tall t1 = new Tall(1);
Tall t2 = new Tall(2);
Tall sum = t1 + t2;

I første kodesnutten opprettes det to tall som objekter av typen Tall. (Tall er her en egenskapt klasse bare for eksempelets skyld.) Her må man bruke metoden Pluss som er del av Tall når man skal legge sammen de to tallene og få en sum.

Dette er unødvendig tungvint, i stedet burde man gjøre slik det er gjort nederst – noe som er mulig takket være «operator overloading».

Så hvordan ordner man «operator overloading»? Det er faktisk ganske enkelt!

Men å lage en generisk klasse for å oppbevare et tall er rimelig tullete og bortkastet, så her kommer en klasse med litt mer substans:

class Garasje : IEnumerable
{
   private ArrayList parkerteKjøretøy = new ArrayList();

   public int AntallKjøretøy
   { get { return parkerteKjøretøy.Count; } }

   public void Parker(Kjøretøy kjøretøy)
   {
      parkerteKjøretøy.Add(kjøretøy);
   }

   public void KjørUt(Kjøretøy kjøretøy)
   {
      parkerteKjøretøy.Remove(kjøretøy);
   }
   
   public IEnumerator GetEnumerator()
   {
      return parkerteKjøretøy.GetEnumerator();
   }

   public override string ToString()
   {
      string innhold = "";
      foreach (var k in parkerteKjøretøy) innhold += ("'" + k + "'" + " ");
      return innhold;
   }

   public override bool Equals(object garasje)
   {
      Garasje annenGarasje = garasje as Garasje;
      if (AntallKjøretøy == annenGarasje.AntallKjøretøy)
      {
         for (int i = 0; i < annenGarasje.AntallKjøretøy; i++)
            if (this[i] != annenGarasje[i]) return false;

         return true;
      }

      return false;
   }

   public override int GetHashCode()
   {
      int hashcode = 0;
      foreach (Kjøretøy k in parkerteKjøretøy) hashcode += (k.GetHashCode() + k.Navn.Length);
      return hashcode;
   }

   // []
   public Kjøretøy this[int index]
   {
      get { return parkerteKjøretøy[index] as Kjøretøy; }
      set { parkerteKjøretøy.Insert(index, value); }
   }

   // +
   public static Garasje operator +(Garasje garasje1, Garasje garasje2)
   {
      Garasje nyGarasje = new Garasje();

      foreach (Kjøretøy k in garasje1) nyGarasje.Parker(k);
      foreach (Kjøretøy k in garasje2) nyGarasje.Parker(k);

      return nyGarasje;
   }

   // +=
   public static Garasje operator +(Garasje garasje, Kjøretøy kjøretøy)
   {
      garasje.Parker(kjøretøy);
      return garasje;
   }

   // -
   public static Garasje operator -(Garasje garasje1, Garasje garasje2)
   {
      Garasje nyGarasje = new Garasje();

      foreach (Kjøretøy k in garasje1) nyGarasje.Parker(k);
      foreach (Kjøretøy k in garasje2) nyGarasje.KjørUt(k);

      return nyGarasje;
   }

   // -=
   public static Garasje operator -(Garasje garasje, Kjøretøy kjøretøy)
   {
      garasje.KjørUt(kjøretøy);
      return garasje;
   }

   // >
   public static bool operator >(Garasje garasje1, Garasje garasje2)
   {
      return garasje1.AntallKjøretøy > garasje2.AntallKjøretøy;
   }

   // <
   public static bool operator <(Garasje garasje1, Garasje garasje2)
   {
      return garasje1.AntallKjøretøy < garasje2.AntallKjøretøy;
   }

   // >=
   public static bool operator >=(Garasje garasje1, Garasje garasje2)
   {
      return garasje1 == garasje2 || garasje1.AntallKjøretøy > garasje2.AntallKjøretøy;
   }

   // <=
   public static bool operator <=(Garasje garasje1, Garasje garasje2)
   {
      return garasje1 == garasje2 || garasje1.AntallKjøretøy < garasje2.AntallKjøretøy;
   }

   // ==
   public static bool operator ==(Garasje garasje1, Garasje garasje2)
   {
      return garasje1.Equals(garasje2);
   }

   // !=
   public static bool operator !=(Garasje garasje1, Garasje garasje2)
   {
      return !(garasje1 == garasje2);
   }
}

Her kan man legge til og fjerne kjøretøy via både egne metoder og tabellsyntaks [ ], enkelt få oversikt over innholdet i garasjen, samt gjøre en rekke forskjellige garasjesammenligninger.

Alle metodene etter kommentaren // [ ] er operatorer som blir overloadet.

Tilhørende Kjøretøy-klasse:

class Kjøretøy
{
   public string Navn
   { get { return navn; } }
   private string navn;

   public Kjøretøy(string navn)
   {
      this.navn = navn;
   }

   public override string ToString()
   {
      return navn;
   }

   public override bool Equals(object kjøretøy)
   {
      return Navn == (kjøretøy as Kjøretøy).Navn;
   }

   public override int GetHashCode()
   {
      return navn.GetHashCode();
   }
}

I grunn ikke så mye av interesse her siden klassen kun pakker inn tekst.

For å teste Garasje kan man kjøre følgende:

static void Main(string[] args)
{
   Garasje bilGarasje1 = new Garasje();
   bilGarasje1[0] = new Kjøretøy("Suzuki GSX F 95");
   bilGarasje1[1] = new Kjøretøy("Toyota Corolla Verso 03");
   Console.WriteLine("Gammel garasje: " + bilGarasje1);

   Garasje bilGarasje2 = new Garasje();
   bilGarasje2[0] = new Kjøretøy("Toyota Corolla 93");
   Console.WriteLine("Ny garasje: " + bilGarasje2);

   Garasje bilGarasjer = bilGarasje1 + bilGarasje2;
   Console.WriteLine("Begge: " + bilGarasjer);

   Garasje sykkelGarasje = new Garasje();
   sykkelGarasje += new Kjøretøy("Grønn skranglesykkel");
   sykkelGarasje += new Kjøretøy("Sportssykkel til 50 000");
   sykkelGarasje -= new Kjøretøy("Sportssykkel til 50 000");
   Console.WriteLine("Sykkelgarasje: " + sykkelGarasje);

   if (bilGarasjer > sykkelGarasje && bilGarasjer >= sykkelGarasje && sykkelGarasje < bilGarasjer && sykkelGarasje <= bilGarasjer)
      Console.WriteLine("Sykkelgarasjen har færre sykler enn bilgarasjene har biler.");

   if (bilGarasje1 != bilGarasje2)
      Console.WriteLine("Bilgarasje 1 er ikke lik bilgarasje 2.");

   Console.ReadLine();
}

.. som gir:

Generisk vs. typer

For nybegynnere er det fort gjort å tenke at generiske klasser, funksjoner, o.s.v., gir unødvendig kompleksitet og at man like greit kan kjøre på med vanlige verdityper og objekter i stedet for.

Noen ganger er dette helt greit, det kommer an på om det blir mye ekstra arbeid eller ikke både nå og senere.

Funksjoner

Om man tar utgangspunkt i en enkel byttefunksjon:

public static void Bytt(ref int a, ref int b)
{
   int midlertidig = a;
   a = b;
   b = midlertidig;
}

Dette er en byttefunksjon for int, men hva om man skal bytte to stk. double? Da trenger man samme funksjon for dette i tillegg:

public static void Bytt(ref double a, ref double b)
{
   double midlertidig = a;
   a = b;
   b = midlertidig;
}

Ah, endelig ferdig. Nå kan man bytte både int og double.

Men hva om man trenger å bytte int med double, double med long, long med string, o.s.v.? Det blir mye arbeid å skrive byttefunksjoner for alle tenkelige kombinasjoner, i tillegg blir det repetivt (altså kjedelig) og det øker vedlikeholdsarbeidet senere.

En mulig løsning er å gjøre alt til objekter, for så å bytte på dem:

public static void Bytt(ref object a, ref object b)
{
   object midlertidig = a;
   a = b;
   b = midlertidig;
}

Her får man faktisk byttet alt mulig rart, problemet er at man må pakke inn hvert argument i et objekt først – noe som gir økt ressursbruk.

Enhver utvikler vil derfor være enig i at en generisk byttefunksjon er best:

public static void Bytt<T>(ref T a, ref T b)
{
   T midlertidig = a;
   a = b;
   b = midlertidig;
}

Denne vil håndtere både verdityper (altså int, double, DateTime, o.s.v.) samt objekter.

For å sjekke at alt fungerer trenger man først noen variabler med forskjellige typer data som kan byttes om på:

static int tall1 = 666;
static DateTime dato1 = new DateTime(2017, 6, 10);
static string tekst1 = "blah";

static int tall2 = 66;
static DateTime dato2 = new DateTime(1987, 1, 1);
static string tekst2 = "hmm";

Også to funksjoner som skal utføre samme arbeidet, men på to forskjellige måter:

public static void BrukByttTyper()
{
   Bytt(ref tall1, ref tall2);
   Bytt(ref dato1, ref dato2);
   Bytt(ref tekst1, ref tekst2);
}

public static void BrukByttObjekter()
{
   // Bytt tall
   object o1 = (object)tall1;
   object o2 = (object)tall2;
   Bytt(ref o1, ref o2);
   tall1 = (int)o1;
   tall2 = (int)o2;

   // Bytt dato
   o1 = (object)dato1;
   o2 = (object)dato2;
   Bytt(ref o1, ref o2);
   dato1 = (DateTime)o1;
   dato2 = (DateTime)o2;

   // Bytt tekst (som allerede er objekt)
   Bytt(ref tekst1, ref tekst2);
}

I siste funksjonen gjøres byttingen i form av objekter. Dette krever at hver variabel som er en verditype «boxes» og blir til et objekt. Etterpå «unboxes» de og blir til verditype igjen. (Unntaket er string som allerede er et objekt.)

Nå gjenstår det bare å ta i bruk dette fornuftig når man kjører sjekker:

static void Main(string[] args)
{
   Console.WriteLine("Test av byttefunksjonene først .. \n");

   Console.WriteLine("Utgangspunkt: ");
   Console.Write(tall1 + "\n" + dato1 + "\n" + tekst1 + "\n" + tall2 + "\n" + dato2 + "\n" + tekst2 + "\n\n");

   BrukByttTyper();
   Console.WriteLine("Etter bytting med ByttTyper():");
   Console.Write(tall1 + "\n" + dato1 + "\n" + tekst1 + "\n" + tall2 + "\n" + dato2 + "\n" + tekst2 + "\n\n");

   BrukByttObjekter();
   Console.WriteLine("Etter bytting med ByttObjekter():");
   Console.Write(tall1 + "\n" + dato1 + "\n" + tekst1 + "\n" + tall2 + "\n" + dato2 + "\n" + tekst2 + "\n\n");

   /////////////////////////////////////////////////////////////////////////

   long bytteoperasjoner = 1000000000; // 1 milliard

   Console.WriteLine("Tidtaking av " + 3*bytteoperasjoner + " bytteoperasjoner .. \n");

   Stopwatch klokke1 = new Stopwatch();
   klokke1.Start();
   for (int i = 0; i < bytteoperasjoner; i++)
   {
      BrukByttTyper();
   }
   klokke1.Stop();

   Stopwatch klokke2 = new Stopwatch();
   klokke2.Start();
   for (int i = 0; i < bytteoperasjoner; i++) {
      BrukByttObjekter();
   }
   klokke2.Stop();

   Console.WriteLine("Tid brukt: BrukTyper() ==> " + klokke1.ElapsedMilliseconds + " ms. BrukObjekter() ==> " + klokke2.ElapsedMilliseconds + " ms.");

   /////////////////////////////////////////////////////////////////////////

   Console.ReadLine();
}

På slutten tas tiden for å undersøke hvor mye «boxing» og «unboxing» har å si.

Dette gir følgende:

På min stasjonære PC med Intel Core 2 Quad Q6600 2.40 GHz hvor bare èn kjerne ble brukt gikk det nesten dobbelt så lang tid med «boxing» og «unboxing». Skal man utvikle på en mobil plattform eller f.eks. lage et spill kan det være greit å vite om at «boxing» og «unboxing» krever ekstra ressurser.

Generiske er derfor å foretrekke – og av flere grunner.

Klasser

Det er ikke bare generiske funksjoner/metoder som gjør livet lettere. Også generiske klasser kan være veldig kjekke å ha. Skal man f.eks. lage ei klasse som kan holde på en koordinat blir det upraktisk om den bare støtter èn type, og samtidig blir det litt for dumt å måtte lage flere like.

Derfor kan man i stedet gjøre den generisk:

public class Koordinat<T1, T2> where T1 : struct, IComparable<T1> where T2 : struct, IComparable<T2>
{
   public T1 X { get { return x; } }
   private T1 x;

   public T2 Y { get { return y; } }
   private T2 y;

   public Koordinat(T1 x, T2 y)
   {
      this.x = x;
      this.y = y;
   }

   override public string ToString()
   {
      return "(" + x + "," + y + ")";
   }
}

Her er where T1 : struct, IComparable<T1> where T2 : struct, IComparable<T2> lagt til for å begrense aksepterte typer så den kun kan ta i mot tall.

For å teste klassen kan man kjøre følgende:

static void Main(string[] args)
{
   ArrayList koordinater = new ArrayList()
   {
      new Koordinat<int, double>(1, 2.0000002),
      new Koordinat<double, double>(3.00003, 4.000004),
      new Koordinat<int, int>(5, 6),
      new Koordinat<float, float>(7.000007f, 8.0008f),
      new Koordinat<decimal, decimal>(new decimal(9.009), new decimal(10.0000010))
   };

   foreach (var koordinat in koordinater)
   {
      Console.WriteLine(koordinat);
   }

   Console.ReadLine();
}

.. som gir:

IComparer

Noen gang lurt på hvordan man ordner støtte for å sortere objekter på flere forskjellige måter. F.eks. slik mange nettbutikker gjør, hvor de tilbyr deg sortering på pris, mest aktuelt, popularitet, o.s.v.?

Dette er enkelt å ordne f.eks. med grensesnittet IComparer. Men først trenger man en eksempelklasse som senere skal gi de objektene man ønsker å sortere:

public class Vare
{
   public string Navn { get { return navn; } }
   private string navn;

   public string Produsent { get { return produsent; } }
   private string produsent;

   public int Pris { get { return pris; } }
   private int pris;

   public DateTime Lanseringsdato { get { return lanseringsdato; } }
   private DateTime lanseringsdato;

   public Vare(string navn, string produsent, int pris, DateTime lanseringsdato)
   {
      this.navn = navn;
      this.produsent = produsent;
      this.pris = pris;
      this.lanseringsdato = lanseringsdato;
   }

   public override string ToString()
   {
      string info = "";
      info += "----------------------------------------------------------------------\n";
      info += "Navn: " + navn + "\n";
      info += "Produsent: " + produsent + "\n";
      if (lanseringsdato != DateTime.MinValue)
      {
         info += "Lansert en gang etter: " + lanseringsdato.Day + "." + lanseringsdato.Month + "." + lanseringsdato.Year + "\n";
      }
      info += "Selges for " + pris + " NOK\n";
      info += "----------------------------------------------------------------------";
      return info;
   }

   public static IComparer SorterBrukPriser { get { return new VareSortering.Prisen(); } }
   public static IComparer SorterBrukProdusent { get { return new VareSortering.Produsent(); } }
   public static IComparer SorterBrukLanseringsdato { get { return new VareSortering.Lanseringsdato(); } }
}

Her er det egentlig ikke så mye som er interessant, bortsett fra de tre siste linjene som man refererer til når man vil bruke de forskjellige sorteringsmåtene.

Disse implementeres i det som blir en hjelpeklasse til klassen Vare over:

public class VareSortering
{
   public class Prisen : IComparer
   {
      public int Compare(object objekt1, object objekt2)
      {
         Vare vare1 = objekt1 as Vare;
         Vare vare2 = objekt2 as Vare;

         if (vare1 != null && vare2 != null)
         {
            if (vare1.Pris < vare2.Pris)
            {
               return -1;
            }
            else if (vare1.Pris > vare2.Pris)
            {
               return 1;
            }
            else
            {
               return 0;
            }
         }
         else
         {
            throw new ArgumentException("Begge objekt som skal sammenlignes må være varer!");
         }
      }
   }

   public class Produsent : IComparer
   {
      public int Compare(object objekt1, object objekt2)
      {
         Vare vare1 = objekt1 as Vare;
         Vare vare2 = objekt2 as Vare;

         if (vare1 != null && vare2 != null)
         {
            if (vare1.Produsent.CompareTo(vare2.Produsent) < 0)
            {
               return -1;
            }
            else if (vare1.Produsent.CompareTo(vare2.Produsent) > 0)
            {
               return 1;
            }
            else
            {
               return 0;
            }
         }
         else
         {
            throw new ArgumentException("Begge objekt som skal sammenlignes må være varer!");
         }
      }
   }

   public class Lanseringsdato : IComparer
   {
      public int Compare(object objekt1, object objekt2)
      {
         Vare vare1 = objekt1 as Vare;
         Vare vare2 = objekt2 as Vare;

         if (vare1 != null && vare2 != null)
         {
            if (vare1.Lanseringsdato.Year < vare2.Lanseringsdato.Year)
            {
               return -1;
            }
            else if (vare1.Lanseringsdato.Year > vare2.Lanseringsdato.Year)
            {
               return 1;
            }
            else
            {
               if (vare1.Lanseringsdato.Month < vare2.Lanseringsdato.Month)
               {
                  return -1;
               }
               else if (vare1.Lanseringsdato.Month > vare2.Lanseringsdato.Month)
               {
                  return 1;
               }
               else
               {
                  if (vare1.Lanseringsdato.Day < vare2.Lanseringsdato.Day)
                  {
                     return -1;
                  }
                  else if (vare1.Lanseringsdato.Day > vare2.Lanseringsdato.Day)
                  {
                     return 1;
                  }
                  else
                  {
                     return 0;
                  }
               }
            }
         }
         else
         {
            throw new ArgumentException("Begge objekt som skal sammenlignes må være varer!");
         }
      }
   }
}

Her er VareSortering gjort til en egen klasse utenfor Vare, men den kunne også vært opprettet inni og gjort privat da omverdenen ikke trenger direkte tilgang.

Og endelig for å teste om dette henger sammen og fungerer kan man kjøre følgende:

class Program
{
   static void Main(string[] args)
   {
      Vare[] varer = new Vare[5];
      varer[0] = new Vare("Intel Pentium G4400 3.3GHz Socket 1151 Box", "Intel", 533, new DateTime(2015, 8, 1));
      varer[1] = new Vare("Corsair Vengeance LPX Black DDR4 PC19200/2400MHz CL14", "Corsair", 399, new DateTime(2015, 1, 1));
      varer[2] = new Vare("ASUS Prime B250M-C", "ASUS", 858, new DateTime(2015, 1, 1));
      varer[3] = new Vare("Fractal Design Core 1100 (Sort)", "Fractal Design", 379, DateTime.MinValue);
      varer[4] = new Vare("Corsair VS450 V2 450W", "Corsair", 419, new DateTime(2015, 1, 1));

      // Før sortering
      Console.WriteLine("Før sortering:");
      foreach (Vare vare in varer) Console.WriteLine(vare);
      Console.WriteLine();

      // Etter prissortering
      Console.WriteLine("Etter prissortering:");
      Array.Sort(varer, Vare.SorterBrukPriser);
      foreach (Vare vare in varer) Console.WriteLine(vare);
      Console.WriteLine();

      // Etter produsentsortering
      Console.WriteLine("Etter produsentsortering:");
      Array.Sort(varer, Vare.SorterBrukProdusent);
      foreach (Vare vare in varer) Console.WriteLine(vare);
      Console.WriteLine();

      // Etter lanseringsdato
      Console.WriteLine("Etter lanseringsdatosortering:");
      Array.Sort(varer, Vare.SorterBrukLanseringsdato);
      foreach (Vare vare in varer) Console.WriteLine(vare);
      Console.WriteLine();

      Console.ReadLine();
   }
}

.. som da resulterer i:

Altså virker alle de tre forskjellige sorteringsmåtene, og har man flere man ønsker å legge inn er dette ikke noe problem.

IEnumerator og IEnumerable

To kjekke grensesnitt som alle C#-folk bør kunne er IEnumerator og IEnumerable.

Med disse kan man enkelt "loope" over innholdet i egne klasser man lager.

Eksempel på klasse hvor disse grensesnittene er støttet:

public class Garasje : IEnumerable
{
   private string[] kjøretøy;

   public Garasje(int plasser)
   {
      if (plasser < 1) throw new ArgumentException("Garasjen må ha minst 1 plass!");
      this.kjøretøy = new string[plasser];
   }

   public void parker(string kjøretøy)
   {
      for (int k = 0; k < kjøretøy.Length; k++)
      {
         if (this.kjøretøy[k] == null)
         {
            this.kjøretøy[k] = kjøretøy;
            kjøretøy = null;
            break;
         }
      }  

      if (kjøretøy != null)
      {
         throw new ArgumentOutOfRangeException("Ikke nok plass i garasjen!");
      }
   }

   public void kjørUt(string kjøretøy)
   {
      for (int k = 0; k < kjøretøy.Length; k++)
      {
         if (this.kjøretøy[k] == kjøretøy)
         {
            this.kjøretøy[k] = null;
            kjøretøy = null;
            break;
         }
      }

      if (kjøretøy != null)
      {
         throw new ArgumentOutOfRangeException("Kjøretøyet er ikke parkert i garasjen!");
      }
   }

   public IEnumerator GetEnumerator()
   {
      foreach (string k in kjøretøy)
      {
         yield return k;
      }
   }

   public IEnumerable ListKjøretøySortert()
   {
      string[] sorterteKjøretøy = new string[kjøretøy.Length];
      Array.Copy(kjøretøy, sorterteKjøretøy, kjøretøy.Length);
      Array.Sort(sorterteKjøretøy);

      foreach (string k in sorterteKjøretøy)
      {
         yield return k;
      }
   }
}

Her er det først funksjonalitet for å opprette garasjeplass, og så for å legge til og fjerne kjøretøy igjen. Enkelt, ukomplisert, kjedelig, og ikke poenget.

Poenget er de to siste metodene som utgjør den ønskede funksjonaliteten for å enkelt kunne "loope" over innholdet som her er kjøretøyene i garasjen.

For å teste at alt henger sammen:

class Program
{
   static void Main(string[] args)
   {
      // Opprett garasje og parker biler
      Garasje garasje = new Garasje(2);
      garasje.parker("Toyota Corolla");
      garasje.parker("Suzuki GSX F");

      // Skriv ut liste over parkerte kjøretøy
      Console.WriteLine("\nKjøretøy parkert i garasjen:");
      foreach (string kjøretøy in garasje) Console.WriteLine(kjøretøy);

      // Skriv ut liste over parkerte kjøretøy sortert
      Console.WriteLine("\nKjøretøy parkert i garasjen, sortert:");
      foreach (string kjøretøy in garasje.ListKjøretøySortert()) Console.WriteLine(kjøretøy);

      // Kjører ut motorsykkelen
      Console.WriteLine("\nKjører ut motorsykkelen .. ");
      garasje.kjørUt("Suzuki GSX F");

      // Skriv ut liste over parkerte kjøretøy
      Console.WriteLine("\nKjøretøy parkert i garasjen:");
      foreach (string kjøretøy in garasje) Console.WriteLine(kjøretøy);

      Console.ReadLine();
   }
}

.. dette gir:

Grensesnitt og klassearv

Etter man er kommet i gang med bruk av klasser i hvilket som helst objektorientert språk, er det helt naturlig å fortsette med grensesnitt.

Klasser brukes når man skal lage noe (altså et objekt), mens grensesnitt er passende når det er snakk om å holde på med noe (altså en aktivitet).

Eksempel på grensesnitt for biler, motorsykler, o.s.v.:

public interface IKjørbar
{
   void gass();
   void brems();
   void sving();
}

Dette er et enkelt grensesnitt for alt som kan kjøres. Gassing, bremsing, og svinging er da opplagt funksjonalitet man trenger.

Grensesnitt har mye til felles med abstrakte klasser:

public abstract class Kjøretøy
{
}

Men bruker man abstrakte klasser i stedet for grensesnitt kan man fort få problemer. Et problem er at man må arve fra klassen for å bruke den og i C# kan man kun arve fra èn klasse.

Derfor er det bedre å bruke den abstrakte klassen til noe annet:

public abstract class Kjøretøy : IKjørbar, IComparable, ICloneable
{
   public int Årsmodell
   {
      get
      {
         return årsmodell;
      }
   }
   private int årsmodell;

   public string Kubikk
   {
      get
      {
         return kubikk;
      }
   }
   private string kubikk;

   public string Navn
   {
      get
      {
         return navn;
      }
   }
   private string navn;

   public int AntallHjul
   {
      get
      {
         return antallHjul;
      }
   }
   private int antallHjul;

   public string Farge
   {
      get
      {
         return farge;
      }
   }
   private string farge;

   public int Hestekrefter
   {
      get
      {
         return hestekrefter;
      }
   }
   private int hestekrefter;

   public Kjøretøy(int årsmodell, string kubikk, string navn, int antallHjul, string farge, int hestekrefter)
   {
      this.årsmodell = årsmodell;
      this.kubikk = kubikk;
      this.navn = navn;
      this.antallHjul = antallHjul;
      this.farge = farge;
      this.hestekrefter = hestekrefter;
   }

   public int CompareTo(object objekt)
   {
      Kjøretøy kjøretøy = (Kjøretøy) objekt;

      if (årsmodell < kjøretøy.Årsmodell)
      {
         return -1;
      }
      else if (årsmodell == kjøretøy.Årsmodell)
      {
         if (navn.CompareTo(kjøretøy.Navn) < 0)
         {
            return -1;
         }
         else if (navn.CompareTo(kjøretøy.Navn) == 0)
         {
            if (antallHjul < kjøretøy.AntallHjul)
            {
               return -1;
            }
            else if (antallHjul == kjøretøy.AntallHjul)
            {
               if (hestekrefter < kjøretøy.Hestekrefter)
               {
                  return -1;
               }
               else if (hestekrefter == kjøretøy.Hestekrefter)
               {
                  return 0;
               }
               else
               {
                  return 1;
               }
            }
            else
            {
               return 1;
            }
         }
         else
         {
            return 1;
         }
      }
      else
      {
         return 1;
      }
   }

   public abstract object Clone();
   public abstract void gass();
   public abstract void brems();
   public abstract void sving();
}

Her er mesteparten av det som er relevant for alle kjøretøy tatt med.

Og siden IComparable og ICloneable er tatt med som grensesnitt som skal støttes av denne klassen er det nå lett å sammenligne forskjellige kjøretøy samt ta kopier.

For å få bruk for det som er i denne klassen må man arve fra den. To eksempler på mer spesialiserte klasser:

public class Bil : Kjøretøy
{
   public DateTime FristEUKontroll
   {
      get
      {
         return fristEUKontroll;
      }
   }
   private DateTime fristEUKontroll;

   public Bil(int årsmodell, string kubikk, string navn, int antallHjul, string farge, int hestekrefter, DateTime fristEUKontroll) : base(årsmodell, kubikk, navn, antallHjul, farge, hestekrefter)
   {
      this.fristEUKontroll = fristEUKontroll;
   }

   override public object Clone()
   {
      return new Bil(Årsmodell, Kubikk, Navn, AntallHjul, Farge, Hestekrefter, fristEUKontroll);
   }

   override public string ToString()
   {
      return
         Årsmodell + " " + Navn + ", " +
         Kubikk + ", " +
         AntallHjul + " hjul, " +
         Farge + ", " +
         Hestekrefter + " HK, " +
         "EU-OK til og med " + fristEUKontroll.Day + "." + fristEUKontroll.Month + "." + fristEUKontroll.Year;
   }

   public override void gass()
   {
      Console.WriteLine("Bilen gasser.");
   }

   public override void brems()
   {
      Console.WriteLine("Bilen bremser.");
   }

   public override void sving()
   {
      Console.WriteLine("Bilen svinger.");
   }
}
public class Motorsykkel : Kjøretøy
{
   bool Kjede
   {
      get
      {
         return kjede;
      }
   }
   private bool kjede;

   public Motorsykkel(int årsmodell, string kubikk, string navn, int antallHjul, string farge, int hestekrefter, bool kjede) : base(årsmodell, kubikk, navn, antallHjul, farge, hestekrefter)
   {
      this.kjede = kjede;
   }

   override public object Clone()
   {
      return new Motorsykkel(Årsmodell, Kubikk, Navn, AntallHjul, Farge, Hestekrefter, kjede);
   }

   override public string ToString()
   {
      return
         Årsmodell + " " + Navn + ", " +
         Kubikk + ", " +
         AntallHjul + " hjul, " +
         Farge + ", " +
         Hestekrefter + " HK, " +
         (kjede ? "har kjede" : "har ikke kjede");
   }

   public override void gass()
   {
      Console.WriteLine("Motorsykkelen gasser.");
   }

   public override void brems()
   {
      Console.WriteLine("Motorsykkelen bremser.");
   }

   public override void sving()
   {
      Console.WriteLine("Motorsyklisten lener seg til en side.");
   }
}

Det er selvsagt alltid rom for forbedringer. Kanskje vil man spesialisere klassen for motorsykler enda mer i tilfelle man skal registrere sykler som har sidevogn.

For å teste grensesnittet samt klassene kan man kjøre følgende:

class Program
{
   private static List<IKjørbar> mineKjøretøy;
 
   static void Main(string[] args)
   {
      // Opprett kjøretøy
      mineKjøretøy = new List<IKjørbar>();
      mineKjøretøy.Add(new Motorsykkel(1995, "600 F", "Suzuki GSX", 2, "svart/oransje", 86, true));
      mineKjøretøy.Add(new Bil(1993, "1.3 XLi", "Toyota Corolla", 4, "hvit", 86, new DateTime(2017, 10, 31)) );

      // Sjekk at sortering virker
      listKjøretøy("Før sortering:");
      mineKjøretøy.Sort();
      listKjøretøy("Etter sortering:");

      // Sjekk at grensesnittmetoder virker
      testKjøretøy("Tester kjøretøyene:");

      Console.ReadLine();
   }

   private static void listKjøretøy(string melding)
   {
      Console.WriteLine(melding);

      for (int k = 0; k < mineKjøretøy.Count; k++)
      {
         Console.WriteLine(mineKjøretøy[k]);
      }

      Console.WriteLine();
   }

   private static void testKjøretøy(string melding)
   {
      Console.WriteLine(melding);

      for (int k = 0; k < mineKjøretøy.Count; k++)
      {
         mineKjøretøy[k].gass();
         mineKjøretøy[k].sving();
         mineKjøretøy[k].brems();
      }

      Console.WriteLine();
   }
}

.. som da gir:

Alt ser ut til å være i orden. Sorteringen virker, og metodene fra grensesnittet gir tilpasset innhold avhengig av om det er bil eller motorsykkel.

TBC