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:

Legg igjen en kommentar

Din e-postadresse vil ikke bli publisert.