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 vi 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 vi skal bytte to stk. double? Da trenger vi 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 vi bytte både int og double.

Men hva om vi 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 vi først noen variabler med forskjellige typer data som kan byttes om på samt funksjoner som tar seg av repetivt arbeid:

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";
public static void BrukByttTyper()
{
   Bytt(ref tall1, ref tall2);
   Bytt(ref dato1, ref dato2);
   Bytt(ref tekst1, ref tekst2);
}

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

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

   Bytt(ref tekst1, ref tekst2);
}

Her 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 igjen 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 vi 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();
}

Her på slutten tar vi 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 altså 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 vi 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 vi 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 vi en eksempelklasse som senere skal gi de objektene vi ø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 vi 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

Klasser

Som med alle andre objektorienterte språk har også C# klasser:

abstract class GeometriskObjekt
{
   public abstract double areal();
   public abstract double omkrets();

   public override string ToString()
   {
      return "Dette er et uspesifisert geometrisk objekt.";
   }
}

Her opprettes en abstrakt klasse som det dermed ikke kan lages instanser av, den legger kun føringer for hva underklasser må inneholde.

Disse underklassene blir mer spesialiserte, f.eks. kan vi lage èn klasse for firkant, èn for rektangel, og èn for sirkel:

class Firkant : GeometriskObjekt
{
   public int Lengde { get { return lengde; } }
   private int lengde;

   public Firkant(int lengde)
   {
      this.lengde = lengde;
   }

   public override double areal()
   {
      return lengde*lengde;
   }

   public override double omkrets()
   {
      return lengde * 4;
   }

   public override string ToString()
   {
      return String.Format("Firkant med sider={0:0.00}.", lengde);
   }
}
sealed class Rektangel : Firkant
{
   public int Bredde { get { return bredde; } }
   private int bredde;

   public Rektangel(int lengde, int bredde) : base(lengde)
   {
      this.bredde = bredde;
   }

   public override double areal()
   {
      return bredde * Lengde;
   }

   public override double omkrets()
   {
      return bredde * 2 + Lengde * 2;
   }

   public override string ToString()
   {
      return String.Format("Rektangel med lengde={0:0.00} og bredde={1:0.00}.", Lengde, bredde);
   }
}
sealed class Sirkel : GeometriskObjekt
{
   public int Radius { get { return radius; } }
   private int radius;

   public Sirkel(int radius)
   {
      this.radius = radius;
   }

   public override double areal()
   {
      return Math.PI * radius * radius;
   }

   public override double omkrets()
   {
      return 2 * Math.PI * radius;
   }

   public override string ToString()
   {
      return String.Format("Sirkel med radius={0:0.00}.", radius);
   }
}

Ingen av konstruktørene er særlig kompliserte. Firkant trenger kun en lengde, og Rektangel som arver fra Firkant trenger dermed bare bredde. Sirkel derimot må ha radius.

For konstruktøren i Rektangel må lengden videreformidles til konstruktøren i Firkant, ellers ville ikke denne blitt initialisert.

Når det gjelder metoder blir areal og omkrets overstyrt i hver underklasse for å gi de tilpasset innhold. Metoden ToString er også overstyrt, men dette er mer frivillig.

Og siden klassene Rektangel og Sirkel ikke trenger nye underklasser blir disse forseglet ved å benytte sealed.

Dermed gjenstår testingen for å se klassene i praksis:

static void Main(string[] args)
{
   object[] objekter = new object[5] {
      new Firkant(2),
      new Rektangel(2, 3),
      "tekst ...",
      "tekst ...",
      new Sirkel(1)
   };

   foreach (var o in objekter)
   {
      if (o is GeometriskObjekt)
      {
         GeometriskObjekt objekt = (GeometriskObjekt)o;

         Console.Write(objekt);
         Console.Write(" ");
         Console.Write(String.Format("Areal={0:0.00}.", objekt.areal()));
         Console.Write(" ");
         Console.Write(String.Format("Omkrets={0:0.00}.", objekt.omkrets()));
         Console.WriteLine();
      }
      else
      {
         Console.WriteLine("...");
      }
   }
   Console.ReadLine();
}

.. som gir:

For hvert objekt som gjenkjennes som GeometriskObjekt kjøres metodene ToString, areal, og omkrets. Men innholdet som gjengis er et resultat av dataene i hver individuelle instans.

struct

Inntil nylig var jeg ikke klar over at C# har struct slik som C/C++.

Av en eller annen grunn har ikke dette blitt nevnt i tidligere bøker jeg har brukt, eller så har jeg bare ikke fått det med meg.

Derfor er det på høy tid å endelig komme i gang:

struct Mattevektor
{
   public int X { get { return x; } }
   private int x;

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

   public Mattevektor(int x, int y)
   {
      this.x = x;
      this.y = y;
   }

   public static Mattevektor operator +(Mattevektor v1, Mattevektor v2)
   {
      return new Mattevektor(v1.x + v2.x, v1.y + v2.y);
   }

   public static Mattevektor operator -(Mattevektor v1, Mattevektor v2)
   {
      return new Mattevektor(v1.x - v2.x, v1.y - v2.y);
   }

   public static Mattevektor operator *(Mattevektor v1, int d)
   {
      return new Mattevektor(v1.x*d, v1.y*d);
   }

   public static bool operator ==(Mattevektor v1, Mattevektor v2)
   {
      return v1.x == v2.x && v1.y == v2.y;
   }

   public static bool operator !=(Mattevektor v1, Mattevektor v2)
   {
      return !(v1 == v2);
   }

   override public bool Equals(Object po)
   {
      Mattevektor p = (Mattevektor) po;
      return x == p.x && y == p.y;
   }

   public override int GetHashCode()
   {
      return int.Parse( x+""+y );
   }

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

Her har jeg lagd en struct som kan holde en matematisk vektor. Den har støtte for diverse matematiske operasjoner, samt forskjellig sammenligningsoperatorer.

En test for å se hvordan dette fungerer i praksis:

static void Main(string[] args)
{
   Mattevektor mattevektor1 = new Mattevektor(1, 1);
   Mattevektor mattevektor2 = new Mattevektor(2, 2);

   Console.WriteLine("mattevektor1.X = " + mattevektor1.X);
   Console.WriteLine("mattevektor1.Y = " + mattevektor1.Y);
   Console.WriteLine("mattevektor1 = " + mattevektor1);
   Console.WriteLine("mattevektor2 = " + mattevektor2);
   Console.WriteLine("mattevektor1 + mattevektor2 = " + (mattevektor1 + mattevektor2));
   Console.WriteLine("mattevektor2 - mattevektor1 = " + (mattevektor2 - mattevektor1));
   Console.WriteLine("mattevektor1 * 2 = " + (mattevektor1 * 2));
   Console.WriteLine("mattevektor1 == mattevektor2: " + (mattevektor1 == mattevektor2));
   Console.WriteLine("mattevektor1 != mattevektor2: " + (mattevektor1 != mattevektor2));

   Console.ReadLine();
}

.. hvor resultatet blir som følger:

Bruk av struct passer best når denne representerer kun èn logisk verdi. Når størrelsen på en instans av denne typen er mindre enn 16 bytes. Når innholdet ikke kan endres. Og når man ikke har som vane å ofte gjøre om verdien til å bli et objekt (kalt «boxing») som da lagres i det dynamiske minnet.

? og ??

Hva kan man gjøre om man ikke vet hvilken verdi man skal gi til en int, double, bool, o.s.v., altså en verditype? Verdityper kan jo ikke settes til null .. :

bool suksess = null;
// gir kompilatorfeil: "Cannot convert 'null' to bool because it is a non-nullable value type"

Løsningen kan være Nullable<T> (hvor T erstattes med int, double, bool, o.s.v.). Dette blir da som en innpakket verditype som også kan settes til null.

Ikke trenger man engang å skrive Nullable<T>, C# tilbyr ? som er et slags suffiks:

bool? test = null;
if (!test.HasValue)
{
   Console.WriteLine("en bool? satt til null,");
   test = true;
   if (test.Value) Console.WriteLine("og så true slik bool også kan gjøres");
}
// skriver ut:
// en bool? satt til null,
// og så true slik bool også kan gjøres
int? tall = null;
if (!tall.HasValue)
{
   Console.WriteLine("en int? satt til null,");
   tall = 666;
   Console.WriteLine("og så {0} slik int også kan gjøres", tall.Value);
}
// skriver ut:
// en int? satt til null,
// og så 666 slik int også kan gjøres

Her opprettes det først en nullable bool, så en nullable int. Begge brukes omtrent som om de skulle vært vanlige verdityper.

Man kan også med enkelhet angi en reserveverdi i tilfelle man støter på null:

int databaseVerdi1 = verdiFraDatabase() ?? 1337; // hvor verdiFraDatabase() her gir null
Console.WriteLine("Verdi fra database: " + databaseVerdi1);
// skriver ut: 'Verdi fra database: 1337' 

.. hvis ikke er man nødt til å bruke en nullable verditype i stedet for:

int? databaseVerdi2 = verdiFraDatabase(); // hvor verdiFraDatabase() her gir null
Console.WriteLine("Kanskje en verdi fra database: " + databaseVerdi2);
// skriver ut: 'Kanskje en verdi fra database: '
// (tomrom på slutten fordi databaseVerdi2 er null)