Introduktion til C#
Alle traditionelle programmeringssprog gemmer kildekode (source code på engelsk) i ganske almindelige tekstfiler – typisk med et sigende filtypenavn. C# filer gemmes som .cs-filer – og i et tegnsæt, som håndterer alle typer tegn og bogstaver. Du må således gerne bruge danske bogstaver, men typisk prøver man at holde sig til engelsk i kildekode. Her “kodes” der på dansk, men det er af pædagogiske årsager.
Information til undervisere
Det mest vigtige er at
- kursisten kender til den helt grundlæggende syntaks
- især hvad tuborgklammer betyder (scope)
- kompileren er ligeglad med whitespace mv
- der er forskel på store og små bogstaver
- kursisten ved hvad er namespace er
- de ved hvad typestærk og typesikker betyder
- de kender helt grundlæggende til programmeringsparadigmer
- jeg plejer at kode eksempler på de forskellige paradigmer men afhængig af niveau er det muligvis overkill
Selve syntaksen har nogle få, men vigtige regler, som du skal kende til.
Kod så meget som muligt
Sørg nu før at prøve at afvikle så meget kode som overhovedet muligt - også selvom du ikke forstår syntaksen endnu. Opret en simpel “Hello World” appliktion og sørg for at den kan afvikles (se guide her), slet den generede kode og erstat den med kode du kopierer herfra. Vær nysgerrig - og husk det er ok at fejle. Hvis det driller alt for meget så kopier koden ind i en AI og bed den fortælle dig hvad der er galt.
Tuborgklammer
C# er et såkaldt tuborgklammesprog - altså { og } - ligesom Java, C++, C og andre sprog. Hvis du har erfaring med lignende syntaks, er det nemt at komme i gang. Hvis ikke så bøvler du sikkert en del i starten med at finde tuborgklammerne på et dansk tastatur (AltGr+7 og AltGr+0). Her er et par eksempler:
int a = 10;
if(a == 10)
{
// blok af kode (virkefelt)
}else
{
// blok af kode (virkefelt)
}
{
// blok af kode (virkefelt)
}
Bemærk, at tuborgklammer bruges til at markere blokke af kode. Indrykning er kompiler ligeglad med, men det bruges normalt for at gøre koden så nem at læse som muligt.
Semikolon afslutter en instruktion
Kompileren er også ligeglad med linjeskift, mellemrum, tabuleringer og lignende. Det, som afslutter en instruktion, er semikolon. Således er alle følgende kodelinjer lovlige:
De fleste vil jo skrive koden som den første linje, men de efterfølgende er helt lovlige. Følgende er også fuldstændig lovlig kode:bool d = true;
if (d == true) { Console.WriteLine(d); }
if (d == true) {
Console.WriteLine(d);
}
if (d == true)
{
Console.WriteLine(d);
}
Bemærk de forskellige måder at sætte tuborgklammerne.
I virkeligheden koder man typisk efter en standard, så det ser pænt ud, især hvis man er flere om at skrive koden – men det er helt op til dig som udvikler. Kompileren er ligeglad, så længe du har styr på dine semikoloner og tuborgklammer.
Både Visual Studio og Visual Studio Code har en mulighed for at formatere kode efter indbyggede regler . Disse regler kan du ændre hvis du ønsker det.
Kommentarer
Kommentarer i koden kan være ret brugbart som dokumentation, og i C# kan man enten skrive en kommentar på en enkelt linje som:
Eller som et afsnit: Jo flere kommentarer, man skriver i koden, jo nemmere bliver den at forstå og vedligeholde på et senere tidspunkt.Store og små bogstaver
Ligesom i de fleste (men ikke alle) moderne programmeringssprog er der i C# forskel på store og små bogstaver. Det gælder både ved definering af typer samt erklæring af variabler med videre. Således er dette eksempelvis forskellige typer, variabler og metoder:
// der er forskel på a og A
int a = 10;
int A = 20;
// der er forskel på metode og Metode
void metode() { }
void Metode() { }
// Der er forskel på a og A
class a { }
class A { }
Linjenumre
Har du erfaring fra ældre programmeringssprog kender du sikkert brugen af linjenummerering som en del af syntaksen, men det bruger man ikke i moderne programmeringssprog, herunder C#, fordi man sjældent benytter jump-kommandoer (goto).
Dermed ikke sagt, at en kompiler og udviklingsmiljø ikke refererer til et linjenummer, men det er ikke noget, du skriver som en del af syntaksen.
Programmeringsparadigmer
På de højere uddannelser inden for IT-udvikling lærer de studerende forskellige programmeringssprog – typisk 2-3 stykker. Det handler ikke om, at de skal have et dybt kendskab til, hvordan man skriver kode i konkrete sprog, men mere at de får en forståelse for, at der fundamentalt, og på et overordnet niveau, kan være forskel på, hvordan man skriver kode. Det kaldes også med et fancy ord programmeringsparadigmer, som vel kan oversættes til måden at skrive kode på.
Det imperative programmeringsparadigme er den gamle måde at skrive kode på - afvikling af én instruktion ad gangen og en masse hop fra den ene blok af instruktioner til den anden. Det skaber en noget uoverskuelig kode, som er svær at vedligeholde, men kæmpestore kodebaser er skabt med imperativ kode.
Iterativ
I iterativ kode finder du grundstammen i alle programmeringssprog:
-
Brug af variabler for at gøre det nemt at refererer til værdier i hukommelsen som bruges medens applikationen afvikles
-
Brug af betingelser baseret på sand/falsk udtryk til at styre hvilken kode der skal afvikles
-
Brug af løkker til at kunne gentage kode
-
Brug af fejlhåndtering til at sikre at kode ikke fejler katastrofalt men i en kontrolleret form
Procedural
Procedural kode er videreudviklingen af imperativ kode, hvor blandt andet funktioner (metoder) erstatter de klassiske goto-instruktioner med funktionskald. Det gør kode meget genbrugelig.
Du kan godt vælge udelukkende at skabe iterativ og procedural kode i C#. Der er skrevet store kodebaser ved hjælp af de paradigmer - se bare Nasa/Apollo.
Objektorienteret
Objektorienteret kode er en helt anden måde at skrive kode på. I stedet for at hoppe rundt i koden kan du i den objektorienterede programmering skabe skabeloner for typer, som definerer elementer (kaldes også entiteter) i koden. Disse skabeloner kan så benyttes til at skabe objekter, der repræsenterer de konkrete elementer.
Skal du eksempelvis kode et Yatzy-spil, ville det være oplagt med typer som en terning, et terningebæger, en spilleplade, en spiller og så videre. Herefter kan du sammensætte de enkelte typer (et terningebæger består eksempelvis af terninger) og skabe objekter af disse. På den måde kan koden både skrives og læses noget nemmere end traditionel procedural kode.
I objektorienteret programmering har man ligeledes mulighed for at skabe genbrug af kode ved hjælp arv, og simplificere og optimere afvikling ved hjælp af polymorfi. Meget mere om dette senere i bogen.
Objektorienteret programmering har rod i sprog som (norske) Simula og Smalltalk, og C++ (skabt af danske Bjarne Stroustrup) er stadig højt på listen over populære programmeringssprog.
Funktionsorienteret
Funktionsorienteret programmering giver mulighed for at afkoble kode ved at arbejde med referencer til funktioner samt har en noget mere stram styring af data i hukommelsen. Det er helt anderledes end både procedural og objektorienteret programmering. Det har rod i Lambdakalkyle (Lambda calculus) fra 1930’erne (opfundet af Alonzo Church, som i øvrigt var PhD supervisor for Alan Turing - begge bør du læse mere om, hvis du ikke kender dem) og er kendt fra sprog som Lisp, Scheme og F# (del af .NET).
C# er et objektorienteret sprog, hvor alt er baseret på typer, mens logik og flow er imperativt og proceduralt. Der er samtidigt en del features relateret til funktionsorienteret kode, og sproget favner dermed meget bredt med grene ud i alle typer af programmeringsparadigmer.
Men det er op til dig at vælge, hvordan du vil skrive kode. I bogen vil du lære alle typer af paradigmer.
Alt er typer i C#
C# er altså baseret på brugen af typer, og du vil falde over det begreb hele tiden.
Der findes helt overordnet fem forskellige typer:
-
Klasser (class)
-
Strukturer (struct)
-
Relaterede konstanter (enumerations)
-
Interfaces (interface)
-
Referencer til metoder (delegate).
Du skal se en type som en skabelon for, hvordan instanser (kaldes også objekter) vil se ud.
Microsoft har eksempelvis defineret en masse strukturer, vi kan bruge til at skabe simple variabler. En af de meget benyttede er strukturen System.Int32 (kaldes også en int), som er Microsofts bud på, hvordan et heltal skal fungere. I strukturen er der defineret, hvilke værdier instanser af et heltal består af, og hvad de fylder i hukommelsen. I System.Int32 bliver der udelukkende opbevaret et 32-bit tal, men i andre typer kan der gemmes mange forskellige værdier. Udover værdier kan der defineres forskellige metoder, som typisk er relateret til værdien. I System.Int32 findes eksempelvis metoden ToString, som kan bruges til at konvertere heltalsværdien til tekst.
Så System.Int32-strukturer repræsenterer altså et heltal, og hvis du ønsker at benytte et heltal i din kode, skal du blot skabe en instans:
// bemærk – skrives normalt på en anden og hurtigere måde
// dette er blot et eksempel
System.Int32 a = new System.Int32();
-
Skab en variabel kaldet a, som kan indeholde en instans af strukturen (typen) System.Int32
-
Opret en ny instans af System.Int32 (bemærk new-kodeordet)
-
Tildel den nye instans til variablen a
Alle instanser af System.Int32 er fuldstændig ens bortset fra værdien (i dette tilfælde heltalsværdien). De har samme metoder, fungerer præcis på samme måde, og fylder det samme i hukommelsen.
Her oprettes to heltal:
De er begge instanser af System.Int32-typen, men har hver sin værdi.Et andet eksempel på en type er en klasse, du selv skaber til at repræsentere en terning i et Yatzy-spil. I typedefinitionen fortæller du kompileren, hvilke værdier en instans vil have (måske blot en værdi af terningens antal øjne), og hvilke metoder, som arbejder på instansens værdier. Måske kunne det være smart med en metode, der udskriver værdien eller en metode, der ryster terningen ved at tildele en tilfældig værdi.
Når du har defineret typen, kan du selv, eller andre der må benytte din klasse, skabe instanser (objekter) af din skabelon, og benytte dem i eksempelvis et Yatzy-spil:
Terning t = new Terning();
t.Værdi = 4;
t.Print();
t.Ryst();
t.Print();
// Du behøver ikke forstå koden - det er blot et eksempel på
// egne typer
class Terning {
public int Værdi { get; set; }
private static Random rnd = new Random();
public void Ryst()
{
Værdi = rnd.Next(1, 7);
}
public Terning()
{
Ryst();
}
public void Print()
{
Console.WriteLine($"[ {Værdi} ]");
}
}
Der findes et hav af typer i runtime, som du kan benytte, men du vil sjældent skabe en C# applikation uden at skabe dine egne typer.
Vi kommer tilbage til strukturer, klasser og de andre typer, samt opdeling af hukommelse, men lige nu er det vigtigt, at du forstår, at alt er baseret på dine og andres typedefinitioner (skabeloner).
Hierarki af typer
Alle definitioner af typer – både dine egne og dem fra eksempelvis Microsoft selv – er placeret i et såkaldt namespace. Et namespace kan bestå af mange typer samt andre namespaces, og man kan derfor opbygge et hierarki af typer. Det sikrer logisk struktur og indkapsling, og samtidigt undgår man et eventuelt navnesammenfald mellem typer. Slutteligt gør det typerne nemme at finde i Visual Studio og Visual Studio Code.
Det bedste eksempel er Microsofts egne typer, som er placeret i eller under System-namespacet. Her finder man typer relateret til konsol (System.Console), matematik (System.Math), tilfældige tal (System.Random), men også andre namespaces som opbevarer typer relateret til filer (System.IO), XML (System.Xml), databaser (System.Data), sikkerhed (System.Security) og så videre.
Under System-namespaces findes et kæmpe hierarki af typer, som du bruger hele tiden, og da man i C# benytter punktumnotation til at adskille namespaces og typer, er det ret nemt at finde rundt.
Eksempelvis skal du senere arbejde med samlinger, og de er placeret i et namespace et stykke nede i træet:
Det kan du læse som typen ListNår du skriver kode, kan du selv opbygge en struktur og et hierarki, som passer dig, ved at benytte namespace-kodeordet. Du kunne eksempelvis ønske en struktur, hvor alle typer ligger under et samlet namespace:
I så fald placerer du typer i samme namespace (NS1 – men det kan kaldes, hvad du har lyst til). Det vil se således ud i kode:
namespace NS1
{
public class A { }
public interface B { }
public struct C { }
public enum D { }
public delegate Action E();
}
Du kan også vælge at benytte to namespaces:
Hvilket kodes som:
namespace NS1
{
public class A { }
public interface B { }
public struct C { }
public enum D { }
public delegate Action E();
}
namespace NS2
{
public class A { }
public interface B { }
public struct C { }
public enum D { }
public delegate Action E();
}
Ved at benytte flere namespaces kan du skabe en struktur og undgå navnesammenfald. Selv om der er to klasser med navnet A, så er de placeret i hvert sit namespace og hedder i virkeligheden NS1.A og NS2.A – det er to forskellige typer.
Sluttelig kan du vælge en mere kompleks struktur som eksempelvis:
Hvilket kodes som:
namespace NS1
{
namespace NS2
{
public class A { }
public interface B { }
public struct C { }
public enum D { }
public delegate Action E();
}
namespace NS3
{
public class A { }
public interface B { }
public struct C { }
public enum D { }
public delegate Action E();
}
}
Her to forskellige klasser A i både NS2 og NS3, og hedder i virkeligheden NS1.NS2.A og NS1.NS3.A.
Den minder om den Microsoft benytter, hvor System-namespacet svarer til NS1-namespacet.
Det er helt op til dig, hvor du ønsker at placere dine typer. Hvis du ikke angiver et namespace, vil kompileren gøre det for dig.
Info
Som begynder behøver du ikke gå så meget op i en fin strukturel opbygning. Placér alle typer i ét namespace til at starte med. Når du skaber en applikation, vil navnet på det overordnede namespace automatisk blive sat til det navn, du har angivet til applikationen.
Helt grundlæggende om tekster
Tekster (kaldet strenge) er en samling af tegn og findes i alle programmeringssprog i en eller anden form. Der kommer meget senere mere om strenge senere, men du vil falde over typen allerede i de første kodeeksempler så her er en kort introduktion.
En streng (datatypen hedder en string) i C# er omkranset af dobbeltplinger (“”). Her erklæres en variabel til at indeholde en kort tekst:
Du kan sammenlægge strenge med + operatoren:
string fornavn = "Mathias";
string efternavn = "Cronberg";
string navn = fornavn + " " + efternavn; // Mathias Cronberg
Yderligere vil du se brugen af såkaldte string-templates i en masse eksempler på nettet. Det er en simpel og effektiv måde at sammenlægge strenge, og sker ved at danne en skabelon med $”” hvor udtryk og variabler kan indsættes med tuborgklammer:
string fornavn = "Mathias";
string efternavn = "Cronberg";
string navn = $"Mit navn er {fornavn} {efternavn}";
// Mit navn er Mathias Cronberg
Tekster kan bestå af specialtegn af forskellige typer (tabulering, backslash, linjeskift med videre) og de kan håndteres på forskellig måde. Følgende kode
er besked til kompileren om at den skal ignorere at \t i virkeligheden er en tabulering. Det sørger tegnet @ for.
Der er meget mere du skal vide om strenge - dette var blot en introduktion så du lige kender lidt til typen.
Helt grundlæggende om metoder
Metoder er i alle programmeringssprog en måde at genbruge kode, og dermed gøre koden mere læsbar og nem at vedligeholde. Du kan se det som en blok kode, der kan afvikles (kaldes) en eller flere gange.
I C# er metoder integreret i en type, og kan enten bestå af kode, der skal afvikles uden en returværdi (kaldes en void-metode), eller returnere et resultat af en konkret type (tekst, tal, sand/falsk med videre). Yderligere kan en metode have argumenter, ligesom en matematisk funktion, og de angives altid i parentes.
Du skal senere lære at skabe dine egne metoder, men her er for en god ordens skyld et par eksempler på brug af allerede eksisterende metoder, så du kan genkende syntaksen, når du ser den:
// Kald til metoden WriteLine på Console-klassen uden argumenter.
// Metoden har ingen returværdi (void-metode)
System.Console.WriteLine();
// Kald til metoden Delete på File-klassen
// med et enkelt argument. Metoden
// har ingen returværdi (void-metode)
System.IO.File.Delete(@"c:\temp\data.txt");
// kald til metoden Delete på Directory-klassen
// med to argumenter. Metoden
// har ingen returværdi (void-metode)
System.IO.Directory.Delete(@"c:\temp\", true);
// kald til metoden RealAllText på File-klassen.
// Metoden har et argument og returnerer en streng (tekst)
string a = System.IO.File.ReadAllText(@"c:\temp\data.txt");
// kald til metoden Max på Math-klassen.
// Metoden har to argumenter
// og returnerer en double (kommatal)
double b = System.Math.Max(5, 4);
Bemærk at hvis en metode ikke har nogen argumenter, skal du angive dette med (), og eventuelle argumenter adskilles med komma.
Info
I nogle programmeringssprog kaldes metoder for funktioner eller procedurer, men i C# hedder det metoder, fordi C# er objektorienteret af natur, og alle metoder er en del af en typedefinition. Det dækker over samme begreb – en blok kode der kan kaldes med eventuelle argumenter som kan returnere et resultat, hvis du ønsker det.
Helt grundlæggende om Console klassen
Du kommer til at skrive værdier ud på konsollen en del gange, så det er vigtigt, du kender til de metoder, du kan benytte i den sammenhæng.
De er alle fra System.Console-klassen, og den vigtigste er metoden WriteLine. Den skriver blot forskellige værdier (tekster, tal, datoer med videre) ud på konsollen. Den kan eventuelt kombineres med Write-metoden, som også udskriver, blot uden linjeskift:
// Udskriver tekst til konsol
Console.WriteLine("Skriver en tekst...");
string a = "Skriver en anden tekst";
Console.WriteLine(a);
// Tom linje
Console.WriteLine();
// Skriver uden linjeskrift
Console.Write("a");
Console.Write("b");
Console.Write("c");
Console.WriteLine();
Metoderne WriteLine og Write er egentlig alt, hvad du behøver at benytte, men nogle gange kan det være praktisk at kunne hente en værdi fra brugeren, skifte farve og måske endda få computeren til at sige beep:
// Læs fra linje
string input = Console.ReadLine();
// Afvent at brugeren trykker på en tast
ConsoleKeyInfo k = Console.ReadKey();
if (k.Key == ConsoleKey.A) { }
if (k.Key == ConsoleKey.Escape) { }
Console.WriteLine();
// Farve
Console.ForegroundColor = ConsoleColor.Red;
Console.WriteLine("Rød");
Console.ForegroundColor = ConsoleColor.Gray;
// "beep"
Console.Beep();
Som du ser er der masser af muligheder med Console-klassen til både input og output, men du vil sikkert bruge WriteLine-metoden i 90% af tilfældende.
Tip
Hvis du benytter Console.ReadLine gennem Visual Studio Code kan du tilrette launch.json således, at der benyttes en integreret terminal. Det kan gøres ved at ændre console-egenskaben fra internalConsole til integratedTerminal.
C# er typestærkt og typesikkert
I C# er alt som nævnt baseret på typer – både dem runtime stiller til rådighed, som eksempelvis simple variabler eller typer der bruges i daglig kode, samt dine egne typer. At C# er typestærkt betyder blandt andet, at når du erklærer variabler og felter (variabler i typer), skal du angive en konkret type. Det gælder også for argumenter til metoder samt eventuelle returværdier. Alt er baseret på konkrete typer.
Det kan lyde besværligt, især hvis man kommer fra typesvage sprog som JavaScript eller Python, men det betyder dels at runtime kan optimere afvikling og hukommelsesforbrug, men også at et udviklingsmiljø som VS eller VSC kan give en masse hjælp i forbindelse med udvikling:
int a = 10; // erklæring af et heltal
a = 10.4; // fejl (kommatal er ikke et heltal)
a = "test"; // fejl (streng er ikke et heltal)
I typestærke sprog som C#, er typesikkerhed også en central egenskab. Type-sikkerhed betyder, at sproget forhindrer mange typer af typefejl ved at sikre, at alle typekonverteringer og operationer mellem forskellige typer er eksplicit defineret og kontrolleret. Hvis du forsøger at tildele en værdi af en inkompatibel type til en variabel, vil dette føre til en kompileringsfejl. Denne tilgang sikrer, at mange typefejl opdages og rettes under kompilering, før koden kører. Dette bidrager til en høj grad af sikkerhed og stabilitet i programmet, da det reducerer risikoen for runtime-fejl relateret til typekonvertering.
Typestærkt og Typesvagt?
Typestærke sprog er karakteriseret ved, at typeinformation er strengt håndhævet. I sådanne sprog, som C# og Java, skal du eksplicit angive typen af variabler, parametre og returværdier. Dette betyder, at typefejl opdages under kompilering, hvilket kan føre til mere stabil og forudsigelig kode. For eksempel i C# skal du erklære variabler med en specifik type, og forsøg på at tildele en anden type vil resultere i en kompileringsfejl. Dette hjælper udviklere med at finde fejl tidligt i udviklingsprocessen og gør det muligt for værktøjer som Visual Studio at tilbyde omfattende hjælp og optimering.
int number = 10;
// number er af typen int. Forsøg på at tildele en
// anden type (f.eks. en string) vil give en kompileringsfejl.
På den anden side er typesvage sprog, som JavaScript og Python, mindre strenge med hensyn til typekontrol. Her kan variabler ændre type dynamisk, og typefejl opdages ofte først ved runtime. For eksempel i JavaScript kan en variabel, der først er en integer, senere blive tildelt en streng uden problemer. Denne fleksibilitet kan gøre det hurtigere at skrive kode og kan være fordelagtigt i visse situationer, men det kan også føre til skjulte fejl, som først bliver opdaget, når koden køres.
let value = 10;
value = "Nu er jeg en streng";
// value kan ændre type fra number til string uden fejl.
Typestærke sprog giver en mere struktureret tilgang, hvor typeinformation er en væsentlig del af programmeringssproget, mens typesvage sprog tilbyder mere fleksibilitet men kan kræve mere omhyggelig fejlfinding under runtime.
C# er hukommelsessikkert
I C# skal du sjældent bøvle med at skrive kode til at rydde op i hukommelsen – det klarer runtime typisk. Men det betyder også, at runtime skal være meget skrap i styring af tilgang til hukommelsen.
Derfor kan du ikke tilgå værdier i hukommelsen på steder, hvor du ikke har noget at gøre, og anden kode kan heller ikke pille ved dine værdier. Det giver ikke blot sikkerhed for dine data i hukommelsen, det giver også mulighed for at skrive kode, som udnytter den feature:
{
int a = 10;
// her må man gerne tilgå værdien af a
a = a + 1;
}
a = a + 1; // fejl - a findes ikke her
Du kan se en blok omkranset af tuborgklammer som en lille sandkasse. Alt, hvad der foregår der, bliver i den sandkasse og kun i den – med mindre du som udvikler giver andre steder i koden en reference til en konkret variabel. Mere om det senere i bogen.