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)

Funksjoner

Det finnes flere måter å skrive funksjoner/metoder på i C#.


Det mest vanlige og ukompliserte eksemplet:

Console.WriteLine("pluss(1,1) = {0}", pluss(1,1));
# skriver ut: 'pluss(1,1) = 2'
 static int pluss(int tall1, int tall2)
{
   return tall1 + tall2;
}

Verdier for tall1 og tall2 mates her inn, også returneres svaret rett etterpå.


Eksempel hvor man opererer uten returverdi:

int svar;
minus(1, 1, out svar);
Console.WriteLine("minus(1,1) = {0}", svar);
# skriver ut: 'minus(1,1) = 0'
static void minus(int tall1, int tall2, out int svar)
{
   svar = tall1 - tall2;
}

Her er funksjonen endret så den kan returnere en verdi slik den får verdier inn.


Enda et eksempel hvor man opererer uten returverdi:

int svar = 0;
gange(2, 2, ref svar);
Console.WriteLine("gange(2,2) = {0}", svar);
# skriver ut: 'gange(2,2) = 4'
static void gange(int tall1, int tall2, ref int svar)
{
   svar = tall1 * tall2;
}

Her gis funksjonen en referanse til variabelen så innholdet kan settes direkte.


Eksempel hvor man trenger å mate inn en ukjent mengde data:

Console.WriteLine("sum(1,2,3,4,5,6,7,8,9) = {0}", sum(1,2,3,4,5,6,7,8,9));
# skriver ut: 'sum(1,2,3,4,5,6,7,8,9) = 45'
static int sum(params int[] tall) // trenger ikke nødvendigvis være int
{
   int summen = 0;
   foreach (int t in tall)
   {
      summen += t;
   }
   return summen;
}

Her er innmatede verdier summert og returnert.


Og så et eksempel på frivillige og navngitte argumenter:

Console.WriteLine("logaritmen(100) = {0}", logaritmen(100));
# skriver ut: 'logaritmen(100) = 2'
Console.WriteLine("logaritmen(basen:2, nummer:512) = {0}", logaritmen(basen:2, nummer:512));
# skriver ut: 'logaritmen(basen:2, nummer:512) = 9'
static double logaritmen(double nummer, double basen = 10)
{
   return Math.Log10(nummer) / Math.Log10(basen);
}

Her er det to kall til funksjonen logaritmen hvor førstnevnte ikke inkluderer noe nummer for base, og dermed brukes 10 som standard.

Sistnevnte derimot navngir både basen og nummer, som så gis verdier.

Som i andre programmeringsspråk kan man også gå for overstyring, her er praksisen den samme som f.eks. i Java.

&& og || vs. & og |

I et logisk uttrykk med && og || sjekkes ikke hele uttrykket hvis det ikke er nødvendig:

int teller = 0;
if (true || ++teller > 0)
{
   Console.WriteLine("Uberørt teller: " + teller);
   # skriver ut: 'Uberørt teller: 0'
}

Her blir ikke ++teller > 0 kjørt siden true var nok til å fortsette videre.

Om man derimot bruker | blir resultatet annerledes:

int teller = 0;
if (true | ++teller > 0)
{
   Console.WriteLine("Berørt teller: " + teller);
   # skriver ut: "Berørt teller: 1"
}

Nå er teller forandret fordi hele det logiske uttrykket ble sjekket.

Implisitt type

Vil man spare seg for å stadig skrive int, char, string, o.s.v. kan man i C# benytte var:

var variabel1 = 1U; // usignert int
var variabel2 = 2; // int
var variabel3 = 3.0F; // float
var variabel4 = 4L; // long
var variabel5 = 5UL; // usignert long
var variabel6 = 6.0D; // double
var variabel7 = 7.0M; // decimal
var variabel8 = true; // bool
var variabel9 = 'c'; // char
var variabel10 = "tekst"; // string
var variabel11 = new DateTime(); // DateTime

Noe som kan være veldig kjekt, f.eks. om man skal benytte LINQ.

(OBS: Passer best for lokal bruk siden det oppstår diverse problemer ellers. Blant annet kan man ikke bruke var for å opprette klassevariabler.)

checked

Om man gir variabler for store eller for små verdier kan man få overflyt/underflyt.


static byte pluss(byte tall1, byte tall2)
{
   return (byte)(tall1+tall2);
}

Om funksjonen pluss her gis verdiene 150 og 150 blir resultatet 44 fordi en byte kun kan inneholde verdiene 0 til 255.

Det som skjer her er at man begynner på 150, teller opp til og med 255, så begynner man ved 0 igjen før man fortsetter til 44.

For å forhindre dette kan man pakke inn sårbar kode med checked:

static byte pluss(byte tall1, byte tall2)
{
   return checked( (byte)(tall1+tall2) );
}
static byte pluss(byte tall1, byte tall2)
{
   checked
   {
      return (byte)(tall1+tall2);
   }
}

Med verdier som gir overflyt/underflyt får man nå i stedet et unntak ved kjøring så man faktisk kan ta tak i problemet i stedet for at det forbigås i stillhet.


Et alternativ er å be kompilatoren om å skru på denne sjekkingen i stedet for, så man selv slipper å måtte pakke inn all tvilsom kode i checked.

I Visual Studio gjøres dette ved å først finne egenskapene for prosjektet:

Så velger man bygging og avansert:

Og til slutt huker man av for at overflyt/underflyt skal sjekkes:

Om du bruker en annen IDE/kompilator enn Visual Studio må du selv sjekke dokumentasjonen for å finne ut hvordan du slår på denne sjekkingen hos deg.

DateTime

Her kommer en innføring i DateTime for å håndtere tidspunkter.


Først opprettes et nytt objekt, med eller uten et utgangspunkt:

DateTime dt = new DateTime(DateTime.UtcNow.Ticks);
// Oppretter ny datetime med tidspunkt lik nå

Om man ønsker å skrive ut datoen, klokkeslettet, o.s.v.:

Console.WriteLine(dt);
Console.WriteLine(dt.ToString());
Console.WriteLine("{0:00}.{1:00}.{2:00} {3:00}:{4:00}:{5:00}", dt.Day, dt.Month, dt.Year, dt.Hour, dt.Minute, dt.Second);
// Alle tre skriver ut:
// 'dag.måned.år timer:minutter:sekunder' 
// men med en time feil
Console.WriteLine(dt.ToLocalTime());
// Skriver ut:
// 'dag.måned.år timer:minutter:sekunder' 
// med riktig antall timer!
Console.WriteLine(dt.ToLongDateString());
// Skriver ut:
// 'dato. månednavn år'
Console.WriteLine(dt.ToShortDateString());
// Skriver ut:
// 'dag.måned.år' 
Console.WriteLine(dt.ToLongTimeString());
// Skriver ut:
// 'timer:minutter:sekunder'
// men med en time feil
Console.WriteLine(dt.ToShortTimeString());
// Skriver ut:
// 'timer:minutter'
// men med en time feil
Console.WriteLine(dt.Millisecond);
// Skriver ut:
// 'millisekunder' 
Console.WriteLine(dt.DayOfWeek);
// Skriver ut:
// 'ukedag'
Console.WriteLine(dt.DayOfYear);
// Skriver hvilken dag i året det er, f.eks. 39 
Console.WriteLine(dt.IsDaylightSavingTime());
// Skriver ut:
// 'False' eller 'True'
// 'True' hvis det er sommertid 

For å legge til eller fjerne tid:

dt = dt.AddHours(1); // tilsvarende finnes for timer, sekunder, o.s.v.
Console.WriteLine(dt);
// Legger til en time og skriver ut:
// "dag.måned.år timer:minutter:sekunder"
TimeSpan ts = new TimeSpan(-1, 0, 0); // new TimeSpan(timer, minutter, sekunder)
dt = dt.Add(ts);
Console.WriteLine(dt);
// Fjerner en time og skriver ut:
// "dag.måned.år timer:minutter:sekunder"

Begge kan brukes om hverandre.


Og for å sammenligne:

Console.WriteLine(dt.Equals(new DateTime()));
// Sammenligner to DateTime-objekter
// og skriver ut: "False" 

Når kodesnuttene vist over kompileres og kjøres får vi følgende:


Det finnes selvsagt flere medlemmer og metoder, men ikke alle er av like stor interesse.

Tekst inn og ut fra terminal

Her kommer en innføring i hvordan man behandler tekst i terminalen.


Først og fremst skriver man tekst ut, og dette er omtrent som i andre programmeringsspråk:

string navn = "Ove Bakken";
Console.WriteLine("Hei, " + navn + "!");
Console.Write("Hei, " + navn + "!\n");
Console.WriteLine("Hei, {0}!", navn);
Console.Write("Hei, {0}!\n", navn);
// Gir alle det samme resultatet:
// 'Hei, Ove Bakken!'
// Hvor \n gir ny linje

Men noen ganger trenger man også å få tekst inn:

string passord = Console.ReadLine();
// Her kan man skrive hva man vil, for å avslutte trykker man på 'Enter'

string tegn = Console.Read();
// Leser kun enkeltbokstaver eller sammenhengende tekst
// Det vil si, den stopper der det er mellomrom, eller hvis 'Enter' trykkes

Console.Write("Passord: ");
string passord = Console.ReadLine();
// Skriver ut 'Passord: ' uten å starte ny linje,
// også kommer indikatoren hvor man begynner å skrive rett etter
// Med tekst fremfor vet bruker hva som forventes

Det er også vanlig å kunne oppgi informasjon ved oppstart.

For eksempel:

mittprogram.exe -auto -100
mittprogram -auto -100
mittprogram -styrke=100
o.s.v.

.. som så må tas tak i og behandles når programmet starter:

static void Main(string[] argumenter)
{
   foreach (string argument in argumenter)
   {
      // gjør et eller annet med argument
   }

   // eller:

   for (int s = 0; s < argumenter.Length; s++)
   {
      // gjør et eller annet med argumenter[s]
   }

   // Environment.GetCommandLineArgs() kan også brukes
   // i stedet for string[] argumenter man får fra Main
}

Man står fritt til å gjøre som man vil – det finnes ikke noe riktig og galt svar.


Et fullstendig eksempel, med innlesing av et argument gitt ved start, innlesing av tekst som bruker skriver inn, samt tekst som skrives ut igjen:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace DNBinnlogging
{
   class Program
   {
      // Viser DNB-logoen som ASCII-kunst
      private static void visLogo()
      {
         Console.WriteLine("                    '");
         Console.WriteLine("                    '");
         Console.WriteLine("                    '");
         Console.WriteLine("                    '");
         Console.WriteLine(" '''''''    ',      '    '''''';");
         Console.WriteLine(" ';    ''   ''      '    '     '");
         Console.WriteLine(" ';     ';  '.'     '    '      ''");
         Console.WriteLine(" ';     .'  '.:'    '    '     ';");
         Console.WriteLine(" ';      '  '. ';   '    '     :'");
         Console.WriteLine(" ';      '  '.  '.  '    '''''''");
         Console.WriteLine(" ';      '  '.   '  '    '     ''");
         Console.WriteLine(" ';      '  '.   .' '    '      '");
         Console.WriteLine(" ';     ''  '.    ; ''   '      '");
         Console.WriteLine(" ';    :'   '.     ''    '     '");
         Console.WriteLine(" '''''''    '.      '    ''''''':");
         Console.WriteLine("            '.");
         Console.WriteLine("            '.");
         Console.WriteLine("");
      }

      // Visker ut teksten på aktiv linje
      private static void viskUtLinje()
      {
         Console.SetCursorPosition(0, Console.CursorTop - 1);
         for (int i = 0; i < Console.BufferWidth; i++)
         {
            Console.Write(" ");
         }
         Console.SetCursorPosition(0, Console.CursorTop - 1);
      }

      static void Main(string[] argumenter)
      {
         // Variabler
         bool kuleFarger = false;

         // Behandler argumenter
         foreach (string argument in argumenter) {
            if (argument.Equals("-kulefarger")) {
               kuleFarger = true;
            }
         }

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

         // Klargjør skjerm
         if (kuleFarger) {
            Console.BackgroundColor = ConsoleColor.White;
            Console.ForegroundColor = ConsoleColor.DarkGreen;
         }
         Console.Clear();
         visLogo();
         Console.WriteLine("Velkommen til DNBs retro fjerntilgang. :)");
         Console.WriteLine("");

         // Inndata
         string personnummer = "";
         string passord = "";
         while (true)
         {
            // Innlogget med bruker
            if (personnummer.Equals("123456 12345") && passord.Equals("passord"))
            {
               // Utdata
               Console.WriteLine("Hei, Ove Bakken!");
               Console.WriteLine("Kontobalanse: {0:c}", new Random().Next(50, 50000));

               break;
            }

            // Ikke innlogget enda
            else
            {
               Console.Write("Personnummer: ");
               personnummer = Console.ReadLine();
               viskUtLinje();

               Console.Write("Passord: ");
               passord = Console.ReadLine();
               viskUtLinje();
            }
         }

         // Avslutt
         Console.WriteLine();
         Console.Write("Avslutt med 'Enter': ");
         Console.ReadLine();

         // Tøm skjerm og nullstill farger
         Console.Clear();
         if (kuleFarger)
         {
            // Skulle gjerne brukt Console.ResetColor(), men ser ikke ut til å virke
            Console.BackgroundColor = ConsoleColor.Black;
            Console.ForegroundColor = ConsoleColor.White;
         }
         Console.Clear();
      }
   }
}

Denne koden kan kopieres, limes inn, kompileres, og så kjøres av hvem som helst så lenge .NET / Mono og kompilator er installert og satt opp riktig.

Ved kjøring hvor argumentet for kule farger er gitt:

Først må man oppgi et gyldig personnummer:

Personnummeret gjemmes vekk før man så skriver passordet:

Når personnummer og passord er riktig vises til slutt kontoinformasjonen:

Til slutt avslutter man med å trykke en gang til på Enter.

Klassen Console har selvsagt flere metoder og egenskaper enn det som kommer frem i eksemplene over: https://msdn.microsoft.com/en-us/library/system.console(v=vs.110).aspx

Men alt er ikke like relevant eller av interesse.

På tide å lære litt .NET

Jeg har i det siste skumlest mye jobbannonser innenfor utvikling, og det ser ut som Java EE og eller .NET nesten alltid går igjen.

Siden jeg ikke har lært meg å bli venn med Java EE enda har jeg bestemt meg for å bli kjent med .NET i stedet for. Og etter sammenligning av anmeldelser på Amazon fant jeg ganske fort følgende:

Dette er C# 6.0 and the .NET 4.6 Framework som på Amazon med 162 anmeldelser ligger på 4.5/5 stjerner. Forhåpentligvis gjør denne susen for meg også, siden så mange andre anbefaler den.