Tegn og tekster
Du har tit behov for at opbevare tegn og samlinger af tegn, og derfor stiller C# naturligvis også forskellige typer til rådighed. Tekster omtales i programmeringsteori som en streng eller flere strenge (oversat string eller strings på engelsk) fordi det netop kan opfattes som en streng af tegn.
Information til undervisere
Følgende er vigtigt
- Strenge er immutable (se evt forklaring)
-
- operatoren må aldrig bruges i løkker mv - brug StringBuilder eller lign
- Interpolerede strenge / string templates bruges en del
- en streng er en klasse - derfor kan den få værdien null
- måske introduktion til stack/heap (her fra SharpLab hvor a er en struct på stack og b er en reference på heap)
Tegn
Hvis du gerne vil gemme et enkelt tegn, kan du benytte datatypen System.Char eller blot char:
Typenavn | Genvej | Størrelse |
---|---|---|
System.Char | char | 16-bit |
Den kan indeholde et enkelt tegn, som en konstant kan den tildeles værdi med enkeltplinger (‘):
// uinitialiseret
System.char a; // eller blot: char a;
// initialiseret med A
char b = 'A';
// initialiseret med *
char c = '*';
// værdien fra c kopieres til d
char d = c;
Et tegn gemmes i virkeligheden som et tal defineret i en standard kaldet Unicode (læs mere hos WikiPedia). Således er et stort A i virkeligheden nummer 65, et stort B er nummer 66, et Ø er nummer 216, en stjerne (*) er 42 og så videre.
Hvis du gerne vil finde et tegns nummer, eller gerne vil finde et tegn ud fra et nummer, kan du foretage en simpel typekonvertering:
char a = '*';
Console.WriteLine(Convert.ToInt32(a)); // 42
int svar = 42;
Console.WriteLine(Convert.ToChar(svar)); // *
Der er flere måder at foretage typekonverteringen på, men metoder fra System.Convert er nemmest at forstå.
Info
Måske har du læst The Hitchhiker’s Guide to the Galaxy af Douglas Adams.
I bogen henvises til super computeren Deep Thought, som har brugt 7,5 million år på at beregne The answer to the Ultimate Question of Life, The Universe, and Everything, og fundet frem til, at svaret er 42. Fans over hele verden har fremført alle mulige teorier om, hvorfor svaret lige præcis skulle være 42, og en af dem er, at 42 svarer til en * i Unicode, og at en stjerne repræsenterer alt i mange kommandoer og instruktioner relateret til IT. I virkeligheden er svaret på spørgsmålet “Hvorfor lige 42” noget helt andet – spørg Google hvis du har tid og lyst.
Strenge
En streng i C# er en samling af tegn og er repræsenteret med datatypen System.String – eller blot string:
Typenavn | Genvej | Forklaring |
---|---|---|
System.String | string | Unicode tekst |
En string-variabel kan tildeles værdier, hvor tekster er omkranset af dobbeltplinger (“):
// uinitialiseret
System.String a; // eller blot string a;
a = "abcde"; // tildelt en værdi
// initialiseret med det samme
string b = "a";
string c = "Kort sætning";
string d = "Noget længere sætning - i øvrigt med danske tegn";
Du kan eventuelt også benytte new-operatoren til at oprette en streng, men det er typisk kun, når du gerne vil starte med en streng med et antal ens tegn:
Lidt forvirrende kan man se strenge oprettet på tre forskellige måder:string a = ""; // genvejsnavn
String b = ""; // System.String (forudsat "using System")
System.String c = ””; // System.String
Operatorer relateret til strenge
Når du benytter strenge, kan du benytte operatorer, du kender fra andre datatyper:
Operator | Forklaring |
---|---|
= | Tildeling |
== | Sammenligning |
+ | Lægger strenge sammen |
+= | Samme som + med tildeler resultat til samme variabel |
Her et par eksempler:
// Tildeling
string a;
a = "a";
Console.WriteLine(a); // "a"
// Sammenligning
bool test;
test = a == "a";
Console.WriteLine(test); // true
Console.WriteLine(a == "b"); // false
// Sammenlægning
a = a + " b";
Console.WriteLine(a); // "a b"
a += " c";
Console.WriteLine(a); // "a b c"
Metoder relateret til strenge
Når du har skabt en instans af System.String kan du benytte en masse forskellige metoder – her nogle af de mest benyttede:
string a;
a = " Dette er en længere sætning ";
Console.WriteLine(a.Length); // 31
Console.WriteLine(a.Contains("sætning")); // true
Console.WriteLine(a.EndsWith("ning ")); // true
Console.WriteLine(a.StartsWith("Test")); // false
Console.WriteLine(a.ToUpper());
// " DETTE ER EN LÆNGERE SÆTNING "
Console.WriteLine(a.ToLower());
// " dette er en længere sætning "
Console.WriteLine(a.Trim());
// "Dette er en længere sætning"
Console.WriteLine(a.Substring(2, 2));
// "De" (fra pos 2 og 2 frem)
Der findes en del flere du bør prøve af i Visual Studio.
Selve System.String-klassen har også nogle interessante statiske metoder:
string a = "a", b = "b";
Console.WriteLine(System.String.IsNullOrEmpty(a));
// false
Console.WriteLine(System.String.Join(" ", "*", a, b, "*"));
// "* a b *"
Console.WriteLine(System.String.Compare(a, b));
// -1
Specialtegn
Nogle gange har du behov for at benytte specialtegn som tabulering, linjeskift, anførselstegn med videre, og disse kan benyttes, hvis du benytter en omvendt slash (\ – en backslash):
Specialtegn | Forklaring |
---|---|
\b | Backspace |
\f | Formfeed |
\n | New line |
\r | Carriage return |
\t | Tabulering |
\’ | Enkelt anførselstegn |
\” | Dobbelt anførselstegn\ |
\x hhhh | Specielt Unicode tegn (hhhh = nummer) |
Det klassiske eksempel på manglende brug af specialtegn er dette:
Bemærk, at \t bliver konverteret til en tabulering, og dermed bliver stien til filen helt forkert. Strengen bør i stedet skrives som: Du kan også benytte en @ foran strenge, som fortæller kompileren, at den skal ignorere alle specialtegn: Her er et par eksempler på brug af andre specialtegn:string a;
a = "Linje1\r\nLinje2\r\nLinje3\r\n";
Console.WriteLine(a);
// skriver tre linjer
a = "a\tb";
Console.WriteLine(a);
// a {tab} b
a = "abc\"def";
Console.WriteLine(a);
// abc"def
Baggrunden for \r\n
i programmeringssprog
I programmering bruges \r\n
til at angive en ny linje i nogle operativsystemer, herunder Windows. Dette format stammer fra de gamle dage med skrivemaskiner og teleprintere:
- Vognretur (
\r
): Flytter skrivepositionen tilbage til begyndelsen af linjen. I ASCII har den værdien 13. - Linjeskift (
\n
): Flytter papiret op, så en ny linje er klar til skrivning. I ASCII har den værdien 10.
Da computere begyndte at bruge teleprintere som terminaler, blev disse mekaniske handlinger overført til softwaren. Operativsystemerne udviklede forskellige metoder til at håndtere nye linjer:
- Unix-systemer: Bruger kun linjeskift (
\n
) til at angive en ny linje, hvilket er en simplificering sammenlignet med den mekaniske operation. - Windows (og tidligere DOS): Anvender både vognretur og linjeskift (
\r\n
) for at bevare kompatibilitet med ældre teknologier og standarder. - Classic Mac OS: Brugte oprindeligt kun vognretur (
\r
), men skiftede til linjeskift (\n
) med introduktionen af Mac OS X, som er baseret på Unix.
I dag kan forskelle i brugen af nye linje-tegn føre til kompatibilitetsproblemer mellem forskellige systemer, hvilket er grunden til, at mange programmeringssprog og miljøer tilbyder værktøjer til at håndtere disse forskelle. I nogle sammenhænge, som f.eks. netværksprotokoller, er det stadig standard at bruge \r\n
.
Interpolerede strenge
I C# har du mulighed for at skabe såkaldte string templates – også kaldt interpolerede strenge. Det kan gøre det meget nemmere at oprette en streng, der består af værdier fra andre variabler, som måske endda ønskes formateret.
En interpoleret streng kan skabes ved at sætte $ foran strenge og tilføje variabler i tuborgklammer:
string navn = "Mathias";
int alder = 13;
string a = $"Jeg hedder {navn} og er {alder} år gammel.";
Console.WriteLine(a); // Jeg hedder Mathias og er 13 år gammel.
// tuborgklammer definerer et udtryk så
// der kan benyttes beregninger, metoder mv
a = $"Jeg hedder {navn.ToUpper()} og er født i {DateTime.Now.Year - alder}.";
Console.WriteLine(a); // Jeg hedder MATHIAS og er født i 2006.
double pris = 2300.2523;
DateTime dato = new DateTime(2020, 1, 1);
string a = $"Prisen på varen er {pris:N2}, og har udløb i måned {dato:M-yy}";
Console.WriteLine(a);
// Prisen på varen er 2.300,25, og har udløb i måned 1-20
Der er flere muligheder ved brug af interpolerede strenge, men det kan du eventuelt læse om i dokumentationen.
Raw String Literals
Med introduktionen af C# 11 findes nu raw string literals, som gør det muligt at skrive strenge over flere linjer uden at bruge escape-sekvenser. Dette er nyttigt, når du arbejder med lange strenge eller tekster, der indeholder specialtegn, som linjeskift og citationstegn.
En raw string literal i C# 11 skrives med tre dobbeltanførselstegn ("""
) i både starten og slutningen af strengen (på en linje for sig selv). Her er et eksempel:
Denne metode bevarer indrykning og linjeskift præcist som skrevet, hvilket gør det meget lettere at håndtere komplekse tekststrukturer.
Strenge er en reference-type
Indtil videre har du lært om såkaldte strukturer (på engelsk struct). Alle de simple variabler, vi har kigget på, har været af denne konkrete type. Hvis du er i tvivl om en type er struct eller en af de andre typedefinitioner, man kan benytte i C# (class, interface, enum eller delegate), kan du altid holde musen hen over typen i Visual Studio eller Visual Studio Code:
Microsoft har valgt at definere de fleste simple variabeltyper som structs for at opnå så høj performance som muligt, og som du skal se senere, kan du også vælge at benytte strukturer til dine egne typer.
Men en string (System.String) er ikke en struktur – det er en klasse:
Det betyder blandt andet, at variablen ikke indeholder værdien, men derimod en reference til et sted i hukommelsen, hvor værdien findes. Der er mange årsager til, at man gerne vil benytte klasser i stedet for strukturer, og du vil lære meget mere om dette senere i bogen. Lige nu skal du blot bide mærke i, at en variabel af typen string er en klasse, og dermed indeholder en reference og ikke en værdi. Derfor kalder man også variabler af klasser for referencevariabler.
Fordi en reference-variabel kan indeholde en reference (i virkeligheden et nummer svarende til en adresse i hukommelsen), kan den også indeholde en værdi, der indikerer, at den ikke refererer til noget. I C# hedder denne værdi null, og alle reference-variabler kan have denne værdi:
Her er et billede af et stack og heap diagram igen. Bemærk, at variablen d indeholder en reference til en streng, som ligger på heap’en og variablen e indeholder null værdi (den peger ikke på noget):
Som tidligere nævnt - hvis du har brug for at få visualiseret stack og heap, kan du benytte SharpLab med følgende kode:
det giver følgende resultat:
Bemærk, at a er en char (struct) og b og c er en string (klasse).
Udfordringen med null
At du kan arbejde med referencer i stedet for værdier, kan være super-smart og meget effektivt, men det betyder også, at du kan have variabler, som ikke refererer til noget, og det kan være et problem:
string a = "mikkel";
Console.WriteLine(a.ToUpper()); // MIKKEL
string b = null;
Console.WriteLine(b.ToUpper());
// Fejl (exception) - b peger ikke på noget
Som C# udvikler vil du tit høre om (og bøvle med) såkaldte null reference exceptions, som opstår, når du tror, du arbejder med en konkret værdi, men i virkeligheden har fat i null-værdi. Derfor bør du være sikker på, at du har fat i noget konkret:
string a = "mikkel";
Console.WriteLine(a.ToUpper()); // MIKKEL
string b = null;
// hvis b er forskellig fra null så...
if (b != null)
{
Console.WriteLine(b.ToUpper());
// Fejl (exception) - b peger ikke på noget
}
// kan (bør) også skrives sådan her
if (b is not null)
{
Console.WriteLine(b.ToUpper());
// Fejl (exception) - b peger ikke på noget
}
Info
Du bør egentlig bruge is null
or is not null
i stedet for at bruge == null
og !=null
. Det skyldes, at man i mere avanceret C# har mulighed for at overskrive måden == og != fungerer.
C# indeholder et par operatorer, som kan hjælpe med at kontrollere, om en variabel har værdien null:
Operator | Forklaring |
---|---|
?. | Tester om variabel er null og fortsætter kun ved værdi |
?? | Tester om variabel er null - ellers returneres værdi |
De kan bruges til at forkorte koden lidt:
string a = null;
if (a != null) {
Console.WriteLine(a.ToUpper());
}
// eller
Console.WriteLine(a?.ToUpper());
// eller - hvis a == null så sæt b til "" (en konkret værdi)
string b = a ?? ""; // eller String.Empty
Console.WriteLine(b.ToUpper());
Sammenligning af strenge
Der er mange forskellige måder at sammenligne metoder på men den statiske Equals-metode er ret brugt. Her er et par eksempler:
bool erCaseSensitiveLige = string.Equals("Hej", "hej"); // False, da standard sammenligning er case-sensitive
bool erCaseInsensitiveLige = string.Equals("Hej", "hej", StringComparison.OrdinalIgnoreCase); // True, ignorerer bogstavernes casing
bool erCultureSensitiveLige = string.Equals("Hej", "HEJ", StringComparison.CurrentCultureIgnoreCase); // True, hvis den nuværende kultur ignorerer casing
bool erInvariantCultureLige = string.Equals("Hej", "HEJ", StringComparison.InvariantCultureIgnoreCase); // True, ignorerer casing i en kultur-uafhængig sammenligning
bool starterMedHej = "Hejsa".StartsWith("Hej", StringComparison.OrdinalIgnoreCase); // True, starter med "Hej" uafhængigt af casing
bool enderMedHej = "FarvelHej".EndsWith("hej", StringComparison.OrdinalIgnoreCase); // True, slutter med "hej" uafhængigt af casing
bool indeholderHej = "Velkommen, Hej!".Contains("HEJ", StringComparison.OrdinalIgnoreCase); // True, indeholder "HEJ", ignorerer casing
Disse eksempler viser forskellige metoder til sammenligning af strenge i C# under hensyntagen til forskellige aspekter som case sensitivity og kulturafhængighed. Ved at vælge den korrekte StringComparison
-værdi kan du præcist kontrollere, hvordan sammenligningen udføres i forhold til bogstavernes store og små bogstaver samt den kulturelle kontekst.
Strenge er immutable
Slutteligt er strenge immutable ligesom de fleste simple variabler.
Info
En immutable datatype er en speciel type, hvor data ikke kan tilrettes, efter de er tildelt værdier ved initialisering.
Derfor vil alle metoder returnere en ny instans og ikke tilrette den eksisterende:
string a = "mathias";
Console.WriteLine(a); // mathias
a.ToUpper(); // Returværdi ignoreres
Console.WriteLine(a); // mathias
a = a.ToUpper(); // Returværdi gemmes
Console.WriteLine(a); // MATHIAS
Prøv følgende kode:
Det er et simpelt stykke kode, der opretter en streng med 500.000 stjerner, og burde afvikles inden for få millisekunder. Men hvis du prøver, vil du konstatere, at det tager over et minut (afhængig af din maskine). Det skyldes, at en sammenlægning af strenge skaber en ny kopi, som bliver sammenlagt til en ny kopi, som bliver sammenlagt til en ny værdi, som …
Faktum er, at koden:
resulterer i, at data flyttes rundt i hukommelsen hele tiden – og at det tager en frygtelig masse tid.
Warning
Husk at du aldrig må manipulere strenge i løkker, som kan risikere at tælle til et ukendt antal. Du kan meget hurtigt løbe ind i et performance-problem.
Hvis du tæller til 10 eller 100, er det ligegyldigt, men der skal ikke meget til, før det tager lang tid at afvikle, og det er nemt at forestille sig situationer, hvor du i udvikling måske tæller til 100, men i produktion til 100.000 – eksempelvis fordi du henter nogle data fra en database og skaber en csv-fil.
Strenge og gentagne ændringer kan altså være et stort performancemæssigt problem, og System.String (string) er ikke skabt til det. Det findes der heldigvis andre typer som er – herunder System.Text.StringBuilder som netop er skabt til effektiv manipulering af strenge. Læs mere om klassen StringBuilder her på sitet eller i dokumentationen.
Eksempel der sætter fokus på problemet med manipulering i løkker
Prøv følgende kode. Den måler hvor lang tid det tager at tilføje en * til en string 500.000 gange.
Console.WriteLine("Start");
// Stopur
System.Diagnostics.Stopwatch s = new System.Diagnostics.Stopwatch();
s.Start();
string a = "";
for (int i = 0; i < 500000; i++)
{
a += "*";
}
s.Stop();
Console.WriteLine($"ms = {s.ElapsedMilliseconds}");
Det tager alt for lang tid set i lyset af, at der kun tælles til 500.000.
Brug StringBuilder i stedet:
Se i øvrigt StringBuilder og Benchmark.NET
Uforanderlighed og string pool
Når vi taler om strenge i C#, er to vigtige begreber: uforanderlighed (immutability) og string pooling:
-
Uforanderlighed (Immutability): I C# er en streng uforanderlig. Dette betyder, at når en streng er oprettet, kan dens indhold ikke ændres. Hver gang du ser ud til at “ændre” en streng, skaber du faktisk en ny streng i hukommelsen. For eksempel, hvis du sammenkæder to strenge, opretter C# en helt ny streng, der indeholder resultatet af sammenkædningen, mens de oprindelige strenge forbliver uændrede. Denne adfærd sikrer trådsikkerhed, da samme streng kan tilgås af flere tråde uden risiko for ændringer.
-
String Pooling: For at optimere ydeevnen og reducere hukommelsesforbruget anvender C# en teknik kendt som string pooling. Når en streng er oprettet ved compile-time (som en literal), placerer C# den i en intern pool. Hvis en identisk strengliteral oprettes et andet sted i koden, henviser C# simpelthen til den streng, der allerede findes i poolen, i stedet for at oprette en ny kopi. Denne proces sker automatisk for strengliterals, men kan også anvendes manuelt for dynamisk genererede strenge.
Disse to aspekter af C# strenge har betydelige konsekvenser for både ydeevne og programmeringspraksis. Uforanderligheden sikrer, at strenge er sikre at dele på tværs af forskellige dele af et program, mens string pooling hjælper med at holde hukommelsesforbruget nede ved at genbruge identiske strenge.