Skip to main content

9789151121383

Page 1


Programmering 2 C#

Andra upplagan

1. Grunder 6

1.1 Elementära datatyper 7

Literaler 8

Implicit typomvandling 9

Explicit typomvandling 9

1.2 Strängar 10

1.3 Operatorer 12

1.4 Styrstrukturer 13

Foreach-loopen 13

1.5 Samlingar 14

Värdetyper och referenstyper 14

Regelbunda fält 15

Oregelbundna fält 17

1.6 Andra datastrukturer 17

1.7 Enumerationer 21

Flaggor 22

Bitoperatorer 23

1.8 Metoder 25

Överlagring 25

Frivilliga parametrar 26

Namnade parametrar 26

Multipla returvärden 27

1.9 Klasser 28

Kopieringskonstruktor 29

Förenklad syntax 30

Statiska metoder 31

Statiska variabler 32

Klassdiagram 35

1.10 Typen dynamic 37

1.11 Undantagshantering 41

Strukturerad felhantering 43

Repetitionsfrågor 46

2. Klasshierarkier 48

2.1 Arv 49

2.2 Polymorfism 53 is och as 54 virtual och override 54

virtual function table 55

2.3 Abstrakta klasser 57

Klassrelationer 58

2.4 Slutna klasser 60

2.5 Interface 60

Att designa en klasshierarki 62

2.6 Generiska klasser 67

Indexerare 70

2.7 IComparable* 71

2.8 Dokumentation 74

Repetitionsfrågor 77

Projekt 2 Ritboken 78

3. Fönster 80

3.1 Dialoger 81

3.2 MenuStrip 83

3.3 ContextMenuStrip 84

3.4 ToolTip 85

3.5 TabControl 86

3.6 RichTextBox 87

Teckensnitt 87

Textjustering 88

3.7 DataGridView 89

DataGridView i designläget 90

DataGridView för att visa data 92

DataGridView för att skriva in data 92

DataGridView för att läsa in data 92

Användbara egenskaper 92

Händelser 94

3.8 TreeView 97

TreeView i designläget 99

TreeView i kod 100

3.9 Layout 103

3.10 Fönsterkontroller i kod 106

3.11 Att göra egna kontroller 108

UserControl 109

Att ärva från en standardkontroll 110

Projekt 3 Den lilla ordbehandlaren 113

Repetitionsfrågor 115

4. Filer 116

4.1 Sökvägar 117

4.2 Fildialoger 118

4.3 Profiler 120

4.4 Textfiler 121

Läsa och skriva rad för rad 123

4.5 Binära filer 125

Projekt 4 CSV-filer 128

Repetitionsfrågor 129

5. Nätverk 130

5.1 Local Area Network (LAN) 131

5.2 Wide Area Network (WAN) 132

Accesspunkter 132

Internet 132

Hemnätverk 132

5.3 Protokoll 133

Det fysiska lagret 133

Datalänklagret 134

Nätverkslagret 134

Transportlagret 136

Portnummer 136

TCP 137

Applikationslagret 138

DNS 138

Brandväggar 138

5.4 Nätverksprogrammering 139

Payloads 139

5.5 UDP-programmering 140

5.6 TCP-programmering 142

Server och klient 142

5.7 A synkron nätverksprogrammering 145

Projekt 5 Chatapplikation 150

Repetitionsfrågor 152

6. Databaser 154

6.1 Relationsdatabaser 155

Nycklar 155

6.2 Datatyper 156

6.3 SQL 157

6.4 *Normalisering 163

Första normalformen 163

Andra normalformen 164

Tredje normalformen 164

Främmande nycklar 165

Referensintegritet 165

6.5 Databasdesign 165

ER-diagram 166

Implementering i tabeller 167

6.6 Implementera skoldatabasen 169

Tabellen Elever 169

Tabellen Böcker med automatiska id-nummer och en främmande nyckel 170

Tabellen Kurser 172

Tabellen Läser med två främmande nycklar 172

Tabellen Telefoner 174

6.7 Entity Data Model 174

Skapa EDM i Visual Studio 175

6.8 LINQ 178

Applikationens fönster 179

LINQ 181

Repetitionsfrågor 187

7. Facit till övningar och repetitionsfrågor

Register 226

190

1Grunder

Programmeringsspråkets grunder omfattar operationer såsom tilldelningar, beräkningar och typomvandlingar, samt styrstrukturer som selektion, iteration och funktion. Dessutom krävs det kunskap om klasser, objekt och referenser.

I det här kapitlet repeteras och fördjupas kunskaperna från kursen

Programmering 1 när det gäller programmeringsspråket C# och programbiblioteket .NET, särskilt Windows Forms.

Grunderna i programmering består i att kunna programmeringsspråket, den utvecklingsmiljö som man programmerar i, samt de viktigaste delarna av ett bibliotek av färdigskriven kod, som till exempel Windows Forms. Med ett sådant bibliotek är det ganska enkelt att hantera fönster och grafik, filer på hårddisken, nätverkskommunikation och många andra vanliga programmeringsuppgifter.

1.1 Elementära datatyper

Ett datorprogram lagrar värden i datorns arbetsminne. Värdets datatyp talar om hur många byte värdet upptar av minnet, och hur det binära värdet är formaterat. I det binära talsystemet används bara siffrorna 1 och 0. Det binära talet 10₂ (basen 2) har till exempel samma värde som decimala talet 2₁₀ (basen 10). 10₂ äpplen är lika många som 2₁₀ äpplen. En binär siffra kallas en bit, och 8 bitar bildar en byte. Det är vanligt att lagra heltal i 32 bitar (4 byte). Värdet 1 ser då ut så här:

I datorns arbetsminne motsvarar detta 32 stycken små elektriska kretsar, där den sista är sluten så att det flyter en ström genom den. I de första 32 kretsarna är strömmen av. 0 betyder alltså ingen ström, 1 betyder ström. Genom att sluta andra kretsar kan andra värden lagras. Det största värdet som kan lagras är när alla kretsar är på: 11111111 11111111 11111111 11111111

Detta representerar ett värde som är lika med 4 294 967 296₁₀

För att kunna lagra negativa tal används den första biten till talets tecken. 0 betyder positivt och 1 betyder negativt. Det största positiva tal som kan lagras blir då istället 01111111 11111111 11111111 11111111

vilket är lika med 2 147 483 64810

För att kunna lagra ett värde med decimaler (till exempel 2,5) i 4 byte anger första biten tecknet, som ovan. Sedan kommer värdet i grundpotensform (i basen 2), fast med exponenten först och värdesiffrorna sist, utan heltalsdel (för i binär grundp otensform blir den alltid 1). Det exakta formatet för flyttal är egentligen överkurs, men det är lätt att hitta bra beskrivningar på internet för den som är intresserad. Om man lagrar värdet 1 (i grundpotensform 1,0 · 2⁰) i det här formatet ser det ut som så:

00111111 10000000 00000000 00000000

Formatet kallas för flyttal. Men dessa byte är också lika med 1 065 353 21610 om formatet är heltal. En viss serie ettor och nollor kan representera helt olika tal, beroende på hur man tolkar ettorna och nollorna.

Nu är det sällan man behöver kunna den binära representationen av flyttal, men det är väsentligt att förstå att för datorns maskinvara är det stor skillnad på heltalsvärdet 1 och flyttalsvärdet 1. Processorn har till exempel särskilda additionskretsar för heltal, och dessa ger fel resultat om man skickar dit värden som är formaterade som flyttal.

Det är ditt jobb som programmerare att tala om hur processorn ska hantera alla värden, genom att ange deras datatyp i programkoden.

C# är ett hårt typat programspråk, vilket innebär att kompilatorn är noga med datatyper. Det finns andra programspråk som inte är lika petiga med datatyper. Det kan tyckas enklare att programmera med sådana språk, men egentligen är det inte det. Maskinvaran är nämligen alltid petig, och bryr sig inte om vilket programmeringsspråk som användes för att skapa maskinkoden. Med ett hårt typat språk har du större kontroll över vad som hamnar i maskinkoden. Många fel upptäcks dessutom av kompilatorn, och man slipper buggar som annars kan vara svåra att hitta.

I tabellen nedan beskrivs de vanligaste elementära datatyperna i C#.

DatatypAnnat namnBeskrivning med ungefärliga värdegränserExempel på literal

shortInt16 2 byte för heltal −32 768 – 32 767. 2

intInt32 4 byte för heltal ± 2 ∙ 109 (ca). −100

longInt64 8 byte för heltal ± 9 ∙ 1018 (ca).

floatFloat 4 byte för flyttal ±3,4 ∙ 1038 (med 7 gällande siffror).2f eller 2F

doubleDouble 8 byte för flyttal ± 1,7 ∙ 10308 (med 15 gällande siffror).2.0 eller 2d eller 2D

charChar 2 byte för en teckenkod enligt Unicode-tabellen.'a' eller 97

stringString En text bestående av ett varierande antal char-värden."Hej världen"

boolBoolean 1 byte för ett sanningsvärde. true eller false

byteByte 1 byte för positiva heltal 0 – 255. 2

ushortUInt16 2 byte för positiva heltal 0 – 65 535 2

uintUInt32 4 byte för positiva heltal 0 – 4 ∙ 109 (ca) 100

ulongUInt64 8 byte för positiva heltal 0 – 1,8 ∙ 1019 (ca)

5000

u i ushort, uint och ulong står för unsigned (utan tecken) och avser positiva heltal.

De elementära datatyperna har alternativa namn i namnrymden System i .NET. Det gör ingen skillnad om man deklarerar en variabel med int eller System.Int32.

Literaler

En literal är ett värde som det ser ut i källkoden. Literaler används till exempel när en variabel initialiseras.

float temperatur = -5f; // -5f är en literal.

F (eller f) anger att typen är float. Värdet hårdkodas i maskinkoden när programmet kompileras, och kopieras till variabeln temperatur när programmet exekveras. Literalens datatyp måste vara samma som (eller kunna omvandlas till) variabelns datatyp.

Implicit typomvandling

Om man skriver en sats som lagrar en int i en double sker en implicit (underförstådd) typomvandling:

double pris = 3;

Literalen 3 är ett heltal, och måste omvandlas till en double för att kunna lagras i pris. Detta sker implicit eftersom en double kan innehålla alla heltalsvärden som en int kan, så information kan inte försvinna.

Ett annat fall där implicit typomvandling sker är när literalen har fler byte än variabeln.

short antalDagar = 5;

Värdet omvandlas implicit till 2 byte. Omvandlingar av literaler görs av kompilatorn, så redan i maskinkoden hårdkodas femman som en short. Om värdet är större än vad som får plats i 2 byte går satsen inte att kompilera.

Omvandlingar mellan variabler görs inte av kompilatorn utan av programmet själv när det kör, men reglerna är desamma. Nedan finns en implicit typomvandling på andra raden.

int a = 2;

double b = a;

Explicit typomvandling

En parentes som bara innehåller en datatyp är en unitär operator och kräver en operand till höger. Operanden omvandlas till den datatyp som står i parentesen.

(int)2.5

Detta kallas för en explicit (uttrycklig) typomvandling och genererar en int med värdet 2. Om man till exempel vill avrunda ett flyttal till ett heltal kan man använda följande trick:

double pris = 3.6; int avrundatPris = (int)(pris + 0.5);

Summan av pris och 0,5 är en double (ett 8 byte stort flyttal som innehåller decimaler). För att kunna lagra det i avrundatPris (ett 4 byte stort heltal utan decimaler), måste man ange en explicit typomvandling, eftersom information försvinner i omvandlingen.

Det går inte att ange vilka typomvandlingar som helst. Satsen nedan går inte att kompilera.

int årtal = (int)"2019";

Ibland kan kompilatorn godkänna en explicit typomvandling i källkoden, som visar sig inte fungera när programmet kör:

object text = "2019"; int tal = (int)text;

Raderna ovan skulle krascha under exekvering med en InvalidCastException (typ omvandling heter ”type cast” på engelska). Typen object tas upp mer senare i boken.

1.2 Strängar

Strängar är av särskilt intresse eftersom mycket väsentlig information lagras eller visas som text av en applikation. En sträng är ingen elementär datatyp eftersom den består av en kombination av char-värden. För att underlätta att skapa, kopiera och hantera text finns en klass i .NET-biblioteket som heter String. Man kan skapa en ny sträng med hjälp av String-klassens konstruktor.

String text = new String( "Hej" );

Eftersom strängvariabler är så vanliga i ett program finns en förenklad syntax som gör att strängar kan deklareras och initialiseras som en vanlig variabel.

string text = "Hej";

string (med litet s) är ett alias för String (med stort S). String-klassen innehåller också ett antal metoder och egenskaper för att manipulera strängar. Nedan ges exempel på några av de mest användbara.

string namn = "Anna Nilsson"; int antalTecken = namn.Length; // Ger 12 char initial = namn[0]; // Ger 'A' int index = namn.IndexOf( ' ' ); // Ger 4

string efternamn = namn.Substring( 5, 7 ); // Ger "Nilsson"

string utanMellanslag = namn.Remove( 4, 1 ); // Ger "AnnaNilsson"

string medBindeStreck = namn.Replace( " ", "-" ); // Ger "Anna-Nilsson"

string endastGemener = namn.ToLower(); // Ger "anna nilsson"

string medMellanNamn = namn.Insert( 5, "Maria " ); // Ger "Anna Maria Nilsson"

string[] delar = namn.Split( ' ' ); // Ger två nya strängar "Anna" och "Nilsson"

Exempel 1.1 Omvandling av strängar

Att omvandla en sträng till ett heltal görs inte med en typomvandling i vanlig mening, utan av en algoritm som räknar ut vad varje tecken är värt beroende på dess position. I C# finns metoden int.Parse för detta ändamål, men det är lärorikt att studera hur omvandlingen egentligen fungerar, och blir dessutom en bra repetition av kunskaper från den tidigare programmeringskursen.

Algoritmen nedan bygger på att tecknet ’0’ har teckenkoden 48, ’1’ har teckenkoden 49, och så vidare. Genom att subtrahera 48 från teckenkoden fås tecknets talvärde. Dessa värden multipliceras sedan med 1, 10, 100 och så vidare i en loop som börjar i slutet av strängen. Vi börjar i slutet eftersom siffran längst till höger alltid är entalet och värd x1, medan siffran längst till vänster har ett värde som beror på hur många siffror det finns.

private void btnKör_Click ( object sender, EventArgs e ) {

// Omvandla till int. int tal1 = ParseToInt( tbxTal1.Text );

// Addera. int summa = tal1 + 5;

// Omvandla summan till sträng och visa svaret. tbxSvar.Text = summa.ToString(); }

// Uppgift a)

private int ParseToInt( string text ) { int tal = 0;

// Börja med värdet av entalet. int positionsVärde = 1;

// "Parsa" texten och beräkna tal. for ( int i = text.Length - 1 ; i >= 0 ; i-- ) { int teckenVärde = text[i] - 48; tal += teckenVärde * positionsVärde; positionsVärde *= 10; } return tal; }

Övning 1.1 Strängar till heltal

a) Exempel 1.1 stödjer inte negativa tal. Skriv ett program som omvandlar text till heltal och som klarar att hantera negativa tal.

b) Exemplet omvandlar alla textsträngar till tal, även om de har andra tecken än siffror. Lägg till en kontroll som genererar en InvalidCastException om strängen innehåller ett tecken som inte är en siffra.

c) Fortsätt på samma program, men ersätt summa.ToString() med din egen algoritm som räknar ut strängen utifrån heltalet.

1.3

Operatorer

De grundläggande operationerna i ett program utgörs av minnesoperationer (reservera minne och tilldela värden), beräkningar med de fyra räknesätten, jämförelser av värden och logiska operationer på sanningsvärden. Tabellen nedan beskriver de vanligaste operatorerna i prioritetsordning, med högst prioritet överst.

SymbolNamn

() parenteser

++ tillväxt (postfix) minskning (postfix)

! ++ ICKE tillväxt (prefix) minskning (prefix)

Beskrivning

Operationerna i parentesen utförs först.

Ökar operanden till vänster med 1. Minskar operanden till vänster med 1,

Ger false om operanden är true och tvärtom. Ökar operanden till höger med 1. Minskar operanden till höger med 1.

(typ) typomvandlingar Kopierar ett värde till en ny datatyp.

* / % multiplikation division modulus

+addition subtraktion

< <= > >= mindre än mindre än eller lika med större än större än eller lika med

== != lika med inte lika med

& Bitvis OCH

^ Bitvis antingen ELLER

| Bitvis ELLER

&& Logiskt OCH

|| Logiskt ELLER

Ger produkten av operanderna. Ger kvoten av operanderna.

Ger heltalsresten av operanderna.

Ger summan av operanderna.

Ger differensen av operanderna.

Jämför operanderna, ger true eller false.

Jämför operanderna, ger true eller false.

Jämför operanderna bit för bit, två ettor blir 1 i resultatet.

Jämför operanderna bit för bit, lika bitar blir 1 i resultatet.

Jämför operanderna bit för bit, två nollor blir 0 i resultatet.

Ger true om båda operander är true.

Ger true om minst en operand är true.

? : VillkorsoperatorExempel: villkor ? värde om sant : värde om falskt.

= *= /= %=

-= tilldela multiplicera och tilldela dividera och tilldela modulus och tilldela addera och tilldela subtrahera och tilldela

Kopierar ett värde från högra till vänstra operanden. Multiplicerar, adderar etc. operanderna. Svaret tilldelas vänstra operanden.

Några av operatorerna är kanske okända för dig. Bitoperatorerna &, ^ och | används för att manipulera enskilda bitar i ett heltal. Se avsnittet om enumerationer och flaggor för att lära dig mer om dem.

Kompilatorn utvärderar uttryck i källkod enligt en sträng prioriteringsordning. Vad får variabeln träff för värde nedan?

int vänster = 10, topp = 10, bredd = 20, höjd = 5; int x = 20, y = 20;

bool träff = (vänster <= x && x <= vänster+bredd ) && ( topp <= y && y <= topp + höjd );

1.4 Styrstrukturer

Att styra programflödet, det vill säga den ordning som satserna utförs, görs av styrstrukturer. Två grundläggande styrstrukturer är selektion och iteration. Selektion innebär att exekvera vissa satser bara om något är sant, annars hoppas de över. Det åstadkoms med if-satser. Iteration innebär att exekvera samma satser upprepade gånger i en loop. I C# finns looparna for, while och do-while

Ett exempel där både selektion och iteration används är en vanlig sekventiell sökning efter ett visst värde i en samling av värden. Elementen i samlingen jämförs i tur och ordning med det sökta värdet.

string text = "Per Olsson han hade en bonnagård, lian lian lej"; int index = -1, i = 0; while ( i < text.Length )

{ if ( text[i] == ',' ) index = i; i++;

}

// Det sökta värdet (kommatecknet) finns på den plats som index anger.

Fundera på följande:

G Vad blir index om man istället hade sökt efter ett mellanslag?

G Om man hade sökt efter ’z’?

Foreach-loopen

När ett program innehåller ett fält, en lista eller någon annan samling är det väldigt vanligt att man vill man loopa igenom alla elementen, till exempel vid en sökning som ovan. Loopar med räknare och variabler med hakparenteser och index ser dock lite stökiga ut. Det finns en snyggare loop som heter foreach.

string[] kompisar = { "Peter", "Johannes", "Markus", "Mattias" }; bool markusÄrMinKompis = false;

foreach ( string kompis in kompisar )

{ if ( kompis == "Markus" ) markusÄrMinKompis = true; }

Foreach-loopen hämtar värden i tur och ordning från fältet kompisar till variabeln kompis. I varje iteration jämförs den sökta strängen "Markus" med värdet som kompis har i just den iterationen.

Det är viktigt att förstå att loopen alltid itererar lika många gånger som det finns element i fältet (om man inte avbryter den med break), och man kan inte ändra värde på iterationsvariabeln (kompis i det här fallet) medan loopen kör. Därför fungerar foreach-loopen inte i alla sammanhang, till exempel i vissa sorteringsalgoritmer.

1.5 Samlingar

En samling (collection) är en grupp värden som har ett gemensamt namn i källkoden.

Den mest grundläggande typen av samling är ett fält (array).

int[] serie = new int[3];

Deklarationen ovan skapar tre heltal som tillsammans heter serie. Värdena i fältet kallas för element och är numrerade med index från 0 och upp. De enskilda elementen är i sig vanliga variabler, med den skillnaden att deras namn består av fältets namn följt av ett index i en hakparentes.

De två exemplen nedan utför samma arbete:

int[] tal = { 1, 2, 3 }; int summa = tal[0] + tal[1] + tal[2]

int tal1 = 1, tal2 = 2, tal3 = 3; int summa = tal1 + tal2 + tal3;

Fält är värdefulla när man har att göra med större mängder värden, eftersom man kan utföra operationer på elementen ett i taget i en loop.

Värdetyper och referenstyper

int, float, char och alla andra typer ovan, förutom string, är så kallade värdetyper När en tilldelning sker med en värdetyp kopieras själva värdet.

int talA = 4; int talB = talA; talB = 5; // talA är fortfarande 4.

De två första satserna ovan resulterar i två heltal i arbetsminnet med samma innehåll, men det är ändå två olika platser. Om man senare ändrar i talB (sista raden) påverkar det inte värdet i talA.

Ett fält (en array) är å andra sidan en referenstyp. Vid tilldelningar med fält kopieras inte fältets innehåll, utan bara dess namn.

int[] serieA = { 1, 2, 3 };

int[] serieB = serieA;

Satserna resulterar i en följd av heltal i arbetsminnet, som har två namn (referenser) i källkoden. Man kan ändra det första talet i fältet till 7 antingen genom att skriva serieA[0] = 7 eller serieB[0] = 7. Namnen serieA och serieB kallas referensvariabler.

Referenstyper kan ha värdet null. Det betyder att du kan deklarera referensvariabler utan att ange innehåll till arbetsminnet:

int[] serieC; // Jömförelsen serieC == null skulle bli true. serieC[0] = 2 // Kraschar programmet, för fältet har inga element.

Arbetsminne till referensvariabler reserveras i allmänhet med nyckelordet new. För fält kan new utelämnas i satser som initialiserar fältet. Nedan ser du tre satser som skapar tre likadana fält.

int[] serieD = new int[3];

int[] serieE = new int[3] { 0, 0, 0 };

int[] serieF = { 0, 0, 0 };

Regelbunda fält

Tvådimensionella fält är fält där varje element har två index. Det är naturligt att tänka på ett tvådimensionellt fält som en tabell. Tabeller är ett mycket praktiskt sätt att organisera information på. Vi ser gärna tågtider, klasslistor, tävlingsresultat och så vidare i tabellform.

Nedan syns ett exempel på hur man deklarerar en tabell för 15 heltal i tre rader och fem kolumner.

int[,] tal = new int[3, 5];

Det första talet heter tal[0,0] och det sista heter tal[2,4]. Den här typen av fält kallas för regelbundna fält eftersom varje rad har lika många kolumner.

tal[0,0]tal[0,1]tal[0,2]tal[0,3]tal[0,4]

tal[1,0]tal[1,1]tal[1,2]tal[1,3]tal[1,4]

tal[2,0]tal[2,1]tal[2,2]tal[2,3]tal[2,4]

Man kan tänka på det första indexet som radnummer och det andra som kolumnnummer. Observera att man lika gärna skulle kunna visualisera fältet som en tabell med 3 kolumner och 5 rader. I så fall får man tänka på det första indext som kolumnnummer och det andra som radnummer.

Fundera på detta:

G Hur skulle du skriva för att få 12 flyttal i fyra rader och tre kolumner?

Om man vill initalisera ett tvådimensionellt fält med literaler ser det ut som nedan.

int[,] tabell = { { 1, 2 } , { 3, 4 } }; // Två rader och två kolumner.

För att utföra operationer på alla element i ett tvådimensionellt fält är det vanligt att använda nästlade loopar. Kan du lista ut vad det tvådimensionella fältet nedan innehåller för värden när looparna exekverats?

int[,] plutifikationsTabellen = new int[10,10]; for ( int r = 0 ; r < 10 ; r++ ) { for ( int c = 0 ; c < 10 ; c++ ) { plutifikationsTabellen[r, c] = (r+1)*(c+1); } }

Man behöver inte nöja sig med två dimensioner. Man kan göra tre eller fler dimensioner också. Hur många element skapas av satsen nedan, tror du?

double[,,] tabell3D = new double[4,2,3];

Övning 1.2

Tvådimensionella fält

Skriv ett program där man kan visa och ändra värden i ett tvådimensionellt fält av strängar med 5 rader och 3 kolumner, på enklaste vis. Man skriver helt enkelt radindex och kolumnindex och vilket värde man vill ha. Tabellen visas i en TextBox, som uppdateras i en dubbel for-loop varje gång man ändrar ett värde.

Tabellen blir kanske inte jättesnygg eftersom tabavståndet inte anpassar sig till längden på orden, men det spelar mindre roll just nu. Om det stör dig kan du söka efter andra alternativ på internet för att visa data i tabellformat, till exempel ListView eller DataGridView.

Oregelbundna fält

Oregelbunda fält är fält av fält. Exemplet nedan visar ett tvådimensionellt oregelbundet fält.

int[] serieA = { 1, 2, 3, 4 }; int[] serieB = { 1, 2 }; int[] serieC = { 1, 2, 3 }; int[][] fält = { serieA, serieB, serieC }; // Ett oregelbundet fält.

Fältet kallas oregelbundet eftersom raderna kan ha olika antal kolumner. Elementens index anges i hakparenteser på följande sätt.

fält[0][0]fält[0][1]fält[0][2]fält[0][3] fält[1][0]fält[1][1]

fält[2][0]fält[2][1]fält[2][2]

Fundera på detta:

G Vilket värde har fält[0][1] ?

G Vilket värde har fält[1].Length ?

G Vad är fält[2] för något? Vilken datatyp har det?

1.6 Andra datastrukturer

Fält är ett sätt att lagra flera variabler med en gemensam referens. Alla variablerna måste ha samma datatyp. Ett annat sätt att lagra flera variabler med ett gemensamt namn är med hjälp av en klass. I klassens objekt kan det ingå variabler med olika datatyp. Först beskrivs vilka variabler som ingår i en klass. Sedan konstruerar man objektet med hjälp av nyckelordet new

Exemplet visar hur man kan skapa två objekt med vardera en sträng och ett heltal i arbetsminnet.

class GatuAdress { public string Gata; public int Nummer; }

GatuAdress adress1 = new GatuAdress();

GatuAdress adress2 = new GatuAdress() { Gata="Storgatan", Nummer=1 };

Klassen GatuAdress definierar hur en plats i arbetsminnet ska se ut för att lagra en gatuadress. De två sista raderna är det som faktiskt reserverar plats i arbetsminnet för två gatuadress-objekt. Den sista raden initialiserar dessutom variablerna i det andra objektet.

En klass är en referenstyp. Betrakta nedanstående kodsnutt, som använder klassen GatuAdress, och figuren som schematiskt visar objekten som ovaler och vilken referensvariabel som identifierar vilket objekt.

GatuAdress adress1 = new GatuAdress();

GatuAdress adress2 = new GatuAdress();

adress1 adress2

Kodsnutten fortsätter med följande två satser. Rita figuren på en bit papper, och fundera på hur den ändras när dessa satser exekveras.

GatuAdress adress3 = adress2; adress2 = adress1;

Du får en referensvariabel och pil till, och en av de gamla pilarna ändras. Vad skulle hända om du fortsatte och exekverade följande sats?

adress3 = adress1;

Jo, du skulle få ett objekt som det inte gick någon pil till. Alla tre referenserna ”pekar på” det första objektet. Det andra objektet ”glöms bort”. Bortglömda objekt raderas automatiskt från arbetsminnet av kod som byggs in i ditt program, men som du aldrig ser i källkoden. Mekansimen kallas Garbage Collection, och gör att moderna program har mindre minnesläckor än gamla program.

Minnesläckor uppstår när arbetsminnet blir upptaget av ”bortglömda” objekt. Ju längre ett läckande program kör, ju fler objekt skapas och glöms bort, och eftersom arbetsminnet är begränsat uppstår till slut prestandaproblem.

En annan typ av datastruktur heter struct. Definitionen av en struct liknar en klass.

struct Punkt { public int X; public int Y; }

En struct är till skillnad från en klass en värdetyp. Det betyder att en struct-variabel representerar en instans av structens data, och all data kopieras vid till exempel tilldelning.

Punkt p; p.X = 10; p.Y = 20;

Punkt q = p;

Den översta raden skapar en instans med två heltal.

Man kan skriva Punkt p = new Punkt(), men det gör ingen skillnad.

Den sista raden skapar en datastruktur med två heltal och kopierar dit heltalen från p. q är namnet på en egen instans, inte en adress till samma datastruktur som p. En struct kan ha konstruktor och metoder precis som en klass, men de är främst avsedda att lagra enkla datastrukturer (några få variabler) som till exempel ovan. Om det blir mycket kod i en struct, är det förmodligen bättre att göra den till en klass.

Exempel 1.2 Fält med objekt

Programmet skapar en rad med bilder i ett fönster. Istället för att lägga till en massa PictureBox-kontroller från ToolBox i designvyn, skapas kontrollerna i en loop. När man klickar på en bild sätts en svart ram runt den bilden.

1 publicpartialclass Form01_01 : Form

2 {

3 public Form01_01( )

4 {

5 InitializeComponent();

6 // Skapa ett fält med PictureBox-kontroller.

7 PictureBox[] bilder = new PictureBox[5];

8 // Position och dimension för första bildrutan.

9 int x = 0, y = 0, bredd = 50, höjd = 50;

10 // Ställ in kontrollerna och lägg till i fönstret.

11 for (int i = 0 ; i < bilder.Length ; i++ )

12 {

13 bilder[i] = new PictureBox();

14 bilder[i].Left = x;

15 bilder[i].Top = y;

16 bilder[i].Width = bredd;

17 bilder[i].Height = höjd;

18 bilder[i].BackgroundImage = Image.FromFile( "Blomma.png" );

19 bilder[i].Click += pbxBlomma_Click;

20

21 this.Controls.Add( bilder[i] );

22 x += bredd;

23 }

24 }

25 privatevoid pbxBlomma_Click ( object sender, EventArgs e )

26 {

27 PictureBox klickadBlomma = (PictureBox)sender;

28 klickadBlomma.BorderStyle = BorderStyle.FixedSingle;

29 }

30 }

PictureBox-objekten skapas i loopen på rad 13. Det går inte att använda en foreachloop här, eftersom elementen ska tilldelas nya värden.

Loopen kommer att iterera 5 gånger, och alltså kommer programmet att skapa 5 objekt av typen PictureBox i arbetsminnet.

På rad 19 kopplas bildernas klickhändelser ihop med metoden pbxBlomma_Click.

Det innebär att alla bildrutor kommer att ”trigga” samma metod när man klickar på dem. Metodens parameter sender refererar dock till den klickade kontrollen, så att vi kan använda den för att sätta kantlinjen (rad 28). Eftersom sender har typen object måste vi göra en typomvandling till PictureBox först (rad 27).

På rad 21 läggs bildrutorna till i fönstrets lista över kontroller. Närmare bestämt är det bildrutornas referenser som kopieras till listan Controls. I programmet finns alltså två referenser till varje bilruta, en i fältet bilder och en i listan Controls.

Övning 1.3 Tvådimensionellt fält

Skapa ett program likt föregående exempel, där det istället finns 25 bildrutor i 5 rader och 5 kolumner. Hantera bildrutorna med hjälp av ett tvådimensionellt oregelbundet fält av typen PictureBox[][]. Använd en dubbel loop för att iterera igenom fältet och skapa bildrutor i ett rutnät liknande en tabell.

PictureBox[,] bilder = new PictureBox[5,5]; for ( int r = 0 ; r < 5 ; r++ ) { for ( int c = 0 ; c < 5 ; c++ ) { bild[r,c] = new PictureBox(); // och så vidare... } }

1.7 Enumerationer

Tänk dig situationen att en klass som beskriver klädesplagg ska innehålla information om plaggets storlek. Storlekarna är Small, Medium, Large och ExtraLarge. Man skulle kunna lagra informationen som strängar:

string storlek = "Medium";

Det är dock inte optimalt att lagra den här informationen som en sträng. Strängar använder 2 byte per tecken, vilket är onödigt mycket, och dessutom måste processorn jobba sig igenom en loop varje gång två strängar jämförs med varandra (eftersom strängar jämförs tecken för tecken).

Det vore bättre att lagra storleken som ett heltal, och bestämma att 0 betyder small, 1 betyder medium, och så vidare. Man kan dessutom deklarera konstanter med beskrivande namn, så att koden blir lättläst. (Konstanter namnges av gammal hävd med stora bokstäver.)

const int SMALL = 0; const int MEDIUM = 1; const int LARGE = 2; const int EXTRALARGE = 3; int storlek = MEDIUM;

Konstanterna upptar inget minne i objekten, för de är bara namn på värden i källkoden som kompilatorn använder för att klistra in tal i maskinkoden. I exemplet ovan kommer kompilatorn att byta ut MEDIUM i sista raden mot 1 innan exe-filen skapas. De översta fyra raderna hamnar inte i maskinkoden alls. Varje klädesplagg behöver nu bara 4 byte för storleken på plagget. Det gör skillnad för SuperMegaModeKedjan AB som har 40 000 plagg i sitt sortiment.

Det finns tyvärr en svaghet kvar även med heltal. Man skulle ju av misstag kunna ge variabeln storlek vilket värde som helst, även ett som saknar betydelse.

int storlek = 54; // Skostorlek, eller???

Därför finns det ett ännu bättre alternativ till att använda konstanter, nämligen enumerationer. En enumeration är egentligen bara en uppräkning av konstanter, som ovan, men hanteras som en egen datatyp på så sätt att man kan deklarera en enumerationsvariabel, och den kan bara innehålla värden från enumerationen.

enum Storlek { Small, Medium, Large, ExtraLarge } Storlek storlek = Storlek.Medium;

Observera att Storlek är en ny, egendefinierad datatyp. Av de två raderna ovan måste den översta stå i ett klassblock, medan den understa kan stå i klassblocket eller lokalt i en metod.

Storlek.Small är egentligen ett heltal, liksom Storlek.Medium och så vidare. Man kan därför omvandla en enum-typ till och från int.

int somHeltal = (int)storlek; // Ger värdet 1 om storlek är Storlek.Medium.

Man kan också i definitionen tala om vilka konstanter man vill använda, om det skulle spela någon roll.

enum Storlek { Small = 10, Medium = 20, Large = 30, ExtraLarge = 40 }

Flaggor

Antag att vi vill spara information om stilen för ett stycke text, till exempel om det är fetstil, kursiv stil eller understruket. Texten kan ha alla stilar samtidigt, eller bara en, eller villken kombination som helst. Man skulle då kunna lagra en fet och kursiv textstil med hjälp av följande variabler.

bool fetstil = true; bool kursiv = true; bool understruket = false;

Men egentligen går det åt onödigt mycket minne för att spara så lite information. Man skulle kunna lagra samma information mycket effektivare med bara en byte, om man bestämde att en av bitarna står för fetstil, en annan för kursiv och en tredje för understruket.

00000110

Används ejAnvänds ejAnvänds ejAnvänds ejAnvänds ej Fetstil Kursiv Understruket

Bitarna kallas i det här sammanhanget flaggor, och om biten för fetstil är 1, säger man att flaggan för fetstil är satt (till ett). I figuren ovan är flaggan för kursiv stil också satt. Samma kursiva fetstil som lagrades med boolean-variabler ovan skulle då kunna lagras som flaggor med satsen:

byte stil = 6; // 0000 0110 binärt.

Andra stilar skulle kunna se ut så här:

byte baraFet = 4; // 0000 0100 binärt.

byte baraKursiv = 2; // 0000 0010 binärt.

byte baraUnderstruket = 1; // 0000 0001 binärt.

byte reguljärStil = 0; // 0000 0000 binärt.

byte allaStilar = 7; // 0000 0111 binärt.

Det blir förstås knökigt att hålla reda på vilken siffra som står för vad, och framför allt kan källkoden bli svårtolkad. I det här läget tar vi istället till en sorts enumeration för flaggor, som gör att vi kan definiera ett namn för varje flagga.

[flags]

enum Stil { Reguljär, Understruket, Fetstil, Kursiv }

Enumerationen är en int, inte en byte, så man kan teoretiskt ha upp till 32 flaggor. Enumerationen skulle kunna användas på följande sätt:

Stil baraFet = Stil.Fetstil;

Stil baraKursiv = Stil.Kursiv;

Stil baraUnderstruket = Stil.Understruket; Stil reguljärStil = Stil.Reguljär;

Bitoperatorer

Med flaggor är det enkelt att kombinera grundstilarna på vilket sätt som helst, med hjälp av tecknet |.

Stil fetOchKursiv = Stil.Fetstil | Stil.Kursiv; Stil allaStilar = Stil.Fetstil | Stil.Kursiv | Stil.Understruket;

Det är samma tecken som används i den logiska operatorn ELLER, fast då används det dubbelt (true || false blir true). Ett ensamt | betyder dock operatorn BITVIS ELLER. Den jämför varje bit i ett heltal med biten på samma plats i ett annat heltal, och resulterar i ett nytt heltal där biten på den platsen blir 0 om båda operanderna har 0, annars blir den 1. Man förstår det bäst med ett exempel.

Vi hugger bara två heltal ur luften, och skriver dem under varandra. 00101101 | 01001001 blir:

0010 1101 |0100 1001 blir0110 1101

Det fungerar precis som om 0 är false och 1 är true, och man gör en logisk ELLER på varje bit för sig.

När man skriver Stil.Fetstil | Stil.Kursiv får man alltså ett nytt tal där båda flaggorna är satta.

0000 0100 Fetstil |0000 0010Kursiv

blir0000 0110Fet samt Kursiv

På samma sätt som det finns en bitvis ELLER | finns det en bitvis OCH &. Den fungerar på exakt samma vis, men med regeln att 1 & 1 blir 1, medan 1 & 0, 0 & 1 samt 0 & 0 blir 0. (Precis samma regel som för && med true och false-operander).

Bitvis OCH används till exempel om man vill testa ifall en viss flagga är satt. Antag att det finns en Stil-variabel som heter stil, och vi vill veta om Fetstil-flaggan är satt. Det kan se ut så här.

// Om flaggan är satt, blir villkoret sant. if ( (stil & Stil.Fetstil) == Stil.Fetstil )

Testa gärna att ställa upp ett eget exempel med talen i binär form under varandra, och försök lista ut hur det fungerar.

Exempel 1.3 FontStyle

Enumerationer, inklusiver flaggor, finns det flera av i .NET. Till exempel har etiketter (Label), textrutor (TextBox) och många andra kontroller som visar text en egenskap som heter Font. Den innehåller i sin tur en egenskap som heter FontStyle, som är en enumeration liknande den vi beskrivit här. Man kan till exempel byta font i en etikett som heter lblInfo med satserna

lblInfo.Font = new Font( "Arial", 10, FontStyle.Bold | FontStyle.Italic );

Då kommer texten i etiketten att visas i typsnittet Arial, storlek 10, med fet och kursiv stil. Programmet nedan är ett litet gränssnitt för att förhandsgranska en viss font.

private void btnOK_Click ( object sender, EventArgs e ) { FontStyle stil = FontStyle.Regular; // Inga flaggor satta.

// Sätt fetstil-flaggan. if ( cbxFet.Checked ) stil = stil | FontStyle.Bold; // Sätt kursiv-flaggan. if ( cbxKursiv.Checked ) stil = stil | FontStyle.Italic;

Font font = new Font( tbxTeckensnitt.Text, 10, stil ); tbxFörhandsGranskning.Font = font; }

Övning 1.4 Förhandsgranskning av typsnitt

Skriv ett eget program för att förhandsgranska typsnitt som användaren anger, likt exemplet. Man ska dock kunna ange mer information om typsnittet, till exempel storleken och om det ska vara understruket. Sök gärna upp dokumentationen över FontStyle på internet för att se alla stilar som stöds, och försök att använda dem i ditt program.

1.8 Metoder

En metod är ett kodblock som kan anropas. Särskilda variabler som kallas parametrar kan utgöra indata till metoden, och den kan returnera ett värde till den anropande satsen. Kodblocket identiferas av metodens namn och parameterlista. Alla metoder har följande mönster.

typ namn ( typ parameter1, typ parameter 2,… ) { satser; return…; }

typ avser datatyp, och kan vara int, string eller vilken typ som helst, inklusive klasser. Först står metodens returtyp. Om metoden inte ska returnera ett resultat anges returtypen till void. Efter returtypen står metodens namn, och därefter en parentes med parameterlista. Parameterlistan kan vara tom, men parentesen måste finnas med ändå. Metodens namn och parameterlista måste vara unika i sin klass. Namnet och parameterlistan tillsammans kallas för metodens signatur.

Överlagring

Lägg märke till att namnet ensamt inte behöver vara unikt. Om man har två eller fler metoder med samma namn men olika parameterlista (alltså olika signaturer) sägs de vara överlagrade (overloaded). Samma klass kan innehålla båda metoderna nedan.

double avrunda ( double tal ) { return (int)(tal + 0.5); } double avrunda ( double tal, double pos ) { return ( (int)(tal / pos + 0.5) * pos) ); }

Metoderna har olika signatur, även om de har samma namn, och det räcker för att det ska gå att anropa dem:

double pi = 3.1415926535897932384626433832795; double avrundatPi = avrunda( pi, 0.01 ); // Ger 3.14

Kompilatorn kan avgöra med parameterlistan vilken av avrunda-metoderna som ska användas.

Frivilliga parametrar

Ett alternativ till att överlagra metoder är ibland att ange förvalda parametervärden. Metoden nedan skulle kunna ersätta båda avrunda-metoderna ovan.

double avrunda ( double tal, double pos = 1 ) { return ( (int)(tal / pos + 0.5) * pos) ); }

Den första parametern är obligatorisk. Metoden måste ju ha ett tal som indata för att kunna avrunda det. Den andra parametern är emellertid valfri i anropssatsen. Om man utelämnar den används det förvalda värdet, i det här fallet 1. Metoden avrunda kan alltså anropas med endera av

double a = avrunda( pi );

// Metoden använder pos = 1. a blir 3.

double b = avrunda( pi, 0.1 );

// Metoden använder pos = 0.1. b blir 3.1

För att inga missförstånd ska kunna uppstå kräver kompilatorn att valfria parametrar står efter obligatoriska i parameterlistan.

Namnade parametrar

Normalt anropas en metod med parametrarna i den ordning som metodens definition anger, men det är möjligt att ange parametrarna i en annan ordning, om man identifierar dem med namn.

Metoden

string helaNamnet ( string förNamn, string efterNamn ) { return förNamn + " " + efterNamn; }

kan anropas på följande sätt:

string a = helaNamnet( "Ann", "Karlsson" );

string b = helaNamnet( förNamn: "Ann", efterNamn: "Karlsson" );

string c = helaNamnet( efterNamn: "Karlsson", förNamn: "Ann" );

Det första anropet identifierar parametervärdena med hjälp av position. De följande två identifierar dem med respektive namn följt av ett kolontecken. Parameternamnen gör att man kan skriva värdena i vilken ordning man vill.

Det finns en syntax för att låta en metod returnera flera variabler. När man anropar en sådan metod kan det se ut som nedan.

(int hela, int rest, int delare) = BråkTillBlandadForm( 10, 3 );

Till vänster om likhetstecknet deklareras tre variabler i en parentes, som tar emot tre värden från metodanropet till höger. Den här metoden omvandlar bråktalet 10/3 till blandad form, det vill säga 3⅓. Vi kallar dessa tre siffror hela, rest och delare i koden för metodens definition nedan.

(int, int, int) BråkTillBlandadForm( int täljare, int nämnare ) { int hela = täljare / nämnare; int rest = täljare % nämnare; int delare = nämnare;

return (hela,rest,delare); }

Det här är användbart som alternativ till out-parametrar eller ref-parametrar och ser snyggare ut. Syntaxen är en naturlig utvidgning av hur metoder med en enkel returtyp ser ut. Man skriver bara en parentes med flera typer och kommatecken emellan som returtyp. Det som returneras i metoden är sedan detta antal värden inom en parentes och med kommatecken emellan.

Ordningen är viktig. Variablerna som tar emot värdena vid anroptet måste stå i samma ordning i sin parentes, som värdena i den returnerade parentesen. I det här fallet är alla heltal, men det går bra att returnera (int,double,bool) eller (int,float) eller någon annan kombination.

Egentligen kan en metod bara returnera ett värde. Kompilatorn löser det genom att ”packa ihop” flera variabler till ett objekt, returnera objektet och sedan ”packa upp” det till de olika delarna. Ett sådant paket av värden kallas i det här fallet en tuppel. En tuppel implementeras av en klass, men i det här fallet sköter kompilatorn allt sådant under ytan.

Övning 1.5

Skriv ett program som kan tala om vilket av några tal som är minst, och vilket som är störst. Användaren kan lägga till tal med en textruta och en knapp. Programmet lagrar talen i en listbox. Det finns också en knapp för att hitta största och minsta värdet. Värdena i listboxen kopieras till ett heltalsfält och skickas som parameter till en metod, som returnerar både minsta och största värdet.

Observera att värdena i listboxen måste typomvandlas till int i loopen som skapar heltalsfältet:

tal[ i ] = (int) lbxSerie.Items[ i ]; // i är ett index

1.9 Klasser

Klasser är kodblock som kan innehålla data (variabler) och körbar kod. Klassen måste inte innehålla variabler, men oftast finns en eller flera variabler som tillsammans beskriver någonting, till exempel en person, ett fordon, en maträtt eller vad som helst. Klasser har därför vanligen namn som är substantiv (till exempel Person, Fordon, Maträtt).

Med hjälp av klassen skapar man objekt i arbetsminnet med den givna uppsättningen variabler, men med egna värden för varje objekt. Objektet utgör en instans av klassen. Klassen fungerar som en datatyp, medan objekten är olika värden av den typen, på samma sätt som int är en datatyp, och det kan finnas många instanser (värden) av typen int i arbetsminnet. Instans och objekt är mer eller mindre synonymer.

Varje objekt identifieras av en referens, som är minnesadressen till objektet i arbetsminnet. Det är viktigt att förstå att referensen är inte objektet, utan bara dess plats. Om man kopierar en referens får man inte en ny instans, utan bara en ny referens som identiferar samma plats i arbetsminnet.

Klassens metoder är anropbara och definierar klassens körbara kod. Metoderna exekverar normalt på en instans av klassen. Det vill säga, metoderna har tillgång till värdena i ett visst objekt. Man måste först skapa ett objekt med sin egen uppsättning variabler, och sedan använda objektets referens för att anropa metoderna:

string namn1 = "Pelle";

string namn2 = "Lisa";

string namn3 = namn2.Replace( 's', 'n' ); // Replace kör på objektet i namn2.

Klassen har en (eller flera) konstruktorer, som är en metod vars syfte är att initialisera variablerna i ett nytt objekt, när de skapas i arbetsminnet. Ofta finns även egenskaper, som egentligen är metoder de också, men som definieras med en lite annorlunda syntax i källkoden. En typisk klassdefinition följer detta mönster:

class JämntTal

{ // Medlemsdata, det vill säga en eller flera variabler. private int tal;

// Standardkonstruktor. Om man inte skriver en själv skapar // kompilatorn en som ger alla variabler värdet 0. public JämntTal ( ) { this.Tal = 0; }

// Egenskap. Ger åtkomst till den privata medlemsvariabeln. public int Tal

{ get { return tal; }

set { if ( value % 2 == 0 ) tal = value; } }

// Metod. Utför operationer på instansens variabler. public void Öka ( ) { tal += 2; } }

Kopieringskonstruktor

Ibland är det smidigt att kunna skapa en kopia av ett objekt, till exempel om objektet innehåller något som redigeras av användaren, och man vill spara olika versioner av objektet så att det går att ”ångra” en redigering. I så fall kan det vara smidigt med en kopieringskonstruktor. Nedan följer en variant av klassen JämntTal som har en kopieringskonstrutor.

class JämntTal

{ // Medlemsdata private int tal;

// Standardkonstruktor

public JämntHeltal ( ) { this.Tal = 0; }

// Kopieringskonstruktor

public JämntTal ( JämntTal original ) { this.tal = original.tal; }

// Egenskap public int Tal

{ get { return tal; } set { if ( value % 2 == 0 ) tal = value; }

// Metod public void Öka ( ) { tal += 2; } }

Förenklad syntax

Det finns en förkortad syntax för klassdefinitioner som är användbar för att snabbt beskriva små enkla klasser.

Förkortad syntax Fullständig syntax

class Konto { public string Namn { get; set; } public string Lösen { get; set; } }

class Konto { private string namn; private string lösen; public string Namn { get { return namn; } set { namn = value; } } public string Lösen { get { return lösen; } set { lösen = value; } } }

De båda klassdefinitionerna ovan genererar samma maskinkod. Med hjälp av den förkortade syntaxen kan kompilatorn själv lista ut resten.

Det finns också en syntax (object initializer syntax) för att initialisera ett objekt även om det inte har någon konstruktor som tar lämpliga parametrar. Den alternativa syntaxen ser du till höger nedan. Det går bara att initialisera sådant som är public, därför används klassens egenskaper Namn och Lösen, istället för de privata medlemmarna namn och lösen. Det måste finnas en standardkonstruktor.

Vanlig syntax Alternativ syntax

Konto nyttKonto = new Konto(); nyttKonto.Namn = "Alrik"; nyttKonto.Lösen = "abc123";

Konto nyttKonto = new Konto { Namn = "Alrik", Lösen = "abc123"; };

Statiska metoder

Metoder exekveras normalt mot en instans av en klass. Det vill säga, för att anropa en metod måste det finnas ett objekt och en referens till objektet. Ta till exempel klassen JämntTal på förra sidan. För att anropa Öka måste man först ha skapat ett objekt.

JämntTal jtal = new JämntTal { Tal = 8 }; // Skapa objektet. jtal.Öka(); // Öka talet i objektet.

Det går emellertid att definiera statiska metoder, som inte anropas med hjälp av en referens, utan med hjälp av själva klassnamnet.

class JämntTal { // som innan och...

// Ordet static gör detta till en statisk metod. public static bool ÄrJämnt( int tal ) { if ( tal % 2 == 0 ) return true; else return false; } }

Metoden ÄrJämnt kan vara bra att anropa innan man skapar ett objekt, för att kontrollera att ett tal verkligen är ett jämnt heltal, innan man ger det till ett objekt. Antag att det finns en textruta i ett program som heter tbxTal.

JämntTal jtal; int tal = int.Parse( tbxTal.Text ); if ( JämntTal.ÄrJämnt( tal ) ) { tal = new JämntTal { Tal = tal; } } else MessageBox.Show( "Talet är inte jämnt." );

Anropet JämntTal.ÄrJämnt( tal ) sker med klassnamnet innan punkten, alltså på formen Klass.Metod(). Lägg märket till att den statiska metoden inte har tillgång till instansvariabeln tal i klassen, eftersom klassen ensamt inte skapar minnesutrymme för variabler. Det skulle inte gå att skriva static framför metoden Öka.

Statiska variabler

Det går att reservera minnesplats för en variabel som tillhör klassen och inte något objekt. Antag till exempel att vi vill kunna bestämma om 0 räknas som jämnt tal eller inte.

class JämntTal { public static bool TillåtNoll = true; // ...och allt det andra. }

Den här informationen passar bra att lagra som en statisk variabel, om vi antar att samma regel ska gälla alla objekt. Antingen tillåts 0 för alla JämntTal-objekt, eller för inga. Det vore onödigt att använda två byte (för en boolean) i varje objekt, om de ändå ska ha samma värde allihopa.

Exempel 1.4 Användarkonton

Design

Användaren anger ett namn och ett lösenord, och klickar på registrera eller logga in. Om man loggar in med ett tidigare registrerat namn och lösenord visas en stor välkomsttext.

Implementering

Ett konto med namn och lösen hanteras av en klass Konto. Klassen ska ha en egenskap för att hämta namnet, och en statisk metod för att kontrollera nya lösenord, så att de innehåller minst 8 tecken. Fönstret lagrar registrerade konton i en List<Konto>-lista.

Koden för programmet ser du nedan.

public partial class Form1 : Form

{ private List<Konto> konton = new List<Konto>();

public Form1( )...

private void btnRegistrera_Click( ... )

{ if ( Konto.GodkännLösen( tbxLösenord.Text )

{ Konto nyttKonto = new Konto( tbxAnvändare.Text, tbxLösenord.Text ); konton.Add( nyttKonto ); lbxAnvändare.Items.Add( nyttKonto );

} else MesssageBox.Show( "Lösenordet måste innehålla minst 8 tecken" );

}

private void btnLoggaIn_Click( ... )

{ foreach ( Konto konto in konton )

{ if ( konto.Namn == tbxAnvändare.Text && konto.Lösen == tbxLösenord.Text )

{ lblVälkommen.Text = "Välkommen\r\n" + konto.Namn; lblVälkommen.Visible = true; } } } }

public class Konto

{ private string namn; private string lösen;

public Konto ( string namn, string lösen )

{ this.namn = namn; this.lösen = lösen; }

public string Namn { get { return namn; } }

public string Lösen { get { return lösen; } }

public static bool GodkännLösen ( string lösen ) { if ( lösen.Length >= 8 ) return true; else return false; }

public override string ToString( ) { return namn; } }

Peka ut så många av begreppen som möjligt i koden ovan.

Deklaration

Klass

Variabel

Egenskap

Konstruktor

Metod

Parameter

Returtyp

Returvärde Iteration Selektion Anrop

Operator Index Objekt Referens

Lägg märke till att alla Konto-referenser lagras på två ställen i programmet, dels i listan konton och dels i listrutans Items. Det kan tyckas onödigt, men det är en bra princip att skilja på fönstrets innehåll och den underliggande datan. Den underliggande datan är användarens dokument, medan fönstrets innehåll är vyn över dokumentet, och ska ibland kunna visa dokumentet på olika sätt.

Klassdiagram

När man designar eller samarbetar kring ett projekt är det av god nytta att kunna beskriva en klass på ett kort och överskådligt sätt. Ett klassdiagram är ett sådant sätt, och det är oberoende av vilket programspråk man använder. Klassdiagramet beskriver klassen, men man kan implementera den i C#, C++, Java eller vilket språk man vill som stödjer klasser. Konto-klassen i förra exemplet skulle kunna se ut så här:

Konto

− namn − lösen : string : string

+ Konto( namn :string, lösen :string ) + Namn :string + GodkännLösen( lösen :string ) + ToString( ) : string : bool : string

Det här diagrammet kallas UML-diagram (Unified Modeling Language) och består av tre rutor. Den översta rutan visar klassens namn. Den mellersta rutan visar datamedlemmarna. Varje datamedlem anges med dess namn följt av datatypen bakom ett kolon.

Den understa rutan visar de körbara kodblocken, medlemsmetoderna, med namn, parameterlista och returtyp längst till höger. En understruken metod betyder att den är statisk.

Tecknen framför varje rad anger om medlemmen är privat eller publik. Ett minustecken betyder privat, ett plustecken betyder publik. Det finns en tredje variant, stakettecknet # som betyder protected. Den kommer till användning i klasshierarkier, som tas upp i nästa kapitel.

Övning 1.6 Fordonsregister

Design

Skriv ett program som kan registrera fordon. Det ska inte gå att registrera två fordon med samma registeringsnummer. Om man försöker ska ett felmeddelande visas. Det ska bara gå att använda registreringsnummer på formen ABC123, alltså med tre bokstäver följt av tre siffror. Fordonstypen ska vara bil eller motorcykel. Listrutan ska kunna visa alla fordon eller bara fordon av en viss typ.

Implementering

Fordonsdata hanteras av en klass som heter Fordon, och alla registrerade fordon (objekt) lagras i en lista (List<Fordon>). Fordonstypen lagras med hjälp av en enum-variabel (inte flaggor, för varje fordon kan bara ha en typ).

Listan i fönstret ska vara en ListBox, som kan visa alla fordon eller bara fordon av en viss typ. Typen för ett nytt fordon väljs i en rullgardinsmeny (ComboBox med egenskapen DropDown). Felmeddelanden visas med en MessageBox. Grupprutan ska ha tre radioknappar för att filtrera listan på fordonstyp. Visa Alla ska vara förvald.

− regNr − märke

− modell − fordonsTyp : string : string : string :FordonsTyp

+ Fordon( regnr :string, märke :string, modell :string, fordonsTyp :FordonsTyp)

+ RegNr + Märke

+ Modell

+ FordonsTyp

+ GodkännRegNr ( regNr :string )

+ ToString( ) : string : string : string : string : bool

Fordon

Typen dynamic

Variabler och referenser har normalt en bestämd datatyp. Att deklarera en variabel innebär att tala om vad den ska heta och vilken typ den ska ha. Anledningen till att C# lägger så stor vikt vid datatyper är att kompilatorn genom att veta lite om avsikten med ett värde kan förhindra vissa operationer som inte har någon logisk mening.

int ålder = 8; bool vänsterHänt = true;

double produkt = ålder * vänsterHänt;

En boolean representerar ett sanningsvärde. En multiplikation av ett sanningsvärde med ett tal saknar mening. Men datorn skulle rent tekniskt kunna genomföra operationen. För datorn är allting tal, även en boolean. Ändå kommer multiplikationen ovan att generera ett kompileringsfel med beskrivningen: Operator ’*’ cannot be applied to operands of type ’int’ and ’bool’.

Exemplet är kanske lite trivialt, men det illustrerar ändå en viktig aspekt av programmering. För datorn är allting tal, men en del operationer på dessa tal saknar mening. C# kallas för ett hårt typat språk, och med det menar man att kompilatorn har en massa regler för vilka operationer man får göra på olika värden, och programmeraren talar om vilka operationer som ska vara godkända genom att ange datatyper. Dessa typer kallas statiska, eftersom de är förutbestämda och kända av kompilatorn när man kompilerar programmet. Syftet är att förhindra många fel redan innan programmet blir till maskinkod.

Det finns dock lägen när man vill kunna hantera värden vars typer inte är kända vid kompileringstillfället. Därför finns det en datatyp i C# som heter dynamic. Den stora poängen med dynamic är att kompilatorn inte kontrollerar operationerna.

int ålder = 8; dynamic vänsterHänt = true;

double produkt = ålder * vänsterHänt;

Den här koden skulle kompilatorn tugga i sig och göra maskinkod av utan problem. Nu är det i och för sig så, att produkten av 8 och true fortfarande är nonsens. Därför uppstår det ett fel ändå, men det uppstår när man kör programmet, inte när det kompileras.

!Tänk på att det är skillnad på vad som är okänt för kompilatorn och vad som är okänt för dig.

Typen dynamic är användbar när man hanterar objekt vars typ är okänd för kompilatorn, till exempel i samband med generiska klasser (se nästa kapitel) eller när man ”samverkar” med extern kod. Som exempel ska vi importera ett värde från Excel. Ha dock i minnet, att i alla lägen där du kan skriva en statisk datatyp, så är det en god idé att göra det. Dynamiska typer är krävande för processorn, minskar felsäkerheten och försämrar kodens läsbarhet.

Exempel 1.5 Importera från Excel

Börja med att skapa ett alldeles nytt Windows Forms-projekt. Högerklicka på References i Solution Explorer och välj Add Reference… Markera COM > Type Libraries och kryssa i ”Microsoft Excel 16.0 Object Library” och klicka på OK. Du ska då få en rad under References där det står Microsoft.Office.Interop.Excel (se bilder).

Det måste inte nödvändigtvis vara version 16.0 av Excel Object Library, men om du inte hittar någon rad alls med Excel behöver du installera Office Developer Tools för Visual Studio. Mer information om detta hittar du på internet.

Man måste kryssa i rutan, det räcker inte att markera raden.

Bygg ett enkelt gränssnitt som nedan. När man klickar på Öppna Excel startar Excel upp. Då kan man skriva något i cellen längst upp till vänster (rad 1, kolumn A). Tryck på Enter när du skrivit klart. Sedan kan man trycka på knappen Importera för att visa värdet från Excel i textrutan. Knappen Stäng Excel sparar Exceldokumentet och stänger Excel.

1 using System;

2 using System.Windows.Forms;

3 using Excel = Microsoft.Office.Interop.Excel;

4 namespace Exempel_01_05_Dynamic

5 { 6 public partial class Form1 : Form

7 { 8 Excel.Application excel; 9 Excel._Worksheet kalkylBlad;

10 public Form1 ( ) 11 {

12 InitializeComponent(); 13 }

14 private void btnÖppnaExcel_Click ( object sender, EventArgs e )

15 {

16 excel = new Excel.Application();

17 excel.Visible = true;

18 excel.Workbooks.Add();

19 kalkylBlad = excel.ActiveSheet;

20 }

21 private void btnImportera_Click ( object sender, EventArgs e )

22 {

23 Excel.Range cellA1 = kalkylBlad.Cells[ 1, "A" ];

24 dynamic värde = cellA1.Value;

25 tbxCellVärde.AppendText( värde.ToString() );

26 }

27 private void btnStängExcel_Click ( object sender, EventArgs e )

28 { 29 kalkylBlad.SaveAs( "Test.xlsx" );

30 excel.Quit();

31 }

32 }

33 }

Rad 3 Det finns en klass som heter System.Windows.Forms.Application, och en annan klass som heter Microsoft.Office.Interop.Excel.Application. För att göra skillnad på dem, och för att ange saker från Excel-biblioteket, lägger vi till ett usingdirektiv som säger att ordet Excel är synonymt med Microsoft.Office.Interop.Excel.

Rad 16–19 Öppnar och visar Excel, samt lägger till en ny arbetsbok och hämtar en referens till det aktiva kalkylbladet.

Rad 24 Här används nyckelordet dynamic för en variabel som refererar till ett objekt från ett annat program.

Rad 25 Här anropas metoden ToString i en okänd klass. Det betyder att det inte är kompilatorn som bestämmer vilken ToString som ska exekveras, utan det bestäms först när programmet exekverar.

Du är nog van vid att det dyker upp en hjälptext med alternativ som visar vad du kan skriva efter punkten efter en variabel. När du skriver ”värde.” i det här fallet får du ingen sådan hjälp, eftersom kompilatorn inte vet vad värdet har för datatyp.

Rad 29–30 Sparar Excel-dokumentet med namnet Test.xlsx och stänger Excel. Filen sparas i samma mapp som vårt programs exe-fil (bin\debug), eftersom vi inte angett sökvägen till någon annan mapp.

Övning 1.7 Importera och exportera till Excel

Skapa ett program som kan importera tre värden från en valfri rad i Excel, och som dessutom kan exportera värden till nya rader i Excel. Alternativt kan du låta ditt program lägga till rader under varandra i Excel, eller hitta på ett eget sätt att använda Excel från ditt program.

try

Undantagshantering

Ett undantag (Exception) är ett fel som uppstår vid exekvering. Det kan beror på en bugg, men det är också så att vissa operationer är känsliga för fel även om koden är felfri. Det gäller till exempel att skriva till en fil (den kan vara skrivskyddad eller låst eller finnas i en mapp som kräver särskilda behörigheter, hårddisken kan vara full, och så vidare). Att läsa från en fil kan också gå fel (användaren kanske anger en fil i fel format, eller en fil som inte finns). Nätverkskommunikation är ”felbenägen”. Brandväggar kan stoppa datatrafiken, eller servern kan ligga nere.

Ett vanligt undantagsfel i alla program är NullReferenceException. I det här fallet får felet nog anses som en bugg, eftersom man bör skriva sin kod så att referenserna aldrig kan vara null när man försöker använda dem. Ett annat vanligt undantagsfel är att försöka komma åt ett element i en lista på en plats som inte finns (IndexOutOfBoundsException). Det får också betraktas som en bugg, eftersom man alltid ska se till att alla index håller sig inom gränserna.

Undantagshantering går ut på att skriva alla satser där man förväntar sig att fel kan uppstå i ett try-block. Efter try-blocket skriver man ett catch-block med satser som ska exekveras om och bara om det uppstår ett fel.

{ // Felbenägna satser här.

}

catch ( Exception error )

{ // Kod för när fel uppstår, ofta ges bara ett meddelande till användaren.

}

Som exempel visar vi ändå hur man kan hantera ett undantagsfel med hjälp av IndexOutOfBoundsException. Programmet nedan har två knappar och tre textrutor. Knappen Generera slumpar fram det antal heltal som står angivet i textrutan

Antal. Knappen Visa visar värdet på det index som står angivet i textrutan Index

private int[] lista; Random generator = new Random();

private void btnGenerera_Click( object sender, EventArgs e )

{ // Läs in antal: int antal;

bool antalOK = int.TryParse( tbxAntal.Text, out antal );

// Kontrollera inmatning: if ( antalOK && antal > 0 ) lista = new int[antal]; else { MessageBox.Show( "Ange ett tal över 0" ); return; }

// Generera slumptal från 1 till 6: for ( int i=0 ; i<antal ; i++ )

{ lista[i] = generator.Next( 1, 7 ); } }

public void btnVisa_Click ( object sender, EventArgs e )

{ // Läs in index: int index;

bool indexOK = int.TryParse( tbxIndex.Text, out index );

// Kontrollera inmatning: if ( ! indexOK ) { MessageBox.Show( "Ange ett tal." ); return; }

// Visa talet på indexet: try

{ tbxVärde.Text = lista[index].ToString(); }

catch ( IndexOutOfRangeException error ) { MessageBox.Show( error.ToString() ); } }

Lägg märke till de två kodblocken try{ } och catch{ }. I try-blocket står den eller de satser som kan generera undantagsfel. Catch-blocket fångar upp felet och visar felmeddelandet.

Observera att automatiskt genererade felmeddelanden som det här ofta är obegripliga för den vanlige användaren. Här skulle man istället kunna visa felmeddelandet "Ange ett index mellan 0 och " + (lista.Length−1)".

Observera också att IndexOutOfRangeException i catch-blockets huvud bara hanterar indexfel. Man skulle också kunna hantera olika fel i separata catch-block:

{ // Satser som kan gå fel.

}

catch ( NullReferenceException error )

{ // Visa ett felmeddelande.

}

catch ( IndexOutOfBoundsException error )

{ // Visa ett annat felmeddelande.

}

catch ( Exception error )

{ // Visa ett tredje felmeddelande.

}

Det första felet som kan uppstå i exemplet är att lista är null. Sedan kan indexet vara utanför gränserna. Några andra fel är inte troliga, så det tredje catch-blocket är lite överflödigt, men det visar hur man kan fånga upp ett fel som inte täcks av de tidigare alternativen.

Strukturerad felhantering

Det är relativt enkelt att generera egna undantagsfel. Ett undantagsfel är nämligen ett objekt som beskrivs av en klass. Vi kommer att skriva ett program där man kan skapa ett användarkonto med hjälp av en e-postadress och ett lösenord. Kontot skapas i form av ett Konto-objekt som vi definierar i en egen klass. Vi kräver av användaren att lösenordet ska innehålla både bokstäver och siffror.

Vi börjar med att skapa en klass för undantagsfelet. Det är brukligt att ge klassen ett beskrivande namn följt av …Exception för att visa att det är ett undantagsfel. I klasshuvudet deklareras klassens namn följt av : Exception. Exception är den fördefinierade klassen System.Exception, och genom att ange den med ett kolon i klasshuvudet talar vi om att vår klass LösenFormatException är en speciell variant av System.Exception.

public class LösenFormatException : Exception { string message = "Lösenordet måste innehålla både bokstäver och siffror"; public override string ToString() { return message; } }

Därefter skapar vi vår konto-klass, där konstruktorn genererar ett lösenordsfel om det inte uppfyller kriterierna.

public class Konto { string epost; string lösenord;

// Konstruktor: public Konto ( string epost, string lösen ) { // Räkna antal siffror: int antalSiffror = 0; for ( int i=0 ; i<lösen.Length ; i++ ) { if ( char.IsDigit( lösen[i] ) antalSiffror++; } // Verifiera lösenordets form: if ( 0 < antalSiffror && antalSiffror < lösen.Length ) { this.epost = epost; this.lösenord = lösen; } else throw new LösenFormatException(); } }

Felet genereras i sista satsen som ett nytt objekt av typen LösenFormatException, och ”kastas ut” till alla try-catch-block som kör Konto-konstruktorn, med kommandot throw. Nedan ser du koden för ett fönster som skapar ett konto-objekt, och som tar hand om eventuella lösenordsfel.

public partial class Form1 : Form { Konto konto;

private void btnSkapa_Click ( object sender, EventArgs e ) { string epost = tbxEpost.Text; string lösen = tbxLösen.Text;

try { konto = new Konto( epost, lösen ); }

catch ( LösenordException fel ) { MessageBox.Show( fel.ToString() ); return; }

MessageBox.Show( "Kontot skapat" ); } }

Övning 1.8 Felsäkra dina program

I exemplet bör man också kräva att e-postadressen som anges följer formatet a@b.c. Komplettera programmet med en EpostAdressFormatException och hantera sådana fel i ett nytt catch-block.

Hädanefter ska du använda try-catch och eventuellt egna Exception-klasser för att felsäkra dina program. Fundera i varje läge på vad som kan gå fel och hur du ska hantera det. Målet är att alla dina program ska vara så ”idiotsäkra” som möjligt. Användaren ska inte kunna krascha ditt program ens om han försöker.

Repetitionsfrågor

rf 1.1 Vilken datatyp har följande literaler?

a) 57

b) '8'

c) false

d) 9.5

e) 4f f) "18"

rf 1.2 Vad blir resultatet av följande operationer

a) 2 + 1 * 5

b) 5 / 2 * 2

c) 3 < 10 % 3

rf 1.3 Betrakta fältet

int[,] tabell = { { 1, 2, 3 }, { 4, 5, 6 } };

a) Hur många element har fältet?

b) Hur många dimensioner har fältet?

c) Vilket värde har tabell[1,1] ?

d) Vad blir värdet av tabell.GetLength(1) ?

rf 1.4 Skriv en nästlad iteration som ger alla element i fältet värdet 3.

int[,] tabell = new int[4,7];

rf 1.5 Vilken sorts fält är typen int[][] ?

rf 1.6 Betrakta variablerna nedan och svara på frågorna.

byte x = 1; // 0000 0001

byte y = 2; // 0000 0010

byte z = 4; // 0000 0100

byte w = 6; // 0000 0110

Vad blir värdet av följande operationer?

a) x | y

b) y & z

c) y | z

d) y & w

e) x | y | z

f) (y & w) == y

rf 1.7 Betrakta UML-diagrammet över en klass och svara på frågorna.

Wizard

− power − spells : int : string[]

+ Wizard( p :int )

+ HasSpell ( s :string )

+ AddSpell ( s :string )

+ RemoveSpell ( s: string )

+ RemoveSpell ( i :int ) : bool : void : void : void

a) Vad heter klassen?

b) Vad betyder minustecknet?

c) Vad betyder plustecknet?

d) Vilken returtyp har metoden HasSpell?

e) Vilken metod är överlagrad?

f) Vad heter parametern i konstruktorn?

Programmering 2 C#

Den här boken är skriven för gymnasieämnet Programmering nivå 2 och är anpassad efter 2025 års gymnasiereform. Den förutsätter kunskaper motsvarande kursen Programmering 1, men innehåller både repetition och fördjupning av dessa kunskaper. Programvaran som används är gratisversionen av Visual Studio och är inriktad på desktop-program kodade i C# med .NET.

Facit finns till de viktigaste övningarna. Facit till samtliga övningar, provförslag och annat lärarmaterial finns att hämta digitalt i Gleerups digitala lärarmaterial.

Författare till Programmering 2 C# är Magnus Lilja. Han har mångårig erfarenhet av att undervisa i programmering.

Turn static files into dynamic content formats.

Create a flipbook