160 82 3MB
Swedish Pages 282 Year 2003
Anders Forsberg
Programmering i
C#
KOPIERINGSFÖRBUD
Detta verk är skyddat av lagen om upphovsrätt. Kopiering, utöver lärares rätt att kopiera för undervisningsbruk enligt BONUS-Presskopias avtal, är förbjuden. Sådant avtal tecknas mellan upphovsrättsorganisationer och huvudman för utbildningsanordnare t.ex. kommuner/universitet. För information om avtalet hänvisas till utbildningsanordnarens huvudman eller BONUS-Presskopia. Den som bryter mot lagen om upphovsrätt kan åtalas av allmän åklagare och dömas till böter eller fängelse i upp till två år samt bli skyldig att erlägga ersättning till upphovsman/rättsinnehavare. Denna trycksak är miljöanpassad, både när det gäller papper och tryckprocess.
Art.nr 31150 ISBN 978-91-44-05300-4 © Anders Forsberg och Studentlitteratur 2003 Omslagsbild: DigitalVision Omslagslayout: Pernilla Eriksson Printed in Sweden Studentlitteratur, Lund Webbadress: www.studentlitteratur.se Tryckning/år 1 2 3 4 5 6 7 8
9 10
2007 06 05 04 03
Innehåll
Inledning 7 1 En infrastruktur för programutveckling 11 1.0.1 Hello World! 13 1.0.2 Komponentorientering 15 1.0.3 Standarder 17 1.1 Typkategorier 18 1.2 Klassbibliotek 22 1.3 Mellanformat med metainformation 25 1.4 Skräpsamling 28 2 Inledande detaljer 31 2.0.1 Variabler 32 2.0.2 Kommentarer 33 2.0.3 Literaler 34 2.1 IO och strängar 34 2.1.1 Strängar 36 2.1.2 Inläsning 37 2.2 Main-metoden 39 2.3 Typomvandlingar 40 2.4 Operatorer 43 2.5 Vektorer 49 2.6 Uttryck och satser 52 2.6.1 If-satsen 53 2.6.2 Switch-satsen 54 2.6.3 While-satsen 56 2.6.4 Do-while-satsen 58 2.6.5 For-satsen 58 2.6.6 Foreach-satsen 59 2.7 XML-kommentarer 61 Kopiering av kurslitteratur förbjuden. © Studentlitteratur
3
3 Klasser 63 3.1 Inkapsling och datagömning 64 3.2 Fält 66 3.3 Konstruktorer 69 3.3.1 Flera konstruktorer 71 3.3.2 UML-notation 74 3.4 Metoder 76 3.4.1 Statiska metoder och instansmetoder 80 3.4.2 Klassen Rational 82 3.5 Egenskaper 85 3.5.1 Egenskap som förenklad konstruktor 87 3.6 Operatorer 89 3.6.1 Design 90 3.6.2 Indexoperatorn 92 3.6.3 Klassen Rational 94 3.7 Typomvandlare 98 3.8 Destruktor 102 3.8.1 Designmönstret Dispose 104 4 Arv och dynamisk bindning 107 4.1 Arv kontra aggregering 111 4.1.1 Terminologi 113 4.2 Konstruktorer och destruktor 114 4.3 Typomvandling 118 4.4 Virtuella metoder 121 4.5 Abstrakta basklasser 123 4.6 Interface 125 4.6.1 Synlighet 128 4.6.2 Statisk och dynamisk bindning 130 4.6.3 Standardinterface 132 4.7 Arvet från Object 135 4.7.1 GetHashCode och ToString 137 4.7.2 Att jämföra objekt 138 4.7.3 Att kopiera objekt 142 4.7.4 Tillämpning 143 5 Structer 145 5.1 Skillnader mot klasser 146 5.1.1 Egendefinierade structer 148 4
Kopiering av kurslitteratur förbjuden. © Studentlitteratur
5.2 5.3
5.1.2 Klassen Rational 150 5.1.3 Structen Rational 156 Enum 161 Boxing 163
6 Undantag 167 6.1 Kasta undantag 168 6.1.1 Klassen Rational 170 6.2 Hantera undantag 172 6.3 Design 176 7 Delegerare och notifierare 179 7.1 Anrop via delegerare 180 7.1.1 Anrop till flera metoder 182 7.1.2 Publicerare och prenumeranter 184 7.2 Anrop via notifierare 187 7.2.1 Fönstersystem 188 7.2.2 Notifierare i interface 189 7.3 Parametrar 190 8 Attribut 8.1 8.2 8.3
195 Läsa attribut 199 Egna attribut 201 Attribut till attribut 203
9 Pekare och osäker kod 205 10 Klassbiblioteket 211 10.1 System 213 10.2 System.Collections 219 10.2.1 System.Collections.Specialized 224 10.3 System.IO 225 10.4 System.Net 231 10.4.1 System.Net.Sockets 237 10.5 System.Threading 241 10.5.1 Synkronisering 245 10.5.2 Asynkron programmering 248 10.6 System.Text 253 10.7 System.Xml 256 Kopiering av kurslitteratur förbjuden. © Studentlitteratur
5
10.8 System.Globalization 261 10.9 System.Diagnostics 262 10.10 System.Reflection 263 10.11 System.Runtime 266 10.11.1System.Runtime.CompilerServices 266 10.11.2System.Runtime.InteropServices 267 10.12 System.Security 269 10.12.1System.Security.Permissions 271 Sakregister 275
6
Kopiering av kurslitteratur förbjuden. © Studentlitteratur
Inledning
Inledning
Det är inte varje dag man får tillfälle att se ett nytt världsspråk födas. Senast det hände var då James Gosling och Green Team inom Sun utvecklade en förenklad C++ avsedd för inbyggda system. Utan att de riktigt visste om det då, skulle deras språk Java bli ett av de absolut största på bara några år. De började 1991, lanserade språket officiellt 1995 och ett par år senare var det en världsomfattande rörelse. C och C++ däremot, föddes extremt långsamt under 1980- och 1990-talen. Åtskilliga varianter kom i användning långt innan de blev standardiserade 1988 respektive 1997. Samtidigt har inga andra språk haft sådant djupgående inflytande över andra språk som dessa båda nära släktingar. Inom väldens största mjukvaruutvecklare Microsoft har C++ länge varit standardspråk. Microsofts anammande av Java blev aldrig någon riktigt ömsesidig kärlek. Java för Windows måste anpassas till objektmodellen COM och Windows fönstersystem, och den plattformsoberoende ideologin i Java utsattes för övergrepp, tyckte Javasamfundet. 1996 värvades Anders Hejlsberg från Borland till Microsoft. Han är en av väldens mest kända språkutvecklare. Som ”Chief Architect” ledde han en arbetsgrupp som år 2000 presenterade det nya språket C# (C sharp). Som programutvecklare och lärare i C++ med huvudsaklig hemvist i Windowsapplikationer reagerade jag genast då jag i ett nyhetsbrev från Microsoft såg de första planerna på utvecklingen av ett nytt språk. Sedan dess har jag plöjt tusentals sidor preliminär dokumentation som varit motstridig och tröttsam, men som efterhand klarnat och numera utgör en någorlunda (!) klar och sammanhängande Kopiering av kurslitteratur förbjuden. © Studentlitteratur
7
Inledning
bild av något som måste betecknas som en hel teknikgeneration inom mjukvaruutveckling. När jag håller kurser i C#-programmering är det inte ovanligt att inbitna Javamänniskor invänder att ”detta är stulet från Java” och ”det där är också stulet från Java”. Naturligtvis är det så! Liksom mycket i Java är stulet från C++ och Smalltalk, och mycket i C++ är stulet från C, som stal det från... Utvecklingen går vidare. C# är nyare, ännu mera objektorienterat och ännu bättre än Java. Det har Javas förenklingar, men mera av C++ flexibilitet. Det har åtskilliga populära mekanismer även från andra språk, exmpelvis från VB! Bara att ta till sig och dra nytta av. Denna bok beskriver C# och den underliggande infrastruktur som heter Common Language Infrastructure (CLI) och som är standardiserad i ECMA- och (sannolikt då detta läses) ISO-standarder. Den beskriver alltså inte Microsofts ursprungliga och mångdubbelt större företeelse kallad .NET (dotnet). .NET är att se som en implementation av CLI, och vid denna boks tryckning pågår flera stora projekt för att implementera CLI och C# för andra plattformar. Leta gärna lite på nätet efter information om exempelvis Mono-projektet för Linux!
Bruksanvisning Boken är tänkt att kunna läsas från början till slut. Kanske inte direkt som en spännande roman, men ändå som en sammanhängande historia. Den är alltså inte referensmässig, och kapitlen är inte fristående utan förutsätter varann. Den bör vara en utmärkt kursbok. För att använda boken som kursbok bör läraren besöka www.grafpro.se, där (med tiden) allt kompletterande och nödvändigt material finns: laborationer med lösningsförslag, samt OH-bilder för undervisningen. Där mottages också tacksamt synpunkter och tips om rättelser eller justeringar.
8
Kopiering av kurslitteratur förbjuden. © Studentlitteratur
Inledning
För akademisk nivå lämpar sig boken för B- eller C-nivå som tilllämpning i objektorienterad och komponentorienterad programutveckling. Med fördel kan den studeras parallellt med en kurs i objektorienterad analys och design, eller i objektorienterade designmönster. Terminologi och notation ansluter till UML. För den redan yrkesverksamme programutvecklaren gäller att kunnande i Java eller C++ gör boken lättläst och man kan då rent av skumma vissa delar. Bakgrund i Smalltalk gör också boken ganska lättläst. Även annat bakgrundskunnande kan underlätta, om än inte lika mycket. Funktionsorienterade språk – särskilt C – har syntaxmässiga likheter, men för den med C-bakgrund kan de objektorienterade idéerna och terminologin bli delvis ny och obekant. Objektorientering introduceras inte i boken utan förutsätts någorlunda bekant. Boken är alltså inte avsedd som introduktion till programmering. För den som inte har datavetenskapliga grunder blir den nog ett riktigt sömnpiller.
Disposition Dispositionen är i stora drag den, att det första kapitlet beskriver språkets nära samhörighet med CLI-standarden. Språket är en tilllämpning av det generella typsystem som är det centrala i CLI, och det är min övertygelse att man ska inse det innan man tar itu med de tusen detaljerna. Därefter gås de tusen detaljerna igenom, och om C# inte är det första språk man lär sig så är det inte så jobbigt som det låter. Detta språk kännetecknas ju av att ha de populäraste språkmekanismerna från flera andra språk. Onödigt många kan tyckas, men därmed känner man igen sig om man har någon erfarenhet från språk särskilt i C-familjen. De följande kapitlen tar sedan upp ett problemområde i sänder: klasser, arv, structer, undantag, delegerare, attribut.
Kopiering av kurslitteratur förbjuden. © Studentlitteratur
9
Inledning
I det sista, ganska stora kapitlet behandlas klassbiblioteket så som det är indelat i namnrymder. Den indelningen motsvarar applikationsområden såsom trådar, IO-hantering, samlingar, etcetera och är därmed en god indelningsgrund för tillämpningar av tidigare kapitel. Samtidigt är det åtminstone en inledande bruksanvisning till klassbiblioteket. Men man bör vara medveten om att de 294 datatyperna och de designmönster de implicerar är ämne för många hundra sidor till. Kanske i en kommande upplaga... En liten brasklapp om kodexemplen måste infogas här. I amerikanska böcker påpekas ofta att alla kodexempel är kompletta och körbara program för realistiska problem. Det brukar innebära oändliga listor där det viktiga utgör en bråkdel och läsaren hoppar därför över hela kodlistorna. I denna bok har jag valt den motsatta principen. Kodexemplen är fragmentariska och visar bara just den språkmekanism eller det lilla stycke programlogik som är aktuell. Kommentarer, omgivande klasser och namnrymder, nödvändiga deklarationer m.m. som måste finnas för att göra koden kompilerbar saknas oftast, för att inte trötta läsaren med upprepningar. På websidan www.grafpro.se däremot, finns kompletta kodexempel. Till sist vill jag rikta ett par stora tack, dels till hustrun Anette och dels till kollegan Harald Lüning, för ovärderlig hjälp med granskning av manuset i språklig respektive innehållslig aspekt. För eventuella felaktigheter måste tyvärr ändå författaren påta sig hela skulden. Kalmar i november 2002 Anders Forsberg
10
Kopiering av kurslitteratur förbjuden. © Studentlitteratur
1 En infrastruktur för programutveckling
1 En infrastruktur för programutveckling
I slutet av 1990-talet påbörjades inom Microsoft ett gigantiskt projekt med avsikt att skapa en helt ny infrastruktur för programutveckling och för distribuerade system. Microsofts utvecklingsverktyg var då helt olika i alla avseenden, och de egna mjukvaruprodukterna exekverade också på olika sätt. Uppdraget bestod i att utveckla en ny modell för objektorienterad programutveckling och en ny modell för exekvering av komponentbaserad mjukvara. Ett genomgående krav var också att exekveringsmodellen skulle vara anpassad för distribuerad exekvering över Internet. Projektet kallades .NET (dotnet). Programmeringsmodellen kom att ta ett nytt radikalt grepp. Det traditionella tänkandet att ett språk och ett typsystem i praktiken är samma sak bröts genom att man istället separat definierade typsystemet utan att koppla det till språk. Varje språk som har mekanismer för att hantera datatyper enligt typsystemet blir då lika användbart. Typsystemet definierar hur variabler kan skapas och användas, men inte hur syntaxen för det faktiskt ser ut. Språk ska vara en smaksak. Man beslutade också att kompilering ska ske till ett mellanformat. Kompilerad kod är alltså inte exekverbar direkt, utan kompileras ytterligare en gång för att bli exekverbar maskinkod. Mellanformatet är oberoende av språket så att komponenter i mellanformat kan användas från klienter skrivna i valfritt språk. Mellanformatet är också oberoende av målmiljö, kompileringen till maskinkod sker vid programstart.
Kopiering av kurslitteratur förbjuden. © Studentlitteratur
11
1 En infrastruktur för programutveckling
Ytterligare en hörnstolpe i modellen är att det i mellanformatet också definierats ett format för information om de kompilerade datatyperna. Kompilerad kod är självbeskrivande, och det behövs ytterst lite extra källkod för att göra kompilerade datatyper exporterade så att de kan laddas och användas som komponenter. Modellen är därmed också en vidareutveckling från objektorientering till komponentorientering. Komponentteknologin stöder implementationsarv, det vill säga att en kompilerad klass kan användas som basklass vid arv i kod som inte ens behöver vara samma språk som det basklassen utvecklades i. Åtskilliga befintliga språk kan användas för programmeringen, förutsatt att de används på det sätt som typsystemet föreskriver. För de flesta befintliga språk innebär det att de måste modifieras och utökas för att erbjuda alla de språkmekanismer som krävs. Den som väljer ett befintligt språk har en viss inlärningströskel att passera, olika stor beroende på hur väl språket råkar passa modellen. Ett språk intar en särställning här: C# (C sharp). Detta språk har konstruerats enkom med avsikt att passa modellen och är därmed sannolikt det mest ändamålsenliga. Det är rent objektorienterat, med många av de mest populära mekanismerna från framförallt Java och C++. Där finns också vissa inslag från Visual Basic. Jämfört med Java är C# betydligt mera omfattande och flexibelt, nästan som C++. Jämfört med C++ är det förenklat och säkrare – åtskilliga av de klassiska buggar man lätt åstadkommer i C++ är helt enkelt omöjliga i C#. I december 2001 standardiserade organisationen ECMA (European Computer Manufacturers Association) både C# (ECMA-334) och de delar av .NET som inte är plattformsspecifika (ECMA-335). En följande ISO-standardisering inträffade sannolikt strax efter denna boks tryckning. .NET är en Microsoftprodukt och stora delar är specifika för Windows. Ändå kunde mycket av det grundläggande i typsystemet och exekveringsmodellen ingå i standarden. Standarden heter Common Language Infrastructure. Framöver i denna bok kommer typsystemet och språket att beskrivas parallellt, de är ju intimt förknippade genom att språket tillämpar typsystemets regler på ett okonstlat sätt.
12
Kopiering av kurslitteratur förbjuden. © Studentlitteratur
1 En infrastruktur för programutveckling
1.0.1 Hello World! Nu hög tid för lite kod. Ett minimalistiskt C#-program kan se ut så här: namespace Studentlitteratur.CSharp { public class Minimal { private string message; public Minimal(string message) { this.message = message; } public void DeliverMessage() { System.IO.Console.WriteLine(message); } static void Main() { Minimal obj = new Minimal(”Hello World!”); obj.DeliverMessage(); } } Källkoden sparas normalt i textfiler vars namn har filändelse .cs. Om kompilatorn startas med kommandot csc och filen heter hello.cs kommer då csc hello.cs vid kommandoprompten att generera filen hello.exe. Den filen innehåller då mellanformatskod. Fördelningen av källkod i kodfiler är ganska valfri, man behöver inte ha just en klass i en fil. Med en flagga till kompilatorkommandot kan istället en fil med filändelse .dll genereras. Den är då en komponent (dynamic-linklibrary). Hela klassbiblioteket är kompilerat till dll-filer och standarddatatyperna i klassbiblioteket används alltid genom att den koden länkas dynamiskt. Statisk länkning (att anropad kompilerad kod länkas in i programfilen) finns inte.
Kopiering av kurslitteratur förbjuden. © Studentlitteratur
13
1 En infrastruktur för programutveckling
När egna datatyper i dll-filer ska användas från en exe-fil räcker det med att dll-filens namn refereras i en flagga till kompilatorkommandot. Det första att notera i källkoden är att klassen Minimal omges av ett namespace-block. Namnrymder finns i andra språk också men är här närmast obligatoriskt. Idén är enkel, genom att omge kod med en namnrymd undviker man eventuella namnkrockar med andra företeelser i samma projekt. Allt som deklareras inom namnrymden får dess namn som ett förnamn. Koden System.IO.Console.WriteLine(message) är exempel på klientkod som använder klassen Console i namnrymden System.IO, vilket är en av namnrymderna i klassbiblioteket. Här anropas den statiska metoden WriteLine i klassen Console. Metoden finns kompilerad i en dll-fil som länkas dynamiskt och som utgör en del av klassbiblioteket. En annan mycket intressant detalj är modifieraren public före klassdeklarationen. Med det lilla ordet blir klassen exporterad, det vill säga att den efter kompilering kan användas från annan kod i en annan kompilerad fil. Kompilerade klasser i en fil som kan användas från andra kompilerade filer kallar vi ju komponenter, och här är komponenttänkandet utomordentligt långt drivet. Den som har bakgrund i något objektorienterat språk känner nog igen en hel del av Minimal-klassens innehåll. Där finns en konstruktor som tar en strängparameter och använder den för att initiera en strängmedlem. Varje medlem har en modifierare för synlighet. Main-funktionen måste i detta språk vara en static medlem i någon klass – globala funktioner finns inte. Funktioner i en klass eller annan abstrakt datatyp kallas ju allmänt metoder, så vi gör det hädanefter. Metoden heter som synes Main och kan ha både returvärde och parametrar, fast det har den inte här. I Main skapas ett objekt av klassen Minimal. Det går alltid till på det sätt som vi ser här. En variabel av typ Minimal är inte ett objekt utan en objekt-referens. Om den inte tilldelas något har den värdet null och refererar inte till något alls. Objektet skapas med new och ett explicit anrop till någon konstruktor. 14
Kopiering av kurslitteratur förbjuden. © Studentlitteratur
1 En infrastruktur för programutveckling
1.0.2 Komponentorientering Några designprinciper bakom språket har formulerats av standardiseringsorganisationen ECMA: • C# är avsett att vara ett enkelt, modernt, generellt och objektorienterat programmeringsspråk. • Språket, och implementationer därav, ska ge stöd för programmeringsprinciper såsom stark typkontroll, gränskontroll i vektorer, upptäckt av oinitierade variabler, och automatisk skräpsamling. Robusthet, varaktighet och programmerarens produktivitet är viktiga faktorer. • Språket är avsett för utveckling av mjukvarukomponenter avsedda för användande i distribuerade system. • Portabilitet på källkodsnivå är viktigt, liksom programmerarens möjlighet att migrera från liknande språk – särskilt C och C++. • Stöd för internationalisering är viktigt. • C# är avsett att passa lika bra för utveckling av applikationer för både stora och små målplattformar, allt från plattformar med omfattande och sofistikerade operativsystem ner till mycket små inbyggda system med specialiserad funktionalitet. • Även om C#-applikationer är avsedda att vara ekonomiska med avseende på behov av minne och processorkapacitet är språket inte avsett att kunna tävla med C eller assembler vad avser storlek och prestanda. I stora delar är språket en mans verk. Anders Hejlsberg på Microsoft har utformat grunderna och skrivit ”C# Language Specifications”, egentligen ett internt Microsoftdokument, men som ändå spelat stor roll i standardiseringarbetet. Han är tidigare känd från Borland, där han haft en ett avgörande inflytande på Delphi och dess objektorienterade Pascal. I ett tidigt framförande på en Microsoft Developer Conference sammanfattade han de bärande idéerna så här: • C# är det första komponentorienterade språket i C/C++-familjen. Primära begrepp är egenskaper (properties), metoder och notifierare (events). Attribut och dokumentation integreras i koden och kan läsas både i designfasen och vid exekvering. • Allt är objekt. Primitiva datatyper som vi känner dem från C++ och Java är historiska rester och har spelat ut sin roll.
Kopiering av kurslitteratur förbjuden. © Studentlitteratur
15
1 En infrastruktur för programutveckling
• Robust och säker mjukvara. Automatisk skräpsamling garanterar att inga minnesläckor uppstår. Undantag (exceptions) ger ett samlat grepp på felhanteringen. Stark typkontroll ger typsäkerhet – inga oinitierade variabler, inga felaktiga typomvandlingar. • Bevara investeringar. Arvet från C++ återanvänder kunnande i C++-programmering. Interoperabilitet med COM, DLL, XML och SOAP bygger vidare på befintliga tekniker. Kort inlärning och ökad produktivitet i det nya språket. Observera särskilt betoningen på komponentorientering. I det avseendet är språket ett avsevärt framsteg. Det behövs nu inga särskilda regler eller åtgärder för att göra en klass till en komponent. Det är rent av så att klasser utvecklade i C# nästan automatiskt blir komponenter. En klass blir exporterad genom att deklareras public, dess metainformation skapas och bakas in i filen utan särskilda åtgärder alls. Att språket är helt och rent objektorienterat har väl framgått, men kanske är ändå påståendet att ”allt är objekt” och att primitiva datatyper spelat ut sin roll överraskande radikalt. Det ska alltså tolkas bokstavligt. I C# finns inga variabler som inte har medlemmar. De primitiva datatyper som man ser i vilket språk som helst är här alltid objekt, och de har som alla objekt en gemensam basklass med några medlemmar. Om vi skapar exempelvis en int, så har den en metod som heter ToString, och som returnerar värdet i en sträng. int x = 100; x.ToString(); // returnerar ”100” Av det följer också att literaler har metoder! Literalen 100 tolkas av kompilatorn som varande en int, och det är helt OK att skriva 100.ToString(); Det är visserligen bara lek med syntaxen, men visar att ”allt är objekt”. En annan central punkt är driftsäkerhet. Ett C#-program har inga minnesläckor, dolda typfel eller oinitierade variabler. Den kontrollen ligger dels i kompileringen, dels i exekveringsmiljön.
16
Kopiering av kurslitteratur förbjuden. © Studentlitteratur
1 En infrastruktur för programutveckling
1.0.3 Standarder De standarder som språket implementerar har ett gemensamt namn: Common Language Infrastructure (CLI). CLI består av standarderna: • Common Type System (CTS). Vilka slags typer som får finnas och deras egenheter. Arv, dynamisk bindning, typomvandling, m.m. • Common Language Specification (CLS). Vilka språkmekanismer som är obligatoriska respektive valfria. • Common Intermediate Language (CIL). Mellanformatet och förmatet för metainformation. • Virtual Execution System (VES). Exekveringsmiljön. Hur klassladdning ska ske, skräpsamling, kompilering till maskinkod. Bland dessa regelverk är CTS den största saken för en utvecklare. CTS säger att det bara finns tre sorters datatyper: klasser, structer och interface. Detta är en verklig grundbult i CLI, och beskrivs lite mera utförligt i nästa avsnitt. I själva verket handlar sedan en stor del av boken om CTS genom att C# är ett språk för att hantera CTS-typer. Klassbiblioteket är också att betrakta som en CTS-tillämpning, det introduceras i avsnitt 1.2 och dess viktigaste datatyper behandlas i tur och ordning i kapitel 10. CLS är i viss mån en specificering och inskränkning av CTS, plus några regler som allmänt är att betrakta som designregler, både för programutvecklaren och för kompilatorutvecklare. Reglerna gäller vilka datatyper som bör förekomma i en klass’ gränssnitt, något om undantagshantering (exceptions) och om operatoröverlagring. Mellanformatet CIL är väl standardiserat, det är ju centralt på det sättet att mellanformatsfiler ska kunna användas som komponenter av klienter skrivna i vilket språk som helst. Inga spår av språket (C++, C#, etc) bör alltså vara kvar. En högintressant del i mellanformatet är metainformationen. Kompilerad kod är självbeskrivande så att inga särskilda deklarationsfiler eller gränssnittsbeskrivningar behövs. Mera om det i avsnitt 1.3. Exekveringsmiljön VES är standardiserad så att funktionaliteten beskrivs noga, däremot inte hur den ska implementeras. Den intressantaste delen för utvecklare där är den automatiska skräpsamlingen. Att skriva program för en miljö med automatisk skräpsamKopiering av kurslitteratur förbjuden. © Studentlitteratur
17
1 En infrastruktur för programutveckling
ling är en helt annan sak än att skriva för miljöer utan. Mera i avsnitt 1.4.
Klientkod i C#, C++, Java, VB, JScript, m.fl.
Klassbibliotek i en enda arvshierarki, indelat i namnrymder och profiler
CTS: Interface Klasser Structer
CLS: Enkla typer Exceptions Operatorer
VES: JIT-kompilator, klassladdare, skräpsamlare
Operativsystem Figur 1.1 Skissen visar några centrala begrepp, och antyder deras logiska samband. Begreppen förklaras efterhand.
1.1 Typkategorier För programutveckling i CLI är språket enbart en smaksak. C# är förvisso ett mycket passande val, men det är fullt möjligt att använda vilket språk som helst som stöder CTS/CLS. I CLS skiljer man på språk som enbart är ”consumers” av datatyperna i klassbiblioteket, och de som är ”extenders”. Consumers är kort sagt språk enbart för klientkod, som alltså inte skapar nya datatyper och inte behöver stödja de objektorienterade mekanismerna. Ett consumer-
18
Kopiering av kurslitteratur förbjuden. © Studentlitteratur
1 En infrastruktur för programutveckling
språk behöver alltså inte ens vara objektorienterat – det kan vara mycket enkla skriptspråk. Vi lämnar den varianten. Ett extenderspråk ska kunna skapa datatyper enligt CLS, och möjligen också enligt den lite vidare CTS. C# är självfallet en extender och med stöd för allt i CTS. De minsta byggstenarna i egna datatyper är de enkla datatyperna. De finns som medlemmar, som returvärden och som parametrar. ”Enkla datatyper” i CLI är ej att förväxla med ”primitiva datatyper” som vi ju tidigare såg förkastas såsom överspelade i historien. Primitiva datatyper är variabler som inte är objekt. Objekt har både tillstånd och beteende (och identitet). Primitiva datatyper från traditionella språk har bara tillstånd – inte beteende. Om mellanformatskoden ska bli språkoberoende måste det alltså där finnas en uppsättning enkla datatyper, och alla språk måste förstå de datatyperna, och inte använda några andra datatyper i det som utgör gränssnitt. CLS stadgar då följande lista.
CLS enkla datatyper bool – kan bara anta värdena true och false char – 16 bitars tecken (Unicode) unsigned int8 – 8 bitars icke teckensatt heltal int16 – 16 bitars teckensatt heltal int32 – 32 bitars teckensatt heltal int64 – 64 bitars teckensatt heltal float32 – 32 bitars flyttal, 7 siffrors precision float64 – 64 bitars flyttal, 15 siffrors precision
Det finns också en lite sällsynt men användbar datatyp som heter native int, och som är en teckensatt int med plattformens typiska storlek. Den är tänkt att vara användbar som pekare ellerhandtag (handle) för plattformsspecifika ändamål, i så kallad osäker kod (behandlas utförligare i kapitel 9).
Kopiering av kurslitteratur förbjuden. © Studentlitteratur
19
1 En infrastruktur för programutveckling
Slutligen finns två datatyper som direkt matchar klasser i klassbiblioteket, nämligen object som refererar till klassen Object, och string som refererar till klassen String. De namn på datatyper vi nu sett är namn i mellanformatet (CIL). I programspråk är det fritt fram att hitta på synonymer till dem, och så är det i C#. Det är alltså så att inga egentliga inbyggda datatyper får finnas, allt är datatyper i – eller härledda från – klassbiblioteket. Alla typnamn vi ser här är alltså redan synonymer till namn i klassbiblioteket. Alla utom object och string motsvarar egentligen structer, och object och string motsvarar som sagt klasser. Nästa tabell visar vad CLS-typerna heter i C# , och vad de motsvarar i klassbiblioteket.
CIL
C#
Klassbiblioteket
bool
bool
Boolean
char
char
Char
unsigned int8
byte
Byte
int16
short
Int16
int32
int
Int32
int64
long
Int64
native int
-
IntPtr
float32
float
Single
float64
double
Double
-
decimal
Decimal
object
object
Object
string
string
String
Det kan verka förvirrande och onödigt att datatyperna har ett namn i CIL, ett annat namn i klassbiblioteket och ytterligare ett tredje namn i C#. Avsikten är att de i C# ska anknyta till traditionen från C/C++ så att alla därifrån ska känna sig hemma. I andra språk kan andra synonymer användas. Notera gärna ytterligare en datatyp i denna tabell. I klassbiblioteket finns Decimal, som är en flyttalstyp med extra stor precision (28
20
Kopiering av kurslitteratur förbjuden. © Studentlitteratur
1 En infrastruktur för programutveckling
siffror). Den har synonymen decimal i språket, är CLS, men har inget eget namn i CIL och var därför inte med i tabell 1.1. I CTS finns alltså bara tre slags datatyper, varav klasser är den centrala. Interface är klasser utan implementation, endast bestående av just gränssnitt. Struct är som klasser i många avseenden, men med den stora skillnaden att structobjekt kan skapas på stacken, vilket klassobjekt aldrig gör. För egendefinierade datatyper är klass den stora saken att studera, och det gör vi i de närmaste kapitlen. Egendefinierade structer är också en möjlighet, och det beskrivs noga i kapitel 5. Men vad gäller structer är man mestadels klient, eftersom språkets enkla datatyper är just structer. I språket verkar det emellertid finnas ytterligare egendomliga saker såsom enum, delegate och annat. Men de är faktiskt också antingen structer eller klasser. Förklaringen är att alla språkkonstruktioner som skapar datatyper gör det genom att i tysthet använda klasser eller structer ur klassbiblioteket. I dokumentation förekommer dessutom ofta terminologin ”värdetyper” och ”referenstyper”. Även det är synonymt med structer respektive klasser, så här används i fortsättningen bara begreppen structer och klasser. Alla datatyper skapas med likartad syntax: class X { … } Ordet class kan också vara interface, struct eller enum.. Ordet enum är en C#-uppfinning för att på ett särskilt sätt skapa en struct. Ett klassobjekt skapas med en tvåstegskonstruktion. Först skapar man en referens av önskad typ. MyClass obj; Variabeln obj finns nu, men refererar inte till något alls. obj = new MyClass(); Operatorn new skapade i denna sats själva objektet, och det skedde på heap om det gällde en klass. De båda stegen kan med fördel kombineras. Kopiering av kurslitteratur förbjuden. © Studentlitteratur
21
1 En infrastruktur för programutveckling
MyClass obj = new MyClass(); Klassobjekt kan skapas endast på detta sätt, och en oinitierad referens har värdet null. Med structer förhåller det sig annorlunda. En struct är en värdetyp, vilket betyder att variabelnamnet kan användas som en symbol för värdet (tillståndet). MyStruct var; Om MyStruct i det exemplet är en struct kommer var att vara ett structobjekt med hull och hår. Dess medlemmar är oinitierade. Om den satsen finns i en metod kommer structen att ha skapats på stacken. Om den ingår i en klass kommer structen att vara inline, alltså skapas som en del av objektet när det skapas på heapen. Om en struct tilldelas en annan kommer två structer att vara lika, men fortfarande två. Om istället en referens tilldelas en annan kommer båda att referera till samma objekt! int x = 5; // en struct av typen Int32 int y = 10; y = x; // y och x är olika variabler med samma värde string s1 = ”Ett”; string s2 = ”Två”; s1 = s2; // s1 och s2 refererar till samma sträng I sista exemplet sätts s1 att referera till ett annat objekt än det nyss refererade till. Objektet som innehåller ”Ett” blir då övergivet och ett offer för skräpsamlaren, som förr eller senare raderar det.
1.2 Klassbibliotek Klassbiblioteket består av 294 datatyper. De är sorterade enligt flera indelningsgrunder, vilket kan göra terminologin lite onödigt omfattande. Hela klassbiblioteket är för det första en enda arvshierarki. Basklassen heter Object. Alla datatyper är alltså ättlingar till Object, och 22
Kopiering av kurslitteratur förbjuden. © Studentlitteratur
1 En infrastruktur för programutveckling
därmed är en Object-referens alltid möjlig att använda för någon datatyp ur klassbiblioteket (och några andra datatyper finns ju inte). Vad arvet från Object innebär ska vi se alldeles strax. För det andra är klassbiblioteket indelat i namnrymder. Roten heter System, och under System finns direkt 100 datatyper och 11 nya namnrymder. Namnrymderna är tänkta att ge datatyperna en indelning som någorlunda motsvarar applikationsområden. Exempelvis finns det mesta som gäller trådar och synkronisering i en namnrymd som heter Threading. Vi ska i ett senare kapitel (kap 10) gå igenom klasserna enligt deras placering i namnrymder, här ges bara en översikt. För det tredje är klassbiblioteket indelat i underbibliotek, och för att komplicera saken ytterligare finns namn på typiska kombinationer av underbibliotek, vilket kallas profiler. Underbiblioteket med de mest generella och oundgängliga klasserna heter Base Class Library (BCL). BCL tillsammans med Runtime Infrastructure Library bildar profilen Kernel. Kernel är tänkt att därmed definiera en miniimplementation av CTS. Observera att indelningen i underbibliotek och profiler går tvärs över namnrymderna. Ett underbibliotek som heter Extended Array Library innehåller rent av enbart medlemmar för klassen Array, och går alltså tvärs över klasser! Fysiskt utgörs klassbiblioteket av ett antal dll-filer i mellanformat, och de måste finnas i målmaskinen för att ett CLI-program ska exekvera. Hur programmet hittar dem, om de delas eller distribueras till varje programs egen katalog etcetera är saker som inte specificeras i standarden. Men i standarden för metainformationen finns ändå en del intressanta detaljer som bäddar för skapande av mekansimer som reglerar laddningen av datatyper på mycket strikta sätt. Mer om det i nästa avsnitt. Klasserna i biblioteket är både avsedda som basklasser och för instansiering direkt. Några är enbart för instansiering och omöjliga att använda för arv. Ytterligare en påtaglig egenskap hos klassbibliotekets datatyper är att dess metoder inte använder den gamla ovanan att returnera irrelevanta värden vid fel, utan mycket konsekvent kastar undantag. Kopiering av kurslitteratur förbjuden. © Studentlitteratur
23
1 En infrastruktur för programutveckling
Undantag är inte någon ny idé och ingenting i CLI tvingar utvecklaren att använda undantag mera än i andra språk – men när nu en helt ny programmeringsmodell ändå skulle uppfinnas tog man sig samman och här används konsekvent undantag och inte returvärden för att signalera felsituationer. Undantagshantering är ett ämne som gott och väl räcker för en hel bok – i denna bok finns ett eget kapitel – men vi kommer att stöta på företeelsen framförallt i klassbibliotekets metoder, så det måste introduceras redan här. En metod som inte kan exekvera klart och returnera ett relevant värde bör inte returnera något värde alls. I många språk finns därför en mekanism som brukar heta throw och som är en slags parallell till return (det normala sättet att avsluta en metod). Både return och throw avslutar metoden och avvecklar dess lokala variabler, men dess respektive typer är oberoende av varann. Den stora skillnaden är att vid throw måste den anropande metoden hantera undantaget genom att anropa inom ett block, som börjar med try och omedelbart efter try har ett block som börjar med catch. Om undantag uppstår avbryts då try-blocket och exekveringen hoppar till koden i catch-blocket. Om den anropande metoden inte har try- och catch-block, och ett undantag uppstår kommer även den att avslutas på ögonblicket och kasta undantaget vidare till sin anropande, och så vidare. En poäng är då att undantag som inte hanteras i kod hamnar i Main, som ju alltid är den första metoden i kedjan, och då stoppas programmet med ett felmeddelande. Ett undantag som inte hanteras gör sig alltså brutalt påmint. try { DangerousCall(); ... } catch (Exception e) { Console.WriteLine(”Oväntat fel...”); } 24
Kopiering av kurslitteratur förbjuden. © Studentlitteratur
1 En infrastruktur för programutveckling
Utan att gå in på mera detaljer här och nu konstaterar vi bara att metoder i klassbiblioteket vilka kastar undantag kan (och bör!) anropas som ovan, men naturligtvis med kod som hanterar problemet – om det är möjligt.
1.3 Mellanformat med metainformation CIL-filer innehåller mellanformatskod som efter JIT-kompilering (Just-In-Time, kompilering vid laddning) blir maskinkod för den aktuella processorn. Men där finns också som vi sett mycket omfattande och utförlig metainformation. Ett stycke metainformation kallas manifest. Ett manifest beskriver ett antal datatyper, vilka kan finnas i samma fil som manifestet, eller i en eller flera separata filer. Separata filer med kompilerade datatyper kallas moduler. Nu till det avgörande begreppet. Ett manifest plus den kompilerade koden, oavsett hur den fördelats i moduler är ett assembly. Normalfallet som också är det enklaste fallet är när ett assembly är en enda fil. Hur som helst så är ett assembly den minsta enheten för installation. En applikation består alltså av en eller flera assemblies, där kanske några av dem är generella komponenter som används också i andra applikationer. Ett assembly är en komponent (eller innehåller komponenter om man så vill), och den är självbeskrivande. Datatyper som är deklarerade public är tillgängliga från andra assemblies, och vid kompilering av dem räcker det med att referera (en flagga till kompilatorkommandot) till komponentens filnamn. I enklaste fall är följaktligen ett assemblys identitet dess filnamn, och i CLI-implementationen kan reglerna för sökning efter assemblyt vara så enkla att assemblyt söks enbart i aktuell katalog. Regler för sökning och laddning av assemblys överlåts i standarden helt på implementationen – standarder gör ju klokt i att inte förutsätta för mycket om målplattformen. Kopiering av kurslitteratur förbjuden. © Studentlitteratur
25
1 En infrastruktur för programutveckling
I förra avsnittet antyddes att det ändå i standarden finns förberett för en avancerad mekanism för sökning och identifiering av assemblys. Det består i att man i manifest kan förse ett assembly (eller en enskild modul) med en krypteringsnyckel och en hash. Åtskilliga moderna verifieringsmetoder bygger på idén med public key och private key. Det hela finns standardiserat i en standard som heter RSA. Om man kombinerar det med hashing enligt standarden SHA1 fås ”SHA1/RSA digital signature”. RSA beskriver hur kryptering/dekryptering kan ske med hjälp av två nycklar, som är sådana att det som krypterats med den ena nyckeln endast kan dekrypteras med den andra. Vidare kan man givet enbart den ena nyckeln inte med rimligt många beräkningar rekonstruera den andra nyckeln. Den ena nyckeln kallas då public key, och hålls inte på något sätt hemlig. Det som krypterats med en public key kan dekrypteras endast av den som har motsvarande private key. Den hålls mycket hemlig. När A vill skicka hemlig information till B får A alltså B:s public key och krypterar med den. Endast B kan dekryptera. Men om man gör tvärtom – krypterar med private key och levererar public key med materialet (filen) – så har man en mycket säker verifiering av avsändaren. Det som kan dekrypteras till något vettigt med A:s public key är garanterat krypterat av A och kommer alltså från A. En hashmetod skapar utifrån det digitala innehållet i assemblyfilen ett hashvärde – ett antal bytes som är beroende av filens innehåll, ett slags fingeravtryck. Hashvärdet krypteras sedan med private key och i manifest placeras både det krypterade hashvärdet och public key för dekryptering. Vid laddning av manifestet kan då klientapplikationen (det kan ske automatiskt i VES) göra det omvända – dekryptera hashvärdet, köra hashmetoden på filens innehåll, och jämföra. Om de båda hashvärdena är identiska är filen i sin helhet exakt den som levererades från den som utger sig vara leverantör. I alla miljöer med dll-filer finns problemet med utbytta filer. I Windowsmiljö är det klassiska problemet ”dll Hell” välkänt. En applika26
Kopiering av kurslitteratur förbjuden. © Studentlitteratur
1 En infrastruktur för programutveckling
tion är beroende av dll-filer, men plötsligt är någon av dem utbytt. Om klienten nöjer sig med att filen har ett visst namn, och kanske metoder med vissa signaturer, så exekveras de godtroget. Resultatet är ofta antingen annan programlogik, annat språk i användargränssnitt, etcetera – eller programkrasch. CLI gör det då möjligt att förse assemblies med digital signatur, så att användaren med mycket stor säkerhet kör just precis den komponent som klientapplikationen är kompilerad mot, och allt annat avvisas, även om det bara gäller en liten binär skillnad i filen. För att ge en utökad versionshantering finns också möjlighet att i manifest lägga in versionsnummer, och att i VES-implementationen ha XML-filer som styr versionskontroll. Man kan exempelvis tänka sig att versionsnummer i ett intervall accepteras vid laddning. För sådant krävs naturligtvis omfattande implementationsdetaljer, och standarden enbart antyder vad som är möjligt En applikation kan samtidigt utgöra en eller flera namnrymder. Namespace-deklarationer får vara återkommande i kodfiler. Men det är i praktiken oftast mera logiskt att ge varje komponent en egen namnrymd. I klientkod är det då möjligt att slippa skriva namnet på namnrymden vid varje typnamn, man kan ange en eller flera namnrymder som standardvärde med using. using Studentlitteratur.CSharp; using System; Minimal obj = new Minimal(); Vid laddning av ett assembly kompileras det till maskinkod. JITkompilering har jämfört med andra tekniker (slutkompilering direkt, eller motsatsen kompilering vid exekvering – tolkning) föroch nackdelar. Naturligtvis tar det tid och fördröjer därmed programstarten. Samtidigt ger det möjlighet att optimera koden för aktuell plattform fullt ut, så att exekvering blir desto snabbare. Den aktuella CLI-implementationen kan här erbjuda åtskilliga optimeringar. Ofta använda komponenter kan kompileras en gång för alla, programstart kan ske innan kompileringen är klar, etc. När ett program exekverar sker det ju normalt i en process, som skapas av operativet. En process består av en minnesrymd, en eller flera trådar, och de resurser som operativet tilldelat processen (fönster, Kopiering av kurslitteratur förbjuden. © Studentlitteratur
27
1 En infrastruktur för programutveckling
synkroniseringsobjekt, filer, etc). I CLI vill man dock inte låsa sig till det upplägget, utan kallar i stället företeelsen en AppDomain. Därmed kan man tänka sig andra upplägg. Exempelvis modellen från Java Virtual Machine, där ofta VM är en enda process och alla instanser av applikationer är trådar i samma process. Hur saker och ting ska gå till konkret är ju klokast att inte standardisera. Standarder för språk och programutveckling brukar så att säga inte låtsas om maskinen eller operativet. Virtual Execution System (VES) måste oavsett hur den är implementerad finnas i målmaskinen. Det måste finnas en JIT-kompilator för kompilering CIL till maskinkod, det måste finnas mekanism som sköter skräpsamlingen och det måste finnas mekanism som sköter laddningen av datatyper, och framförallt måste åtminstone ett minimum av klassbiblioteket finnas i CIL-format. Ett CLI-program är oftast en överraskande liten fil, vars mesta kod egentligen utgörs av klassbiblioteket, till vilket det alltid länkas dynamiskt. I en maskin utan VES är ett CLI-program enbart nonsens.
1.4 Skräpsamling En intressant egenskap i exekveringsmiljön är den automatiska skräpsamlingen (garbage collection). För programutvecklaren är skräpsamling något som förändrar tillvaron avsevärt, jämfört med att arbeta i miljö där man själv måste hålla ordning på de objekt som skapas så att de också raderas. Objekt som inte längre refereras ska i en miljö med skräpsamling raderas automatiskt. Utan skräpsamling måste man i den kod där objekt skapas själv hålla räkning på dem och radera objekt som inte längre behövs. I exempelvis C++ görs det med operatorn delete. När objekt raderas exekverar eventuellt en särskild metod som allmänt kallas destruktor. I den finns då kod som avallokerar andra resurser som objektet hanterar. Det kan vara andra objekt, filer, sockets, fönster etcetera. I VES finns skräpsamling. När klassobjekt skapas med new så som vi sett tidigare anropas procedurer i exekveringsmiljön som i sin tur 28
Kopiering av kurslitteratur förbjuden. © Studentlitteratur
1 En infrastruktur för programutveckling
allokerar minne. Samtidigt ger också exekveringsmiljön logiken för att spåra alla referenser till varje enskilt objekt (kan vara implementerat som en egen tråd). MyClass obj = new MyClass(); // använd objektet obj = null; När referensen försvinner eller sätts till annat värde blir antalet referenser till objektet en mindre. När inga referenser alls finns kvar är objektet föremål för skräpsamling. Situationen är i realiteten ofta mycket mera komplicerad eftersom flera objekt ofta kan hänga samman inbördes, kanske rent av i cirkulära referenskedjor. Algoritmer för skräpsamling måste kunna göra avancerad grafanalys för att hitta objekt som egentligen är övergivna trots att det finns referenser till dem. Skräpsamlaren i VES ska klara sådana situationer. För skräpsamling finns allmänt flera tänkbara designprinciper. Men kan tänka sig att objekt raderas i det ögonblick sista referensen försvinner, eller man kan tänka sig att hela strukturen av allokerade objekt traverseras då och då, eller kanske att radering av övergivna objekt sker bara då tillgängligt minne kommer under någon kritisk nivå. I VES-standarden sägs för säkerhets skull inte när skräpsamling ska ske. Vi är därmed utlämnade åt det faktum att vi inte kan förutsätta något alls om när objekt utan referenser faktiskt raderas. Det kan ske en klockcykel efter sista referensens försvinnande, eller när programmet avslutas, eller någon gång däremellan. I de fall det har någon betydelse när ett objekts destruktor exekverar måste vi alltså i egen kod ta hand om det problemet. Det är skräpsamlingens stora nackdel. I ett senare kapitel (om konstruktorer och destruktorer) ska en vedertagen design för ändamålet beskrivas. Men i de flesta klasser är skräpsamlingen problemfri. Bara att skapa objekt med new, och slarva bort dem efterhand! Observera att structer inte berörs av resonemanget här alls. Structer är antingen inlinemedlemmar i klasser eller structer (ingår i aggregat), eller stackvariabler. Stackvariabler raderas inte utan det minne de använder markeras bara som ledigt när det kodblock de skapats i exekverat klart. Kopiering av kurslitteratur förbjuden. © Studentlitteratur
29
2 Inledande detaljer
2 Inledande detaljer
I förra kapitlet fanns visserligen många kodexempel, men språket i sig har vi egentligen inte kommit fram till ännu, och det är ju det denna bok ska göra. Förhållandena är lite speciella genom att det är CLI som utgör grundlagar och som sätter sin tydliga prägel på allt vi ska göra i C#. Därav den omfattande inledningen. Först ska nu några språkkonstruktioner gås igenom, vilka är nödvändiga för att överhuvudtaget göra små exempelprogram. Det gäller de allra minsta detaljerna i syntaxen, och så gäller det Input/ Outputhantering (IO) mot terminal, det vill säga inmatning från användaren till variabler, och utmatning till text i ett terminalfönster. Syntaxreglerna på allra lägsta nivå är i korta drag följande: En sats är en eller flera uttryck som avslutas med ett semikolon. Raddelning i källkod har med några få undantag ingen betydelse, det är semikolonet som gäller. Tilldelning sker med ett enda likamedtecken. Överallt där en sats kan förekomma, kan ett antal satser inom klammerparenteser användas, och de kallas då ett block. if (var) { int a = 0; Method(var++, a); }
Kopiering av kurslitteratur förbjuden. © Studentlitteratur
31
2 Inledande detaljer
2.0.1 Variabler Variabeldefinitioner består av ett typnamn och en kommaseparerad lista av variabelnamn. Initiering är valfri och består av ett tilldelningstecken och ett uttryck. double InterestRate = GetCurrentInterest(), TemporaryInterest; Variabeln InterestRate initieras med returvärdet från ett metodanrop, variabeln TemporaryInterest är oinitierad. Om initiering saknas för en lokal variabel kommer kompilatorn att bevaka att variabeln tilldelas innan den används. Oinitierade stackvariabler har ju på assemblernivå ett värde från den variabel som råkade ligga på samma plats just innan, och därför måste stackvariabler initieras eller tilldelas i C#. För variabler som ingår i en klass (kallas då fält) gäller att automatisk initiering till värdet noll eller motsvarande sker. Där är alltså initiering valfri. Namn kan bestå av bokstäver, siffror och understrykningstecknet (_). Första tecknet får emellertid inte vara en siffra. Understrykningstecknet som första tecken är inte förbjudet, men används sällan så. Stilregler (regler om kodens utseende, utan verkan på hur programmet sedan uppför sig) är naturligtvis helt valfria, men i många sammanhang rekommenderas att namn görs ganska långa och informativa, gärna med användande av både gemena (små) och versala (STORA) bokstäver. Så kallad Pascalnotation rekommenderas allmänt. Då görs namn så att de kan läsas som flera sammanskrivna ord där varje börjar med versal. MittBeskrivandeVariabelnamn. Variabler av mycket tillfällig karaktär, såsom indexvariabeln i ett slinguttryck behöver inte ha beskrivande namn, de kan heta i eller x. Det finns också en konvention i många kodexempel att namn som inte är synliga i en klass har gemen första bokstav, medan namn som är synliga har versal första bokstav. Synlighet i klasser är ett ämne som kommer senare i boken.
32
Kopiering av kurslitteratur förbjuden. © Studentlitteratur
2 Inledande detaljer
2.0.2 Kommentarer Det finns inte mindre än tre sätt att skriva kommentarer i C#-kod. Den första är hämtad från C, den andra från C++ och den tredje är en variant av en uppfinning från Java. Innan vi tittar på syntax kan ett moralens ord på vägen vara på sin plats. ”Välskriven kod är självdokumenterande” säger man ibland och är då djupt självironiskt. Kod är aldrig självdokumenterande – det som är solklart idag är obegripligt några veckor senare. Även för den som skrev det. Den erfarenheten är brutalt entydig. Stilregler brukar därför inskärpa betydelsen av omfattande och gärna strukturerad dokumentation av koden i form av kommentarer. Många stilregler föreskriver hela formulär av kommentarer vid varje klass, metod eller annat. Viktigt är då att kommentarerna skrivs samtidigt med koden så att den verkligen blir initierad. En bra detalj i sammanhanget är också att idéer till förbättringar, misstankar om buggar och liknande funderingar också läggs in i koden som kommentarer. Den första syntaxen för kommentarer kommer från C. /* Kommentar som kan omfatta flera rader */ Syntaxen har ett problem – kommentarer kan inte nästlas. Ibland vill man tillfälligt kommentera bort hela sjok av kod som kanske ska designas om helt, eller som är buggig, eller som ska inaktiveras av annat skäl. Om den koden då innehåller kommentarer kommer första slutsekvens att tolkas som slut på kommentar och övriga kommentarsekvenser kommer i otakt och ger kompileringsfel. I C++ infördes därför enradskommentaren: // Kommentar while (expr) // Kommentar för while-satsen { ... } Kommentaren behöver alltså inte avslutas, utan gäller till radens slut. Båda dessa kommentartyper finns alltså i C# och båda används för de fall de passar bäst. Många editorer ger färgmärkning av
Kopiering av kurslitteratur förbjuden. © Studentlitteratur
33
2 Inledande detaljer
koden, där kommentarer då syns i avvikande färg och det är en mycket praktisk finess. Den tredje typen av kommentarer är lite mera avancerad och beskrivs därför i ett eget avsnitt 2.7 senare.
2.0.3 Literaler Literaler är värden i källkod. Följande skrivsätt finns: ’A’ // en char ”A” // en string true false // bool-värden 123 // en int med decimal notation 0x123 // en int med hexadecimal notation 1.23 // en double 1.23e8 // en double (1.23 * 108) Dessutom finns för tecken och strängar en uppsättning escapesekvenser, vilket är sättet att skriva oskrivbara tecken. Om man exempelvis i en sträng vill ha med citattecknet (”) måste det ske med hjälp av escapesekvensen \”, annars markerar ju citattecknet slut på strängen. Av liknande skäl kan man skriva \n för ny rad, \t för tabulering och \\ för själva \-tecknet. Yterligare ett tiotal sådana finns. En liten finess i sammanhanget är att man kan stänga av tolkningen av escapesekvenser genom att skriva ett snabel-a: @”I denna sträng betyder \ inget särskilt”
2.1 IO och strängar IO består traditionellt av mekanismer för ett program att ta in data från omvärlden, och att skicka ut data. I sammanhanget talar man då främst om terminalen och om filer. IO mot filer ska behandlas i samband med klassbiblioteket. Här ska bara minsta nödvändiga konstruktioner för inläsning från tangentbord till variabler, och 34
Kopiering av kurslitteratur förbjuden. © Studentlitteratur
2 Inledande detaljer
utmatning av variabelvärden till terminalfönster beskrivas, så att exempelprogram som åtminstone visar något på skärmen kan utvecklas. De centrala metoderna finns i en klass som heter Console i namnrymden System. De är statiska, vilket betyder att de anropas utan att man skapar något objekt av klassen. Utmatning är påtagligt enkelt: Console.WriteLine(”Hello World!”); Metoden har många finesser som gör den mycket flexibel, men riktigt hur den fungerar är för tidigt att beskriva. Däremot kan vi se några fler exempel på hur den kan användas. Anropet till WriteLine skrev en literalsträng (en sträng inom citattecken i källkod). Men parametern kan också vara en variabel av valfri datatyp. Alla de enkla structtyperna fungerar genast. int x = 100; Console.WriteLine(x); Texten ”100” skrivs på skärmen. Värdet blir alltså siffror med decimal notation. För flyttal gäller också decimal notation, med punkt som decimaltecken. Boolvariabler skrivs som false eller true. Ofta vill man ju kombinera literaltext (ledtext) med variabelvärden för att få en mera begriplig utmatning. För det ändamålet finns en särskild syntax med formatparametrar. int x = 100; Console.WriteLine(”Värdet i x är {0}”, x); Formatparametern {0} kommer att bytas ut mot det som WriteLine skulle gjort av enbart x i Console.WriteLine(x). Nollan betyder att det är den första av de följande parametrarna som används. Man kan använda valfritt antal formatparametrar och motsvarande parametrar i WriteLine, men antalet måste matcha och de måste vara numrerade 0 och uppåt. int x = 100; double y = 123.5; Console.WriteLine( ”Värdet i x är {0}, och i y {1}”, x, y); Kopiering av kurslitteratur förbjuden. © Studentlitteratur
35
2 Inledande detaljer
Formatparametrarna kan också styra formateringen på ett mycket omfattande sätt. Syntaxen för formatparametrar är { N [, M][: formatString] } där N är ordningsnumret, M är ett heltal som anger utmatningsfältets storlek (negativt tal ger vänsterställning, positivt högerställning), och formatString är en sträng med tecken som styr utfyllnad med mera. Vi lämnar formatString åt den särskilt intresserade att studera i standarddokumentationen. Det finns också ett annat sätt att skapa utmatningssträngar. Eftersom alla datatyper i CTS ärver från klassen Object, så har alla variabler en metod ToString(). Den returnerar en sträng som på ett eller annat sätt beskriver värdet. Oavsett vad x är så kan man alltså skriva x.ToString(). Strängen som returneras är av klassen string, och den i sin tur kan slås samman med andra string-objekt med +-operatorn. Därmed kan man skriva: Console.WriteLine(”Värdet i x är ” + x.ToString() + ” och i y ” + y.ToString()); Det ger samma resultat som exemplet ovan med formatparametrar. Allt som nu sagts om Console.WriteLine gäller också en metod som heter Console.Write. Enda skillnaden är att Write inte ger en radmatning efter utskriften.
2.1.1 Strängar Redan har vi ett par gånger stött på string-klassen. Den är inte en alldeles traditionell strängklass. Visserligen har den massor av de vanliga metoderna för stränghantering (kopiera, jämföra, slå samman, söka, ändra, etc.) men samtliga returnerar en ny string. Klassen är vad man kallar immutable, det vill säga att den teckenvektor som ett objekt består av inte kan förändras. I de fall man vill skapa en sträng som byggs steg för steg genom att dess teckenvektor manipuleras finns i stället en klass StringBuilder (avsnitt 10.6). En annan lite udda sak med string är att man inte ska skapa objekt genom att använda new om man vill initiera med en literalsträng.
36
Kopiering av kurslitteratur förbjuden. © Studentlitteratur
2 Inledande detaljer
Literalsträngar är nämligen som vi tidigare sett i sig string-objekt, så man bara tilldelar referensen. string s1 = ”Rätt”; // OK string s2 = new string(”Fel”); // fel Ytterligare en viktig sak att veta om tecken och strängar i CLI är att tecken alltid är enligt teckenuppsättningen Unicode. För den som har brottats med historiens flora av teckenuppsättningar såsom ASCII, ANSI, EBCDIC med flera är det en befrielse att det problemet med otillräckliga teckenuppsättningar äntligen är löst. Unicode är 16-bitars tecken i en uppsättning som täcker all världens språk. Inga problem längre med å, ä och ö! Notera därmed att datatypen char är en 16-bitars datatyp, och att den inte kan tilldelas från någon heltalstyp. Däremot sker implicit typomvandling från char till de heltalstyper som är större (ushort och större).
2.1.2 Inläsning Så över till formaterad inläsning från terminal, det vill säga från tangentbord till variabler. Utbudet av möjligheter här är överraskande små och primitiva. Den bittra sanningen är att formaterad inläsning, så som den finns i många andra miljöer, inte finns här! Metoden Console.ReadLine() läser från tangentbordsbufferten då användaren slår retur. ReadLine returnerar en sträng i form av ett string-objekt. string userString = Console.ReadLine(); Nu återstår att parsa strängen så att den kan användas för att tilldela en variabel något värde. Klassen Convert har en mängd sådana metoder för parsing till heltal, flyttal och annat. int userInt = Convert.ToInt32(userString); Alla metoderna är statiska och har namn som börjar med ”To” och därefter ett typnamn från klassbiblioteket. Designen av dessa metoder följer en princip: parsingen lyckas endast om samtliga tecken matchar ändamålet. Parsing till heltal
Kopiering av kurslitteratur förbjuden. © Studentlitteratur
37
2 Inledande detaljer
förutsätter att samtliga tecken är siffror 0–9. Inte ens ett blanktecken accepteras. Om användaren inte uppför sig perfekt, utan matar in en bokstav av misstag genereras ett undantag. Man bör följaktligen anropa To-metoderna inom ett try-block och ta hand om undantaget i ett catch-block. För att inte fastna i detaljer kommer exempelkod framöver inte att ta hand om den sortens felsituation. Vi låtsas tills vidare att vi lever i programutvecklarens paradis, där alla användare gör precis som vi tänkt att de ska. Ett exempel på ett litet program som använder terminal-IO kan då se ut så här: namespace Studentlitteratur.CSharp { using namespace System; class IODemo { static void Main() { Console.Write(”Skriv in ett heltal: ”); string s = Console.ReadLine(); int left = Convert.ToInt32(s); Console.Write(”Skriv ett till ”); s = Console.ReadLine(); int right = Convert.ToInt32(s); Console.WriteLine(”{0} + {1} = {2}”, left, right, left + right); } } } Funktionaliteten i Convert-klassens metoder finns också i respektive datatyp i form av en metod Parse. Det finns en Int32.Parse, en Boolean.Parse och så vidare. Det är en smaksak vilken man vill använda, Convert.ToInt32(string) gör samma sak som Int32.Parse(string). Det finns också möjligheter att styra parsingen genom att använda varianter av dessa metoder, vilka då tar en extra parameter av typen
38
Kopiering av kurslitteratur förbjuden. © Studentlitteratur
2 Inledande detaljer
IFormatProvider. Se mera detaljer om den datatypen i avsnitt 10.8 och i standarddokumentationen. Slutligen ska påpekas att terminalapplikationer är en mycket liten och borttynande kategori program. De finns numera nästan bara i läroböcker. Verklig mjukvara är komponenter, server services, fönsterklienter, inbyggda system, webapplikationer och annat som inte använder terminal-IO. Men för att göra små lärorika testapplikationer är de oöverträffade.
2.2 Main-metoden Varje applikation som ska startas i en instans måste ha en Mainmetod. En CLI-applikation startar genom att den JIT-kompileras, VES skapar en AppDomain, och Main anropas som startpunkt. I kompilerade språk såsom C++ motsvarar detta att programfilens maskinkod laddas i en ny process och Main anropas av operativsystemet. En första anmärkningsvärd sak är att Main liksom alla andra metoder måste ingå som metod i en klass (eller struct). För att kunna anropas utan objekt måste den vara static. Synligheten däremot är valfri. (Synlighet och static-metoder ska egentligen förklaras ordentligt senare). Det har ingen som helst betydelse vilken klass i applikationen som Main ingår i. Man kan göra en egen nonsensklass endast för ändamålet. En andra anmärkningsvärd sak är att det får finnas flera Main-metoder i applikationen. Man anger då med en flagga till kompilatorn vilken klass’ Main-metod som faktiskt ska användas. Det är inte ovanligt att man för teständamål i åtskilliga klasser skriver en Main med lite kod som skapar några objekt av klassen och provkör några metoder, kanske som illustrativa exempel på hur klassen kan användas, och med spårutskrifter till terminal. Main kan vara deklarerad utan parametrar, men den kan också ta en string-vektor som parameter. Kopiering av kurslitteratur förbjuden. © Studentlitteratur
39
2 Inledande detaljer
static void Main(string[] args) { ... } Om den gör det kommer VES att fylla den med innehåll från den kommandorad som startar programmet. Det första elementet i vektorn (args[0]) kommer att innehålla den första flaggan från kommandoraden, och så vidare. Observera att själva kommandot inte kommer in som parameter såsom sker i C/C++. Framöver ska vi se typiska sätt att traversera vektorer och andra samlingar, men för att visa ett realistiskt sätt att läsa kommandoradsparametrar kan vi ändå gå händelserna lite i förväg och visa en foreach-sats: static void Main(string[] args) { foreach(string arg in args) Console.WriteLine(arg); } Returtypen på Main kan som synes vara void. Ett enda alternativ är tillåtet, nämligen int. Märkligt nog så är skillnaden mycket liten. I båda fall returneras nämligen ett heltal till VES! Om Main är void returneras värdet 0, vilket indikerar normalt programavslut när Main antingen når slutklammern eller när satsen return; påträffas i Main. Det är tillåtet att implementera VES så att returkoder från Main används, men det är också tillåtet att ignorera dem. Om den aktuella VES använder returvärdet kan följaktligen Main deklareras med returtypen int och innehålla satser såsom return 1; eller return var;
2.3 Typomvandlingar Ett heltal kan hållas i en heltalsvariabel av exempelvis datatypen byte eller int. Skillnaden är att en byte inte kan rymma lika stora 40
Kopiering av kurslitteratur förbjuden. © Studentlitteratur
2 Inledande detaljer
tal som en int. Är det då rimligt att en byte får tilldelas en int – eller att en int får tilldelas en byte? I somliga språk är det tillåtet att göra även de tilldelningar som kan innebära overflow, det vill säga att värdet i den tilldelande variabeln helt enkelt inte får plats i den tilldelade. Resultatet i den tilldelade blir då i praktiken odefinierat eftersom bara de bitar som får plats används och de som inte får plats inte används. I C# är alla tilldelningar som inte kan ge overflow tillåtna, och sker utan anmärkning från kompilatorn. Tilldelningar som kan ge overflow ger ett kompileringsfel. Det gäller alla typer av heltal och flyttal. Mera formellt fungerar det så att vid tilldelning av en variabel av en datatyp till en variabel av annan datatyp gör kompilatorn en typomvandling av den tilldelande variabeln. Typomvandling betyder att en variabel tillfälligt används som om den hade varit av annan typ. Mekanismen gäller inte alls bara vid tilldelning, utan i alla situationer där det skulle funnits en variabel av en viss datatyp men finns en variabel av annan datatyp. När typomvandlingen sker automatiskt kallas den implicit typomvandling. För alla de enkla datatyperna finns en lista över andra datatyper som den aktuella datatypen får typomvandlas till, men i stället för att lära sig rabbla den kan man lära sig principen: implicit typomvandling sker i alla kombinationer som säkert inte påverkar värdet. int var = 1000000; byte b = var; // kompileringsfel long l = var; // OK double d = var; // OK bool b = var; // ingen typomvandling till bool! Inte så sällan vill man ändå använda en variabel så att den typomvandlas åt ett riskabelt håll, det vill säga till en mindre datatyp. Man bör då vara säker på att värdet faktiskt kan hållas av måltypen. Syntaxen för sådan explicit typomvandling är: byte b = (byte) var; // var är en int
Kopiering av kurslitteratur förbjuden. © Studentlitteratur
41
2 Inledande detaljer
Vi tvingar därmed kompilatorn att acceptera typomvandlingen. Om värdet i alla tänkbara fall inte är för stort, eller negativt när det typomvandlas till en icke-signed datatyp, är problemet löst. Om å andra sidan situationer kan uppstå då värdet inte ryms, utan overflow uppstår, finns i språket en operator för att göra typomvandlingen kontrollerad. Den heter checked och innebär att när overflow uppstår vid exekvering så genereras ett undantag. int var = 1000000; checked (byte b = (byte) var); // undantag genereras! Det är också möjligt att genom en flagga till kompilatorns kommando aktivera checked för alla explicita typomvandlingar. Då kan man också undanta en eller annan typomvandling från kontrollen genom att skriva unchecked. Checked och unchecked kan också följas av klammerparenteser så att ett antal satser kontrolleras mot overflow. Explicit typomvandling kan ske mellan alla heltalstyper och flyttalstyper. Men det går inte att göra vad som helst – man kan exempelvis inte ens med explicit typomvandling tilldela en bool värdet från en int! När man skriver flyttal som literaler får literalen datatypen double, vilket gör att den inte kan användas där det skulle varit en float och faktiskt inte heller där det skulle varit en decimal. Decimal är inte implicit kompatibel varken med float eller double. Den har nämligen mindre omfång (men större precision). För de typomvandlingarna finns särskild syntax: float f; f = 1.23F; // 1.23 tolkas som en Float decimal d; d = 1.23M; // 1.23 tolkas som en deciMal Utan F respektive M ger dessa tilldelningar kompileringsfel. När vi senare studerat klasser och arv ordentligt ska vi se att typomvandling mellan klassreferenser följer särskilda regler, där man kan använda också ett par andra operatorer för att göra det på ett säkert sätt. Det finns också typomvandling för en del andra datatyper, och 42
Kopiering av kurslitteratur förbjuden. © Studentlitteratur
2 Inledande detaljer
en mekanism för typomvandling från struct till object-referens, vilken är lite excentrisk och kallas ”boxing”.
2.4 Operatorer Operatorer är symboler som i allt väsentligt är metoder. Att se operatorer som metodanrop är en bra mental förberedelse för ett kommande kapitel om operatoröverlagring. Där kommer vi att konstatera att operatorer i detta språk alltid är metoder, närmare bestämt statiska metoder i respektive klasser och structer. Fördelen med operatorer kontra vanliga metoder är att de anropas på ett sätt som mera anknyter till traditionella skrivsätt. Det är mera naturligt att addera tal med a + b än med Add(a, b). Uppsättningen operatorer framgår av följande tabell: Kategori
Operatorer
Anmärkning
Aritmetiska
+ - * / ++ -- %
Beräkningar
Logiska
&& || !
Returnerar true eller false
Jämförande
< >= == !=
Returnerar true eller false
Bitvisa
& | ^ ~ >
Skapar bitmönster
Typhantering
() is as typeof
Typkontroll och typomvandling
Övriga
. = () [] new ?:
En operator och en eller två operander kallas allmänt ett uttryck, och ett uttryck har alltid värde och typ. Operatorer som tar en operand kallas ofta unära, och de som tar två operander kallas binära. Vissa operatorer (+ och -) finns både i unär och binär variant. I en sats med flera uttryck innehållande operatorer finns regler för i vilken ordning de exekveras. Operatorerna sägs ha olika prioritet. I många fall är prioriteten självklar, exempelvis exekverar multiplikation och division naturligtvis före addition och subtraktion. Men ofta kan man bli osäker på om den kod man just skriver kommer att exekvera i önskad ordning. Den absolut bästa lösningen är då att Kopiering av kurslitteratur förbjuden. © Studentlitteratur
43
2 Inledande detaljer
omge uttryck med parenteser. Det ger både den rätta logiken och gör dessutom koden tydligare att läsa och förstå. int x = 5, y = 3, z = 1; int sum1 = x * y + z; // 16 int sum2 = x * (y + z); // 20 bool a = false, b = false, c = false; if (a && b == c) ... // false if ((a && b) == c) ... // true När en operand står mellan två operatorer med samma prioritet gäller regler för vilken ordning de då exekveras, vänster-till-höger, eller höger-till-vänster. Det kallas associativitet. Här är reglerna möjliga att memorera – alla utom tilldelningsoperatorn är nämligen vänster-till-höger. a() + b() - c(); // anropas i ordningen a() b() c() a = b = c; // b tilldelas c, därefter: a tilldelas b De aritmetiska operatorerna är implementerade i åtskilliga klasser och structer i klassbiblioteket. Naturligtvis alla enkla heltals- och flyttalstyper, men också olika sorters klasser. Man kan exempelvis konkatenera strängar genom att skriva s1 + s2 och då få en ny sträng som är en sammanslagning av strängarna s1 och s2. Ett undantag är modulooperatorn (%) som ger resten vid heltalsdivision och följaktligen bara accepterar operander av heltalstyp. Den är särskilt användbar när man vill undersöka om två tal är jämnt delbara. Då är ju (a % b) lika med noll. Lite speciella är operatorerna ++ och -- på det sättet att de kan vara prefix (före operanden) eller postfix (efter operanden). Logiken är att de räknar upp operanden ett steg. För heltal och flyttal räknar de upp/ner med ett, men i andra klasser kan de ha ganska fantasifull implementation. Skillnaden på prefix och postfix variant är att prefix returnerar värdet i operanden efter upp/nedräkning, medan postfix returnerar värdet före upp/nedräkning. int x = 1; Console.WriteLine(x++); // skriver 1, därefter 44
Kopiering av kurslitteratur förbjuden. © Studentlitteratur
2 Inledande detaljer
// är x lika med 2 Console.WriteLine(x); // skriver 2 Console.WriteLine(++x); // skriver 3 De logiska operatorerna används ofta för att kombinera uttryck. Bland de enkla datatyperna är de definierade endast för booltypen. Vanligt är då att operanderna är uttryck som returnerar bool, exempelvis a == b, eller a > b. if (a >= 2 && a > 11; Console.WriteLine(
46
Kopiering av kurslitteratur förbjuden. © Studentlitteratur
2 Inledande detaljer
”Bit 11–13 i pattern har värdet {0}”, temp); Skiftoperatorn (>>) flyttar bitmönstret ett antal steg. Inskiftade bitar är 0. När de intressanta bitarna är längst till höger och alla övriga är 0 kan värdet användas som ett vanligt heltal. Operatorerna för typhantering gör det möjligt att vid exekvering undersöka vilken klass ett visst objekt kommer från. Ändamålet blir påtagligt när vi längre fram studerat arv, och ser att man ofta tvingas göra explicit typomvandling av referenser. Då måste man först kontrollera att typomvandlingen är giltig, det vill säga att objektet verkligen är av en viss datatyp. Det gör man med hjälp av is, as eller typeof och en Object-metod som heter GetType. Parenteserna runt ett typnamn ger ju explicit typomvandling, och det kan man då göra säkert om man först testat med dessa operatorer. De beskrivs utförligare i samband med arv. Bland de övriga operatorerna är några lite udda och några kanske man inte genast ser som operatorer. Punktoperatorn först, ger medlemsval. I C# används den för alla sorters medlemsval, för att skilja namnrymder i en sökväg, för att skilja klass från medlem, och för att skilja objektreferens eller structnamn från medlem. Tilldelningsoperatorn är en välkänd företeelse som kanske inte kräver någon förklaring, men en par saker kan ändå betonas. För det första kan tilldelningsoperatorn inte överlagras, och för det andra är klassobjekt referenser och structer är värden. Det är därmed alltid så att tilldelning av referenser är tilldelning av identitet och tilldelning av structer är tilldelning av värde. int a = 5; // int är en structtyp int b = 10; a = b; // a och b är olika objekt med samma värde string s1 = ”Ett”; // string är en klasstyp string s2 = ”Två”; s1 = s2; // s1 och s2 refererar till samma objekt Tilldelningsoperatorn kan också kombineras med åtskilliga av de andra operatorerna för att få enklare skrivsätt. Det är aldrig nödvändigt, men används i praktiken mycket ofta.
Kopiering av kurslitteratur förbjuden. © Studentlitteratur
47
2 Inledande detaljer
a = a + 5; // helt OK men oftast skriver man: a += 5; // exakt samma sak Nu till en märkligare operator. Parenteserna i denna kategori operatorer är inte samma som parenteserna i samband med typhantering. Här utgör ett parentespar metodanropsoperatorn. Namnet på en metod (utan parenteser) är egentligen en referens till metoden. Med parenteser och eventuella parametrar däri blir det plötsligt ett anrop till metoden. Metodanropsoperatorn kan inte överlagras, men de särskilda mekanismerna delegerare och notifierare (delegates och events) ger motsvarande logik. De är ämne för ett helt kapitel senare. Indexoperatorn ([]) är en klassiker för att indexera i vektorer i många språk. I C# änvänds den mycket flitigt i allehanda klasser. Alla klasser som har någon karaktär av samling har indexoperator så att man kan traversera eller söka bland objekten i samlingen. I vektorer indexeras med ett heltal som börjar med 0, men i andra klasser kan indexet vara av andra datatyper och det kan också vara flera än ett index. Den kan uppenbart överlagras. Operatorn new är egentligen ett anrop till procedurer i VES minneshanterare. Den returnerar ju en referens till det objekt som den skapar. Den kan inte överlagras. Slutligen den syntaxmässigt egendomliga ?:-operatorn. Den är en historisk rest från C-språket och varken nödvändig eller elegant. I uttrycket a ? b : c har den effekten att uttrycket returnerar b om a är true och c om a är false. Den kan alltså användas i stället för en if-else, och ger ibland en enklare lösning. I C-programmering brukar den vara särskilt intressant för den som vill tävla i att skriva obegriplig kod (jo, sådana tävlingar finns!), eftersom den dessutom kan nästlas i flera nivåer utan att parenteser behövs...
48
Kopiering av kurslitteratur förbjuden. © Studentlitteratur
2 Inledande detaljer
2.5 Vektorer Lite tillspetsat kan man här chocka läsaren genom att påstå att vektorer inte finns! En vektor är traditionellt ett antal variabler skapade intill varandra i minne, och av samma datatyp. I föregångaren C++ är vektorer tämligen primitiva. När man indexerar sig i en vektor med indexoperatorn [] betyder det bara att man ger en offsetadress från den adress som symboliseras av vektorns namn. Ingen kontroll sker av att man indexerar sig inom det antal element som skapades, och indexering utanför vektorn är en vanlig felkälla där. Ett annat problem med vektorer i C++ är att det inte finns någon enkel syntax för initiering av vektorer. I C# finns en vektorsyntax som liknar C++, men den är bara språklig förklädnad för hantering av objekt av klassen Array! Eller, för att vara mera korrekt: objekt av en klass som automatiskt härleds från klassen Array. En vektor i C# blir alltså automatiskt ett objekt av en samlingsklass från klassbiblioteket, och klassen tar naturligtvis hand om all möjlig implementationsproblematik, däribland indexeringen. En vanlig endimensionell vektor skapas enligt följande exempel: int[] vec = new int[100]; Observera att vektorns namn är en referens. Vektorn är ett klassobjekt som skapas med new. Det är möjligt att skapa vektorreferensen i en sats och tilldela den vektorobjekt i andra satser. Referensens datatyp innehåller ingen information om vektorns längd. Elementen i vektorn initieras automatiskt till noll eller motsvarande. Det finns inget sätt att initiera dem till annat gemensamt värde. Däremot finns den otympliga syntaxen från C++ att initiera elementen i en initieringslista: int[] vec = new int[5] { 1, 2, 3, 4, 5 }; Den möjligheten är mest relevant för små vektorer och inte så kul för vektorer med några tusen element. Då är en iterationssats såsom for-satsen mera praktisk. Initieringslistan måste innehålla precis så många värden som det finns element i vektorn.
Kopiering av kurslitteratur förbjuden. © Studentlitteratur
49
2 Inledande detaljer
Det finns också ett par varianter där man inte anger antalet element, utan får en vektor med så många element som i initieringslistan. int[] vec = new int[] { 1, 2, 3, 4, 5 }; // eller: int[] vec = { 1, 2, 3, 4, 5 }; Antalet element som vektorn ska innehålla behöver inte vara en literal, förutsatt att initieringslista inte används. Alla typer av uttryck som ger ett heltal kan användas. Vektorns storlek kan alltså beräknas vid exekvering. Men vektorn är inte dynamisk, den kan inte allokeras om vid exekvering utan behåller sin storlek. I klassbiblioteket finns en annan klass för de fall man behöver en dynamisk vektor, den heter ArrayList och ska beskrivas i kapitlet om klassbiblioteket (kap 10). Indexeringen sker i denna typ av vektor alltid med ett heltal, och första elementet har nummer 0. vec[3] = 100; Console.WriteLine(”Fjärde elementet är {0}”, vec[3]); Indexering utanför vektorn genererar ett undantag, vilket ju stoppar programmet om det inte hanteras. Därmed är en riktigt klassisk buggtyp från C/C++ eliminerad. När en vektor skapas på detta sätt översätter kompilatorn koden till det den egentligen är, alltså skapandet av ett Array-objekt. Array vec = Array.CreateInstance(typeof(int), 100); Elementtypen ges med typeof-operatorn och ett typnamn, andra parametern är antalet element som ska skapas. Det är fritt fram att skapa vektorer också på detta sätt, men stilmässigt rekommenderas den särskilda C#-syntaxen som i de förra exemplen. Hur som helst så har en vektor ett antal intressanta metoder. Några exempel: vec.Length // antal element (i alla dimensioner) vec.GetLength(0) // element i första dimensionen Array.IndexOf(vec, 5) // söker första element med 50
Kopiering av kurslitteratur förbjuden. © Studentlitteratur
2 Inledande detaljer
// värdet 5 Array.Sort(vec); // sorterar elementen Det finns också metoder för kopiering, även till/från andra slags samlingar, nollställning av alla element, reversering med mera. Men designen tillåter tyvärr inte metoder för initiering av medlemmarna. Det gör man ofta med tilldelning i en for-sats: int num = ... // hämtas från omvärlden MyClass[] objvec = new MyClass[num]; for (int i = 0; i < objvec.Length; i++) objvec[i] = new MyClass(); Här skapades en vektor av referenser till klassobjekt. Elementen får då automatiskt värdet null. Varje element tilldelas sedan i for-satsen ett objekt som skapas med new. Värdet i objvec.Length är ju i exemplet lika med num, men Length ger alltid korrekt längd även om vektorn kommit in som referens i metodanrop etc. Vektorer kan som läsaren säkert anat också vara flerdimensionella. Exempel på en matris av heltal: int[,] matrix = new int[5, 10]; Referensen deklareras med kommatecken som anger antalet dimensioner, och i new-satsen anges antalet element i respektive dimension. Ett kommatecken betyder alltså två dimensioner. Array-klassen har överlagrade varianter av CreateInstance som skapar motsvarande vektorobjekt. for(int dim = 0; dim < matrix.GetLength(0); dim++) for(int el = 0; el < matrix.GetLength(dim); el++) Console.WriteLine(matrix[dim, el]); Observera att Length ger totala antalet element i en flerdimensionell vektor, här används därför GetLength() som gör det möjligt att få veta antal element per dimension. Slutligen finns en möjlighet till att skapa flerdimensionella vektorer, nämligen vektor av vektorer. För de ändamål då man vill ha olika många element i andra dimensionens kolumner är det då möjligt, samtidigt som designregler brukar avråda. Gör hellre flerdimensionella vektorer som i förra exemplet. Annars är syntaxen: Kopiering av kurslitteratur förbjuden. © Studentlitteratur
51
2 Inledande detaljer
int[][] vecvec = new int[10][]; for (int i = 0; i < vecvec.GetLength(0); i++) vecvec[i] = new int[100]; vecvec[3][45] = 123; Varje element i första dimensionen är nu en vektorreferens. I exemplet är alla vektorerna i andra dimensionen lika långa – det hade varit enklare att göra en int[,].
2.6 Uttryck och satser Med begreppet uttryck menar man en minsta teckensekvens i kod som vid exekvering beräknar ett värde och därmed har en datatyp. En operator med operand(er) är ett uttryck. Ett metodanrop är också ett uttryck. Uttryck bildar satser och satser avslutas med semikolon. I detta språk liksom i föregångarna C och C++ är det tradition att man nästlar uttryck ganska flitigt. Man gör så att säga flera saker i varje sats. I andra mindre flexibla språk är det vanligt att man undviker nästlade anrop och i stället har flera temporära variabler. Stilmässigt bör man nästla ”lagom”. Allt för många nästlade uttryck i en sats blir ju enbart svårare att läsa, det gör inte på något sätt programmet bättre. Är man osäker är det alltid bättre att göra saker steg för steg, lugnt och försiktigt. För vissa ändamål finns i alla språk speciella satser med egen syntax. Det gäller typiskt villkorade satser och iterationer (slingor, loopar). Utbudet av sådana satser är i C# ovanligt stort, och i många fall är det en ren smak- eller stilsak vilken man väljer. Sats
Logik
Anmärkning
if-else
Villkorad exekvering
Villkoret är en bool, else-gren är valfri, kan nästlas
switch
Villkorad exekvering
Villkor av andra typer, bättre än nästlade ifelse
while
Iteration på villkor
Villkoret testas först, den enklaste grundvarianten
52
Kopiering av kurslitteratur förbjuden. © Studentlitteratur
2 Inledande detaljer
Sats
Logik
Anmärkning
do-while
Iteration på villkor
Villkoret testas sist, första varvet utan villkor
for
Iteration med index
Som while med utökning av indexhantering
foreach
Iteration med enumerator Enklast, när den är möjlig. Förutsätter stöd i elementen, ej för tilldelningar
2.6.1 If-satsen Den riktiga klassikern här är if-satsen. Det finns inte många rader programkod i något av de närbesläktade språken utan en eller annan if-sats. Logiken är enkel: om villkoret är true exekveras satsen eller blocket, och om villkoret är false och det finns en elsegren exekveras satsen eller blocket där. Eftersom if kan nästlas i obegränsat antal nivåer är det också ganska vanligt med mycket stora if- else if - else if.... if (x < { ... } else if { ... } else if { ... } else // { ... }
0)
(x < 10) // mellan 0 och 9
(x < 20) // mellan 10 och 19
>= 20
Här är if-else nödvändigt, eftersom bara en av grenarna ska exekveras. Stilmässigt borde man i sådan kod göra ett nytt indrag för
Kopiering av kurslitteratur förbjuden. © Studentlitteratur
53
2 Inledande detaljer
varje if-else, men då skulle man snart hamna orimligt långt in på raderna, så man brukar inte indentera if-else. En situation som kan uppstå i komplicerade if-else-satser är att en else-gren av misstag kommer att hänföras till fel if. Kompilatorn bryr sig inte om eventuell indentering och är ju tyvärr inte heller tankeläsare, så var noga med att skapa block i alla grenar, då syns logiken bättre. En else gäller alltid närmast föregående if. Notera att villkoret måste ha datatypen bool, och att ingen implicit typomvandling till bool finns för de andra enkla datatyperna. Oftast använder man därför någon operator som returnerar bool för att skapa ett villkorsuttryck.
2.6.2 Switch-satsen I stora nästlade if-else-satser är det inte ovanligt att man upptäcker att samma kodsekvenser förekommer på flera ställen, eller att villkoren blir komplicerade att beskriva. Då kan en alternativ sats vara relevant: switch-satsen. Logiken i switch-satsen är att ett uttryck jämförs med ett antal literaler. Varje literal utgör en gren, och den matchande literalens gren exekveras. Det är ungefär vad man ofta också åstadkommer med jämförelser i en if-else-kombination. switch (x) { case 1 : Console.WriteLine(”Ett”); break; case 2 : Console.WriteLine(”Två”); break; ... default: Console.WriteLine(”Stort”); break; } Några regler: uttrycket som ska jämföras måste vara en heltalstyp eller en string. En gren kan vara tom eller innehålla en eller flera satser. En defaultgren kan finnas sist, den exekverar om ingen matchande gren fanns. Om en gren är tom exekverar nästa case i stället. Varje gren måste avslutas med break eller goto.
54
Kopiering av kurslitteratur förbjuden. © Studentlitteratur
2 Inledande detaljer
Avslutad med break avslutar en gren hela switch-satsen. Avslutad med goto kan grenen åstadkomma hopp i switch-satsen, och det är med den finessen switch blir riktigt användbar. Ett lite mera avancerat exempel: switch { case case case case
(str)
”Anders” : ”Bertil” : ”Cecilia”: Console.WriteLine(”ABC”); break; ”David” : Console.WriteLine(”David och ”); goto case ”Erik”; case ”Erik” : Console.WriteLine(”Erik”); break;
} De första två case-grenarna är tomma, vilket gör att om någon av dem väljs kommer den tredje grenens kod att exekvera. Grenarna med värdena ”David” och ”Erik” illustrerar hur man kan undvika att skriva samma kod på flera ställen, vilket man skulle gjort med en motsvarande if-else. Det är visserligen inte mycket som är gemensamt här, men i realistiska fall kan det vara åtskilliga rader kod. Om ”David” är fallet kommer ”David och Erik” att skrivas, om ”Erik” är fallet kommer enbart samme ”Erik” att skrivas. Ett case gäller alltså även som etikett för en goto. Goto har just den effekten att exekveringen kan hoppa till annan plats i samma metod, genom att platsen märkts med en etikett. Möjligheten att göra så är egentligen inte begränsad till switch-satser, men det brukar avses vara amatörmässig stil att använda goto – utom just i switch-satser. Vi nöjer oss därför med att illustrera goto just i det fallet. Några extra ord om det där med att skriva samma kodsekvens på flera ställen. Det kan inte nog betonas hur viktigt det är att undvika det. Skälet är inte så mycket att programmet blir större eller att koden blir onödigt omfattande. Problemet med duplicerad kod är att det är en tidsinställd bomb när programmet ska underhållas. När någon liten del av programlogiken i framtiden ska förändras är risken stor att den utvecklare som då gör det inte hittar alla ställen, utan tror att ändringen är gjord när i själva verket några rader lika-
Kopiering av kurslitteratur förbjuden. © Studentlitteratur
55
2 Inledande detaljer
dan kod undsluppit ändringen. Man får då ett annat program än man tänkt sig.
2.6.3 While-satsen Den enklaste och mest grundläggande satsen för iterationer är while. Egentligen skulle man kunna klara sig hela livet med bara while. Alla de andra iterationssatserna är varianter eller utökningar av while. Logiken är att ett villkorsuttryck av datatypen bool utvärderas före varje varv. Så länge det är true körs ett varv till. while (TestMethod(x)) { ... } Villkoret är här ett metodanrop till en metod som antas returnera en bool. Satserna i slingan exekverar och därefter sker ett nytt anrop till TestMethod(). Så mycket mer är inte att säga om while-syntaxen, men vi ska i samma veva ta upp ett par andra nyckelord i språket och ett vanligt designproblem. Problemet består i att villkoret ibland kan vara ganska komplicerat. Man kan då ofta ändå formulera det genom att kombinera uttryck med olika jämförelseoperatorer. Men ett mera praktiskt sätt kan vara att med if-satser inuti slingan testa förhållanden och därifrån avbryta slingan. För att avsluta hela iterationen, det vill säga avbryta pågående varv på stället och inte starta ett nytt varv finns nyckelordet break. Det är samma break som vi tidigare såg i switch-satsen, och med samma logik: hopp till satsen efter denna sats kropp (block). En annan möjlighet är att avbryta pågående varv och börja om på ett nytt. Nyckelordet är då continue och det innebär att hopp sker till nytt varv med ny utvärdering av villkoret.
56
Kopiering av kurslitteratur förbjuden. © Studentlitteratur
2 Inledande detaljer
Genom att kombinera break och continue klarar man mycket komplicerade villkorskombinationer. Det är inte ovanligt att man löper linan fullt ut och sätter villkoret till konstant true för att hantera avbrytandet enbart med break. while (true) { if (state == 1) { ReadInput(); if (input != ’Y’) { state = 2; continue; } else break; } else if (state == 2) { ... } ... else { state = 1; continue; } ReadInput(); if (input == ’X’) break; else ConvertInput(); } Utan att fördjupa sig i logiken i detta fragmentariska exempel kan det illustrera en slinga som driver en tillståndsmaskin. Beroende av tillstånd och input ska tilstånd ibland förändras, ibland ska nytt input hämtas och så vidare. Slingan exekverar tills ett tillstånd/ input uppstår som ger break. Kopiering av kurslitteratur förbjuden. © Studentlitteratur
57
2 Inledande detaljer
2.6.4 Do-while-satsen Vid enstaka tillfällen behöver man göra en iteration som så att säga skapar sitt villkor i första varvet. Den bör då testa villkoret efter varje varv i stället för före varje varv. När slingan exekverar är skillnaden mot en while-sats då egentligen obefintlig, men första varvet exekverar oberoende av villkor. Den körs alltså minst en gång. Syntaxen är: string s; do { s = Console.ReadLine(); ... } while (s != ”return”); Här är just ett sådant fall då slingan måste exekvera en gång innan det finns något att skapa villkor av. Notera också att string-referensen s måste skapas utanför slingan, annars existerar den inte i villkorssatsen. Lokala variabler upphör ju att existera då deras omslutande block exekverat. Lokala variabler i block som utgör iterationer skapas om på nytt för varje varv.
2.6.5 For-satsen En mycket vanlig situation är att en slinga ska exekvera ett visst antal varv, känt vid kompilering eller åtminstone då första varvet påbörjas. Det kan lösas med en while-sats och en indexvariabel. int i = 0; while (i < 100) { ... i++; } Just det upplägget motsvaras av en standardsats som finns i många språk och är mycket populär: for-satsen.
58
Kopiering av kurslitteratur förbjuden. © Studentlitteratur
2 Inledande detaljer
for (int i = 0; i < 100; i++) { ... } De två exemplen ger exakt samma program men i realiteten väljer man nästan alltid en for-sats. Förutom att vara en praktisk kombination har for-satsen några små extra egenheter. Alla tre delarna kan utelämnas – ersättas av tom sats (;) – och då kommer villkoret att tolkas som true. Vidare kommer ett eventuellt continue i slingan att exekvera den tredje delen, vilket man ju vill. Slutligen kommer indexvariabeln att upphöra att existera efter for-satsen trots att den inte skapats inom något block. Man kallar därför nästan alltid indexvariabeln i. Det finns en liten egendomlig syntaxregel som säger att överallt där en sats ska förekomma kan flera satser finnas om de skiljs åt av kommatecknet. Den är bra för att åstadkomma obegriplig kod (så kallad write-only-code), och har nästan bara ett vettigt tillämpningsområde, nämligen i for-satsen. Det kan vara så att man vill ha två indexvariabler och att båda ska räknas upp parallellt. Om man då har flera continue i slingan blir det extra krångel. Då är lösningen att i den första delen skapa båda indexvariablerna och i tredje delen räkna upp båda med hjälp av kommaoperatorn. for (int i = 0, j = 0; i < max; i++, j++) { ... if (...) continue; // både i och j räknas upp ... }
2.6.6 Foreach-satsen Som om det inte fanns nog av möjligheter att skapa iterationer finns i C# ytterligare en variant som heter foreach. Den kan alltid ersättas av en while- eller for-sats, men den är i särklass enklast och elegantast och har därför ett existensberättigande.
Kopiering av kurslitteratur förbjuden. © Studentlitteratur
59
2 Inledande detaljer
Satsen är mycket speciell genom att den är implementerad så att den i själva verket i tysthet anropar ett par metoder i den samling man traverserar. Metoderna är standardiserade i ett interface (en samling metodsignaturer), vilket är implementerat i vektorer och i alla de andra samlingsklasserna i klassbiblioteket. Det är alltså inte alltid säkert att foreach kan användas. Om inte ges ett kompileringsfel. Den logik som foreach använder kallas allmänt enumeratorer (i andra miljöer kallas samma sak ibland iteratorer). Först ett exempel: string[] names = {”Anna”, ”Britta”, ”Cecilia” }; foreach (string s in names) { Console.WriteLine(s); } Notera att man inte behöver formulera något villkor. Magi ser till att slingan avslutas när den traverserade samlingen är slut. Magin består av en enumerator. En enumerator är ett objekt som refererar till ett element i en samling, och som har metoder för att flytta referensen till nästa eller föregående element. Foreach-satsen hämtar en enumerator från samlingen genom att anropa en metod som heter GetEnumerator() och använder den genom att för varje varv anropa dess MoveNext() och Current. MoveNext() returnerar false när samlingen är traverserad till slut och foreach avslutas då. En lite tråkig effekt av foreach-satsens implementation är att den blir felbenägen om man förändrar en samling medan man traverserar. I vissa fall går det inte, men om koden alls kompilerar blir det lätt en bugg. Om elementen är enkla datatyper (structer) kan inte ändras, det ger ett kompileringsfel. Foreach kan alltså användas för traversering där man enbart läser elementen.
60
Kopiering av kurslitteratur förbjuden. © Studentlitteratur
2 Inledande detaljer
2.7 XML-kommentarer Inledningsvis i detta kapitel beskrevs två av tre sätt att skriva kommentarer. Den tredje typen av kommentarer är betydligt mera avancerad. Den består av kommentarer efter tre snedstreck. /// Class Point defines a point in a /// twodimensional plane class Point { ... } Kommentartexten ska i detta fall vara XML-formaterad, dvs försedd med taggar enligt XML (en beskrivning av XML ligger utanför denna boks ambition). Den ska också vara placerad omedelbart före en typdeklaration eller före en medlem i en datatyp, och därmed referera till den. Egentligen är det allt som språkstandarden föreskriver. Kompilatorn betraktar även dessa kommentarer som något att hoppa över, och innehållet har därmed ingenting med programlogiken att göra. Men för att ge vägledning och för att göra XML-kommentarer meningsfulla har ett antal rekommendationer formulerats i standarden. Avsikten med det hela är att ett verktyg (det kan vara kompilatorn) ska extrahera XML-kommentarerna och möjligen också generera ytterligare text – för att skapa en separat dokumentationsfil. Det extraherande verktyget rekommenderas också kontrollera att kommentarerna är korrekt XML-format. Här finns alltså stora möjligheter för CLI-tillämpningar att utöka med avancerade finesser för generering av dokumentation. Det är en bra idé att dokumentation skrivs nära koden, och att det sker på ett strukturerat sätt. De taggar som strukturerar texten kan vara vilka som helst, men återigen ger standarden några rekommendationer för att någorlunda strömlinjeforma den genererade dokumentationen. I exemplet nyss var texten omgiven av en tag .... Den är allmänt avsedd att omge fritt beskrivande text för en datatyp, i exemplet en klass. En liknande tag heter ... och är avsedd för fri beskrivning av en medlem. För dokumentation av metoder
Kopiering av kurslitteratur förbjuden. © Studentlitteratur
61
2 Inledande detaljer
finns också särskilda taggar för beskrivning av parametrar och returvärde. /// This method changes the points location /// to the given coordinates. /// xor is the new x-coordinate /// yor is the new y-coordinate /// void public void Move(int xor, int yor) { ... } Taggen exception kan också beskriva vilka undantag en metod kan generera. För beskrivning av egenskaper finns taggen value och för stilsättning av texten finns ett antal rekommenderade taggar: code, example och c är avsedda att markera kodexempel på lite olika sätt. Det går också att förse texten med hyperlänkar. Åtskilliga detaljer här lämnas åt den intresserade läsaren att botanisera bland i standarddokumentet. Varje CLI-implementation har också med stor sannolikhet en rejält utökad repertoar av taggar och funktionalitet för generering av XML-dokumentation. Det ska påpekas att trots att denna bok varmt rekommenderar flitigt XML-kommenterande så är kodexemplen i boken inte försedda med kommentarer. Här ingår ju koden i ett sammanhang av förklarande text och kommentarer i koden skulle bara skymma sikten. Ta inte det som ett föredöme, utan använd XML-kommentarer systematiskt i skarp kod!
62
Kopiering av kurslitteratur förbjuden. © Studentlitteratur
3 Klasser
3 Klasser
Begreppet klass i objektorienterad programutveckling – och i objektorienterad analys och design – är för många i början svårt att så att säga fylla med något mentalt innehåll, så att det verkligen får någon betydelse. Många begrepp inom datavetenskap är ju ord som man helt enkelt måste inse att man inte ska associera med vardagsspråkets betydelse, utan ge en helt ny betydelse i det speciella sammanhanget. En klass i programkod är en datatyp. Av en datatyp kan man i kod skapa variabler, antingen så att de skapas vid kompilering och ingår i den kompilerade koden (globala, statiska), eller så att de skapas vid exekvering (dynamiskt). Klassen är typen, objektet är variabeln. Man säger också att objektet är en instans av klassen. En klassdeklaration är dels klassens metoder (objektens beteende), dels receptet för hur objekt ska skapas. Programspråket har regler för hur metoderna ser och hittar det aktuella objektet. Ett objekt får därmed tillstånd och beteende. Objektets tillstånd är den samlade bilden av dess innehåll. Klass och objekt kan aldrig sammanblandas, det finns ingen gråzon mellan begreppen. Klassen är ju receptet och beskrivningar av vad man kan göra med ett objekt. Objektet är en portion av det receptet. Ingen skulle få för sig att äta en sida ur en kokbok. En klassdeklaration kompileras till kod som utgör ett antal metoder. Koden sparas i en eller flera filer som är ett assembly. När klassen laddas kompileras koden än en gång, nu till maskinkod för den aktuella plattformen. Maskinkoden läggs i processens minnesrymd. metoder exekverar genom att exekveringen hoppar fram och tillbaka i minnet.
Kopiering av kurslitteratur förbjuden. © Studentlitteratur
63
3 Klasser
Ett objekt består av sina medlemsvariabler. Den objektorienterade programmeringsmodellen består mest av synlighetsregler i källkoden. Endast klassens egna metoder bör kunna förändra objekts tillstånd. Ett antal objekt som kan representera samma tillstånd, och som har samma beteendemönster, har uppenbart en hel del gemensamt – de är av samma klass. Termen är ändå inte helt fel vald.
3.1 Inkapsling och datagömning I funktionsorienterad programmering består programmet av ett antal variabler och ett antal funktioner. Variabler som är globala och synliga i alla källkodsfiler kan förändras av vilken som helst av funktionerna. Det ger ganska snart ett oöverskådligt mönster av beroenden. När någon eller några av variablerna och någon eller några av funktionerna egentligen är en avgränsad del av programlogiken kan ändå beroenden finnas gentemot andra variabler eller funktioner som egentligen inte hör till den. Punkt ett i objektorientering är därför att paketera samman variabler och funktioner så att beroenden blir synliga och hanterbara. Att göra variabler och funktioner till någon slags enhet kallas inkapsling, att kunna styra synligheten så att enbart vissa funktioner ser vissa variabler kallas datagömning. class Trivial // klassdeklarationen { private int val; public int GetVal() { return val; } } // Klientkod använder klassen Trivial t = new Trivial(); // ett objekt skapas 64
Kopiering av kurslitteratur förbjuden. © Studentlitteratur
3 Klasser
int x = t.GetVal(); // OK int y = t.val; // Fel! ingen access till private Redan med dessa enkla språkmekanismer får man flera rejäla fördelar gentemot traditionell strukturerad (funktionsorienterad) programmering. Genom att göra vissa medlemmar public och andra private försäkrar man sig om att det som är private garanterat inte används direkt i annan kod än klassens egna metoder (medlemsfunktioner). Därmed kan det förändras utan att den kod där objekt skapas och används (klientkod) påverkas. När klassers logik och publika delar väl är fastställt kan man alltså i lugn och ro utveckla klassen utan att behöva samordna sig stup i ett med de andra utvecklare i projektet som skriver klientkod. Effektiviteten ökar. Ordning och reda i källkod upprätthålls. I en C#-klass förses varje medlem med en modifierare för synlighet. Den kan vara private eller public, den kan också vara protected, internal eller internal protected. De senare ska vi återkomma till längre fram. Private betyder synlig endast i klassens egna metoder. Public betyder synlig i egna metoder och synlig via objekt av klassen, vilket vi alltså kallar klientkod. Om ingen modifierare skrivs blir synligheten private, och det är stilmässigt helt OK att göra så. I alla objektorienterade språk brukar man som grundregel ha att medlemsvariabler görs private och metoder görs public. Från grundregeln gör man sedan ofta avsteg. I C# finns extra många sätt att skapa medlemmar, så att public variabler faktiskt aldrig behövs. Alltså: gör aldrig medlemsvariabler i en klass public! (Det kan däremot finnas skäl att göra structmedlemmar public.) C# har en rekordlång lista över olika slags möjliga medlemmar i klasser. Resten av kapitlet ska beskriva de flesta. Ett par beskrivs i eget kapitel därefter. Observera dock att samtliga i listan här nedan egentligen är metoder eller variabler eller datatyper. Något riktigt nytt under solen finns inte – även i C# kan en klass innehålla metoder, variabler och datatyper. Inget annat.
Kopiering av kurslitteratur förbjuden. © Studentlitteratur
65
3 Klasser
Klassmedlemmar Fält (field) – medlemsvariabler, kan vara struct, klass eller interface Konstruktor (constructor) – funktion som initierar objekt Metod (method) – medlemsfunktion Egenskap (property) – metod som ser ut som fält Operator (operator) – metod som anropas via operator Typomvandlare (conversions) – metod som anropas vid typomvandling Destruktor (destructor) – metod som anropas vid skräpsamling Delegerare (delegate) – datatyp för referens till metoder i andra objekt Notifierare (event) – egenskap som döljer delegerarobjekt
3.2 Fält Medlemmar som är variabler är av datatypen struct, klass eller interface. Något annat finns ju inte i CTS. De som är struct skapas då i sin helhet när objekt av klassen skapas, vilket kallas inline. De som är klassreferenser utgör endast referensvariabeln. De som är interface är också referensvariabler, precis som klassreferenser. Observera att vi nu talar om medlemsvariabler – nästlade struct- klass- och interfacedeklarationer får också förekomma i en klass men det är inte det som är ämnet just nu. Initiering av medlemmar sker automatiskt till det värde som ges av att alla bitar i variabeln sätts till noll. Det vill säga klassreferenser blir null, enkla datatyper blir 0, 0.0 respektive false. Egna structer får alla sina medlemmar satta på samma sätt, och de i sin tur får alla sina satta på samma sätt... I botten består ju alla objekt av de enkla datatyperna. Det går också bra att initiera fält direkt och explicit i klassdeklarationen. Enkla datatyper kan tilldelas en literal eller en annan variabel om den är static eller const (orden förklaras strax). Klassreferenser kan tilldelas ett objekt med anrop till new på vanligt sätt.
66
Kopiering av kurslitteratur förbjuden. © Studentlitteratur
3 Klasser
class Trivial // klassdeklarationen { int val = 100; string str = ”Etthundra”; double[] vec = new double[100]; MyClass obj1 = new MyClass(); YourClass obj2 = null; // endast för tydlighet // blir ändå null public int GetVal() { return val; } } Ofta kan man vilja göra fält skrivskyddade. De ges ett värde när de skapas i objekten, och därefter ska de inte kunna ändras. Det finns två sätt att göra det: med const eller med readonly. En const-deklarerad medlem måste initieras med ett värde som kan beräknas vid kompilering, normalt en literal. En readonly är lite mera avancerad. Den behöver inte initieras utan kan få ett värde i den konstruktor som initierar objektet. Men det är tillåtet att initiera den precis som en const. Om objektet skapas med en konstruktor som initierar med annat värde gäller det istället. I vilket fall kan den inte tilldelas något annat värde när den väl skapats och initierats en gång. Det faktum att en const måste initieras med en literal eller en annan const innebär att endast de datatyper som har ett sätt att skriva literaler kan komma ifråga. Det är med andra ord de enkla datatyperna, som ju är structer, samt ett undantag: string. Den enda referenstypen som kan const-deklareras är alltså string. En literalsträng är ju en string. Däremot kan klassreferenser utan problem göras readonly. Objektet skapas med new, antingen i klassdeklarationen eller i konstruktor. Observera att det då är referensen som är skrivskyddad. Den kan inte sättas att referera till något annat objekt. Själva objektet däremot är ju inte på något sätt skrivskyddat för det. Konstanta klassobjekt finns helt enkelt inte i detta språk. Kopiering av kurslitteratur förbjuden. © Studentlitteratur
67
3 Klasser
I vad mån objektet kan förändras eller inte blir helt en fråga om klassdesign. Om klassen inte har några metoder för att ändra objektets tillstånd är objekten ju i någon mening skrivskyddade, men då kallas klassen immutable. Ett exempel på en immutable klass är just string-klassen. Alla metoder i den klassen som är sådana att de verkar förändra strängen returnerar i själva verket alltid en ny sträng. En string är alltså immutable och en string-referens är den enda referens som kan deklareras const. Nu till ett exempel: class Trivial { const int val = 100; const string str = ”Ständigt innehåll”; readonly SomeClass obj = new SomeClass(); public int GetVal() { return val; // alltid 100 } } Klassen innehåller tre fält som samtliga blir skrivskyddade. Heltalet och strängen får värden som kan beräknas vid kompilering och kan därför deklareras const. Referensen till objektet av den påhittade SomeClass kan däremot inte skrivas const. Varje objekt av klassen Trivial kommer att innehålla en referens till ett eget SomeClassobjekt, så referensen har uppenbart olika värde i olika Trivialobjekt och om den ska skrivskyddas blir det med readonly. Om man funderar lite över de const-deklarerade medlemmarna så undrar man kanske om det inte är lite dumt att i varje objekt ha en medlem som i alla objekt har samma värde – varför inte ha en enda gemensam och spara minne? Det är i själva verket så det automatiskt blir. En const blir också implicit static, och static betyder just att den inte ingår i objekt utan finns i ett gemensamt exemplar. Objekt av Trivial-klassen blir följaktligen utomordentligt små. De består bara av referensen till SomeClass-objektet!
68
Kopiering av kurslitteratur förbjuden. © Studentlitteratur
3 Klasser
Både static- och const-deklarerade medlemmar refereras enbart med klassnamnet, inte med objektnamn: Trivial obj = new Trivial(); ... obj.val; // Fel, val är const och därmed static ... Trivial.val; // Rätt! En medlem kan naturligtvis göras static utan att vara const. Man skriver static i deklarationen. Det är fortfarande tillåtet att initiera den som vanligt, och dess värde kan användas för att initiera andra icke-static medlemmar. En static finns alltså i ett gemensamt exemplar. Man undrar då när detta gemensamma exemplar skapas. I reglerna är det inte definierat på annat sätt än att det finns när det första gången används! Vi kan se det som att det skapas när programmet startar och försvinner när programmet avslutas.
3.3 Konstruktorer Initiering av fält kunde alltså ske genom tilldelning direkt i klassdeklarationen. Man kunde också avstå från explicit initiering och fick då ett värde motsvarande noll. Men oftast vill man ju i klientkod kunna skapa objekt med ett tillstånd redan från start. För structer gör man ju det genom att tilldela i samma sats där man skapar objektet: int x = 5; För string-objekt gör man också på samma sätt, trots att string är en klass och inte en struct. Det beror på att en strängliteral är en string, som vi tidigare sett. Referensen tilldelas alltså ett objekt, vilket ju är helt i sin ordning. string str = ”Hej”; För andra klassobjekt har vi i tidigare exempel sett att objekt skapas på följande sätt: Trivial obj = new Trivial();
Kopiering av kurslitteratur förbjuden. © Studentlitteratur
69
3 Klasser
Det som efter new ser ut som ett metodanrop är just precis det, närmare bestämt ett anrop till defaultkonstruktorn. Med defaultkonstruktor menas den konstruktor som inte tar några parametrar. Man kan nämligen göra så många konstruktorer man anser behövs, genom att ge dem olika parameterlista. Det bör vara så att för varje tänkbart sätt man i klientkod vill kunna skapa och initiera objekt bör det finnas en konstruktor. Klasser i verkliga livet har ofta åtskilliga konstruktorer, varav flera kanske logiskt överlappar varann, men det gör inget. Huvudsaken är att de konstruktorer som användaren av vår klass gissar finns – de ska också finnas. Konstruktorn anropas med klassens namn. Den är med andra ord en metod med samma namn som klassen. class Rational { int numerator = 0; int denominator = 1; public Rational(int n, int d) { numerator = n; denominator = d; } ... } Här såg vi en klass Rational, för rationella tal (bråktal) med en konstruktor som gör det möjligt att skapa Rational-objekt med två initialvärden – täljare och nämnare. Klientkod skriver då: Rational r = new Rational(2, 3); Observera syntaxmässigt att konstruktorer skrivs utan deklarerad returtyp. Synligheten är valfri, det finns typfall då man vill göra konstruktorer med annan synlighet än public, även om public naturligtvis är vanligast. Om en konstruktor inte är public kan den i princip inte anropas, och objekt inte skapas! Men när vi sett alla synlighetsvarianter (och kombinationer) ska vi se att man ibland faktiskt vill ha det så. Om vi gör en konstruktor som i fallet Rational uppstår plötsligt en egenhet. Det går nu inte att skapa objekt genom att anropa default70
Kopiering av kurslitteratur förbjuden. © Studentlitteratur
3 Klasser
konstruktorn! Defaultkonstruktorn existerar endast i klasser där ingen konstruktor alls definierats. Den är så att säga ett minimum som kompilatorn förser klasser helt utan konstruktor med. Rational r = new Rational(); // Fel! ingen default// konstruktor Om vi vill göra det möjligt att skapa objekt utan explicit initiering får vi alltså lov att skriva den konstruktorn. Ofta blir den tom eftersom initieringen i klassdeklarationen sannolikt gör det önskvärda i det fallet. class Rational { int numerator = 0; int denominator = 1; public Rational(int n, int d) { numerator = n; denominator = d; } public Rational() // defaultkonstruktor { } ... } I en klass Rational skulle man också vilja ha en konstruktor för ett heltal (bara täljaren, nämnaren sätts till 1), och kanske en för flyttal (skapar ett bråk med närmsta tänkbara värde). Vi ska i flera kapitel framöver bygga vidare på Rational-klassen så att den med tiden blir fullt realistisk.
3.3.1 Flera konstruktorer Som exempel på en klass med en rejäl uppsättning konstruktorer kan vi se på standardbibliotekets klass StreamReader. Objekt av den klassen kan användas för att läsa från en fil. När StreamReader-objektet skapas öppnas filen och därefter kan man anropa
Kopiering av kurslitteratur förbjuden. © Studentlitteratur
71
3 Klasser
metoder för att läsa från filen. Med filer är det ju så, att de kan öppnas för läsning på flera sätt – med eller utan konverteringar, med namnet som en sträng eller från ett Stream-objekt, med olika storlek på buffert, etc. – och för alla varianter bör då finnas en passande konstruktor. public StreamReader(Stream stream); public StreamReader(Stream stream, bool detectEncodingFromByteOrderMarks); public StreamReader(Stream stream, Encoding encoding); public StreamReader(Stream stream, Encoding encoding, bool detectEncodingFromByteOrderMarks); public StreamReader(Stream stream, Encoding encoding, bool detectEncodingFromByteOrderMarks, int bufferSize); public StreamReader(string path); public StreamReader(string path, bool detectEncodingFromByteOrderMarks); public StreamReader(string path, Encoding encoding); public StreamReader(string path, Encoding encoding, bool detectEncodingFromByteOrderMarks); public StreamReader(string path, Encoding encoding, bool detectEncodingFromByteOrderMarks, int bufferSize); Vi ska inte fördjupa oss i alla dessa konstruktorer, se dem bara som ett exempel på en realistisk uppsättning konstruktorer. När man skriver konstruktorer kan det ibland vara praktiskt att kunna anropa en konstruktor från en annan. Man bör ju inte skriva samma kod för samma ändamål på flera ställen. Det går då att åstadkomma med en lite udda syntax. class Rational { int numerator, denominator; public Rational(): this(0, 1) {} public Rational(int n): this(n, 1) {} public Rational(double val): this((int)(val * 1000), 1000) {} 72
Kopiering av kurslitteratur förbjuden. © Studentlitteratur
3 Klasser
public Rational(int n, int d) { if (d == 0) d = 1; numerator = n; denominator = d; } } En annan konstruktor kan alltså anropas som om den hette this, och det måste i så fall ske i den så kallade initieringslistan. Initieringslistan är det som kommer efter kolon och före första klammerparentesen. Den finns bara i konstruktorer och vi kommer att se viktigare ändamål för den när vi studerar arv senare. Men den kan alltså också användas för detta ändamål. Här såg vi också att de konstruktorer i Rational som vi tidigare saknade kunde implementeras enkelt genom att anropa den konstruktor som tar två heltal. I den har också en kontroll av att nämnaren är skild från noll lagts in. Det tillståndet är ju ogiltigt och får inte uppstå. Här sätts den till ett, men det rätta vore att generera ett undantag, vilket vi ska göra i senare versioner. Konstruktorn för flyttal implementeras genom att förlänga täljare och nämnare och anropa den befintliga konstruktorn för två heltal. Genom att förlänga med 1000 skapas bråktalet utan avrundning för flyttal med högst tre decimaler. Tyvärr kommer ytterligare decimaler i flyttalet att trunkeras vid typomvandlingen av produkten till heltal, och dessutom kommer intervallet av möjliga värden i täljare och nämnare att reduceras med en faktor 1000. Risken för overflow ökar. Om vi ökar faktorn blir precisionen bättre men intervallet ännu mindre. Notera också att parenteserna runt multiplikationen är nödvändiga, utan dem skulle typomvandlingen ske före multiplikationen, och det betyder trunkering av alla decimaler! (int)(val * 1000) // multiplikationen ger ett flyttal (int)val * 1000 // multiplikationen ger ett heltal
Kopiering av kurslitteratur förbjuden. © Studentlitteratur
73
3 Klasser
Här vore ett bra tillfälle att använda checked. Genom att lägga till checked kommer kontroll av overflow vid typomvandlingen att ske, och ett undantag att genereras om overflow uppstår. public Rational(double val): this(checked((int)(val * 1000)), 1000) {} Apropå anrop mellan konstruktorer så kan man kanske undra över huruvida det går att anropa andra metoder i klassen från en konstruktor. Konstruktorn exekverar ju när objektet skapas, är det då inte för tidigt att börja göra saker med objektet? Svaret är att det går utmärkt. När konstruktor exekverar har objektet skapats, det vill säga dess minne är allokerat. Det konstruktorn gör är att initiera ojektet. De konstruktorer vi nu sett har endast haft ändamålet att initiera det minne som utgör objektet, eller att skapa ägda objekt genom att sätta medlemsreferenser att referera till dem. Ägda objekt betraktar vi som medlemmar oavsett om de är structer eller klassobjekt.
3.3.2 UML-notation I Unified Modeling Language (UML), vilket är en standard för terminologi och notation i bland annat klassdiagram, kallar man sådant ägande ”komposition” (composition) och ritar det som ett streck med en romb mellan två rektanglar som symboliserar klasser.
A
B
Figur 3.1 Komposition. Klassen A består av objekt av klass B, odefinierat antal.
Komposition betyder att det ägda objektet verkligen är en del av ägaren, det skapas alltid med ägaren och dör med ägaren.
74
Kopiering av kurslitteratur förbjuden. © Studentlitteratur
3 Klasser
Men i många fall blir det så att kanske bara vissa konstruktorer – eller andra metoder – skapar det ägda objektet. Kanske har man i klassen en referens som ibland behåller värdet null. Då är ägarförhållandet lite mindre solklart. För sådana och andra fall då ägandet inte är helt hundraprocentigt av ett eller annat skäl ritar man istället linjen med en ofylld romb och kallar relationen mellan objekten för ”aggregering” (aggregation).
A
B
Figur 3.2 Aggregering. Klass A skapar objekt av klass B, men kanske inte alltid, eller de kanske refereras till på annat sätt också. Ägandet är inte totalt.
När vi ändå är inne på UML-notation för klassdiagram kan vi passa på att visa en tredje sorts linje mellan klasser, nämligen en pil eller en ren linje, utan detaljer alls. Den varianten kallas ”association” (association) och betyder att en klass innehåller en referens till objekt av en annan klass, men utan att skapa eller radera objekten av den andra klassen. Det är också ett vanligt förhållande, och även då kanske referensen sätts i någon konstruktor.
A
B
Figur 3.3 Association. Klass och och/eller klass B har referenser till objekt av den andra klassen. Antal och riktning är odefinierat. Inget ägarförhållande.
I förra avsnittet såg vi att fält i en klass kan deklareras static, och att de då inte ingår i objekt av klassen, utan finns i ett gemensamt
Kopiering av kurslitteratur förbjuden. © Studentlitteratur
75
3 Klasser
exemplar som skapas vid programstart. Detsamma gällde constdeklarerade fält. För static fält finns nu möjlighet att göra en egen konstruktor. Den ska deklareras static, inte ha någon modifierare för synlighet och inga parametrar. Samtidigt måste man komma ihåg att även static-medlemmar kan initieras i klassdeklarationen, så behovet av en static konstruktor är inte stort. En sista kategori konstruktorer är sådana där någon systemresurs allokeras i konstruktor. Det kan vara anrop till någon API-funktion i operativsystemet, för att skapa fönster, öppna filer, sockets etcetera. Då uppstår ett helt nytt och tyvärr ganska stort problemkomplex. För objekt som enbart består av minne och referenser till ägda objekt som i sin tur bara består av minne behöver vi ju inte bekymra oss om konstruktorns motpol: destruktorn. VES har ju automatisk skräpsamling. Men om konstruktor allokerar resurser som måste avallokeras när objektet dör, så måste vi också skriva en destruktor och se till att resurserna avallokeras där. Nu kör det dock ihop sig lite. VES lovar ingenting om när objekt dör och destruktorn exekveras. Så hur går det om vi exempelvis öppnar nya sockets i en slinga som kör hundra varv i sekunden? Lösningar finns, men de kommer senare...
3.4 Metoder Konstruktorer i förra avsnittet är en slags metoder – funktioner i klasser. Vi såg då genast en egenhet hos metoder i detta språk: man kan göra flera med samma namn. När man gör en ny metod med samma namn som en befintlig, men med annorlunda parameterlista kallas det överlagring. Det räcker alltså inte att enbart returvärdet skiljer, antal och/eller datatyp på parametrarna ska skilja. Vanligen gör man inte heller metoder där enbart datatyp skiljer och där datatypen är närbesläktad. Det är ju för att få olika implementation man överlagrar, och då är det naturligt att parameterlistan är avsevärt olika.
76
Kopiering av kurslitteratur förbjuden. © Studentlitteratur
3 Klasser
Det finns ett par modifierare för parametrar, de heter ref och out och gör det möjligt att åstadkomma referensanrop,vilket vi strax ska reda ut ordentligt. Vad gäller överlagring enbart med avseende på ref eller out finns en liten fallgrop. CLS innehåller ju regler som gör att komponenter skrivna i olika språk kan exekvera tillsammans, och en av de reglerna är att overlagring inte får ske enbart med avseende på parametrars modifierare. Men observera – det är en CLS-regel, inte en C#-regel, så kompilatorn protesterar inte. void F(int x) {...} int F(int x) {...} // Fel! parameterlistan lika void F(string s) {...} // OK void F(ref int x) {...} // OK, men ej CLS! void F(int x, int y) {...} // OK Ett par saker innan vi går vidare: Vi har sett att metoder deklareras med returtyp, namn och parameterlista, samt metodblock (kropp). I C/C++ ser det också ut så, men med ett antal tillåtna varianter – glöm dem! I C# är returtyp obligatoriskt, och några separata deklarationer finns inte. Metoder definieras i sin helhet i klassdeklarationen. Headerfiler, fördeklarationer och annat som ofta krävs i C++ är också eliminerat. Nu åter till modifierarna vi hastigt såg nyss – ref och out. Saken är den, att på assemblernivå finns inget annat än så kallat värdeanrop. Med det menas att alla lokala variabler, även parametrar, skapas på stack (ett minnesutrymme som ständigt återanvänds för lokala variabler). Därmed är alltså de variabler som utgör parametrar alltid kopior. Skillnaden mellan parametrar och andra lokala variabler är endast att parametrar initieras i anropet. I många språk finns emellertid syntax som döljer detta förhållande och gör det möjligt att i metoden ha en parameter som blir en synonym till en variabel hos det anropande programmet. Så blir det i C# om parametern är en klassreferens. Då kommer metoden att ha en referens till ett objekt som metoden därmed kan göra bestående ändringar i. Själva referensen överförs med värde, men logiken blir referensanrop. Structer däremot hanteras ju inte med referens, variabelns namn representerar dess tillstånd och structen i sin helhet kopieras på Kopiering av kurslitteratur förbjuden. © Studentlitteratur
77
3 Klasser
stacken. Vad göra om man vill ha referensanrop med en struct, exempelvis en int? static void F(int x, int[] vec) { x = 100; vec[0] = 100; } static void Main() { int i = 0; int[] iv = new int[3]; // alla är 0 F(i, iv); Console.WriteLine(”{0} och {1}”, i, iv[0]); // skriver ”0 och 100” } Kodexemplet visar att en aktuell parameter som är av datatypen int inte påverkas av ett metodanrop, men den aktuella parameter som är av datatypen int[], och som ju är en Array-referens kommer att ge metoden tillgång till objektet och metoden kan sätta om ett element i vektorn. Om vi skulle vilja att även int-parametern skulle kunna ändras av metoden kan vi skriva ref i parameterlistan och i anropet. static void F(ref int x, int[] vec) { x = 100; vec[0] = 100; } static void Main() { int i = 0; int[] iv = new int[3]; // alla är 0 F(ref i, iv); Console.WriteLine(”{0} och {1}”, i, iv[0]); // skriver ”100 och 100” }
78
Kopiering av kurslitteratur förbjuden. © Studentlitteratur
3 Klasser
Ordet ref gör alltså om anropet så att en referens till en struct överförst som parameter, men vi behöver inte hantera den med någon särskild syntax, det sköts i tysthet. Ordet ref kan faktiskt också användas på en referensvariabel. Det är inte ofta man behöver den varianten, men den har sin egen logik. Om den referens som utgör aktuell parameter ska användas för att tilldelas ett annat objekt måste den överföras som ref. Observera att för att modifiera ett klassobjekt som utgör parameter räckte det med att få en ny referens till samma objekt, men om det är själva referensen som ska modifieras i anropet så måste vi skicka med en referens till en referens. För den som jonglerat med pekare i C/C++ är ovanstående inget nytt, men för andra dödliga brukar dessa förhållanden kräva lite eftertanke... Det finns i språket också en variant av ref. För de fall då referensen finns som parameter enbart för att få ett värde i anropet, alltså där invärdet är irrelevant, kan man skriva out i stället för ref. Enda skillnaden är då att kompilatorn inte kontrollerar om variabeln är initierad hos anroparen, och istället kontrollerar att parameterns värde i metoden inte används, bara tilldelas. Att använda referenser på det sättet är lösningen på problemet med fler än ett returvärde. En metod kan ju bara returnera ett värde, men via referenser (ref eller out) kan valfritt antal värden beräknas och skickas tillbaka till anroparen. static void SplitName(string FullName, out string FirstName, out string LastName) { ... } static void Main() { string first, last; SplitName(”Kalle Anka”, out first, out last); }
Kopiering av kurslitteratur förbjuden. © Studentlitteratur
79
3 Klasser
3.4.1 Statiska metoder och instansmetoder I exemplen ovan noterade den uppmärksamme läsaren att metoderna deklarerats static. Med samma logik som ett fält kunde göras static kan en metod göras static. Motsatsen brukar kallas instansmetod, och skillnaden är att en instansmetod måste anropas med ett aktuellt objekt, medan static-metoden anropas med klassnamn istället för objekt. class X { int x; static int s; public int XInstance() { int y = x * s; // OK instansmetod ser både // instansmedlemmar och statiska medlemmar return y; } public static int XStatic() { int y = x; // Fel! finns ingen x - inget objekt return s; } static void Main() { X.XStatic(); // OK X obj = new X(); obj.XStatic; // Fel! ska anropas via klassnamn obj.XInstance(); // OK } } En static-metod har inget aktuellt objekt att manipulera, och kan alltså inte accessa andra medlemmar (fält eller metoder) än de som också är static eller const. Ofta kan man se static-metoder som något ganska likt globala funktioner i funktionsorienterade
80
Kopiering av kurslitteratur förbjuden. © Studentlitteratur
3 Klasser
språk. Här är de ändå åtminstone förpackade i klasser med någorlunda logisk hemmahörighet, men skillnaden är inte stor. I klassbiblioteket finns klasser med enbart statiska metoder. Klassen System.Math har 61 statiska metoder och två const-deklarerade fält! Där har man helt enkelt samlat alla matematikmetoder såsom Sin(), Cos(), Pow() och så vidare. Eftersom det är meningslöst att skapa objekt av Math har man också gjort defaultkonstruktorn private. Försök att skapa ett Math-objekt ger bara ett kompileringsfel. Instansmetoder har till skillnad från statiska metoder ett aktuellt objekt att manipulera. Om man lite filosofiskt studerar koden i en instansmetod kan man ställa sig den högst motiverade frågan: hur identifierar instansmetoden det aktuella (anropande) objektet? Där använder vi ju klassens fält som namn på de delar av objektet vi vill accessa, men inget objektnamn och ingen punkt för att prefixa fält. Det kanske kan verka självklart att det är det anropande objektets medlemmar vi accessar, men det syns ju faktiskt inte i kod. Här finns uppenbart någon implicit mekanism i aktion, och det är parametern this. Varje instansmetod förses av kompilatorn med en extra parameter vars datatyp är referens till objekt av denna klass. Om vi gör en instansmetod void Method(int x) { ... } så kommer den i själva verket att vara void Method(MyClass this, int x) { ... } Den extra osynliga parametern heter alltid this och är en referens till det anropande objektet. Det är som om anropet obj.Method(i); i stället vore MyClass.Method(obj, i); Detta kan verka som onödigt orerande om detaljer enbart intressanta för kompilatorutvecklare, men det visar sig att man inte så sällan vill explicit göra något med det anropande objektet. Då går det bra. Vi kan exempelvis inifrån en instansmetod behöva anropa en Kopiering av kurslitteratur förbjuden. © Studentlitteratur
81
3 Klasser
statisk metod eller en metod i en annan klass, med det aktuella objektet som parameter. SomeStaticMethod(this); I några av de konstruktorer vi såg i tidiga exempel fanns som en ren stilsak en vanlig företeelse: parametrar hade samma namn som de fält de skulle initiera. Lokala variabler döljer ju variabler på nivå utanför, så då måste instansvariabeln skiljas från parametern med ”this.”. Här är repris på den allra första konstruktor vi såg: public Minimal(string message) { this.message = message; } Medlemmen message i denna konstruktors klass initieras med en parameter med samma namn. Medlemmen specificeras då med this så att kompilatorn förstår vem som är vem. Denna stil är i själva verket helt dominerande i kodexempel och dokumentation. Det är en ren smaksak om man gillar den, men förslagsvis ansluter vi till gällande stil och tar detta till oss.
3.4.2 Klassen Rational I den Rational-klass som sakta växer fram i avsnitten kan man nu tänka sig att lägga till ett stort antal metoder, men så gör man inte i detta språk. I en motsvarande klass i Java skulle minst ett tiotal metoder för addition, subtraktion etcetera läggas till, men i C# finns den betydligt elegantare lösningen att skriva operatorer. För addition av rationella tal kan man visst tänka sig en metod som heter Add eller något liknande – men + är ju betydligt elegantare i klientkod, så vi återkommer i det ärendet i avsnittet om operatorer lite senare. Någon eller några metoder behövs dock. De konstruktorer som finns i klassen så här långt är sådana att de tar klientens aktuella parametrar helt okritiskt och gör ett bråk av dem. Rationella tal bör inte se ut hur som helst, de bör vara normaliserade. Med det menas att de dels inte bör ha negativ nämnare (täljaren kan vara negativ), 82
Kopiering av kurslitteratur förbjuden. © Studentlitteratur
3 Klasser
värdet noll ska ha nämnaren ett, och att de är reducerade till minsta möjliga tal i täljare och nämnare. En metod för det bör skapas, och anropas från alla konstruktorer. private void Normalize() { // hitta största gemensamma divisor int m = Math.Abs(numerator); int n = Math.Abs(denominator); int r = m % n; while (r != 0) { m = n; n = r; r = m % n; } // reducera numerator /= n; denominator /= n; // noll ska ha nämnaren 1 if (numerator == 0) denominator = 1; // nämnaren ska alltid vara positiv if (denominator < 0) { denominator = -denominator; numerator = -numerator; } } Algoritmen för att hitta största gemensamma divisorn är lite extra intressant eftersom den är en av de äldsta algoritmerna i sitt slag. Den formulerades av Euclides ungefär 300 år före Kristus! Den intresserade kan hitta teorem och bevis i nästan vilken bok om algoritmteori som helst. Konstruktorerna kan nu med lätt förändrat utseende enligt senaste avsnitten se ut så här: public Rational(): this(0, 1) {} public Rational(int numerator): this(numerator, 1)
Kopiering av kurslitteratur förbjuden. © Studentlitteratur
83
3 Klasser
{} public Rational(double val): this(checked((int)(val * 1000)), 1000) {} public Rational(int numerator, int denominator) { if (denominator == 0) denominator = 1; this.numerator = numerator; this.denominator = denominator; Normalize(); } Slutligen en lite extra fyndig finess i språket. En vektor som parameter är ju en referens. Vektorns längd kan därmed vara olika från gång till gång – den kan undersökas med metoden GetLength() exempelvis. Samtidigt är det så att en referens av en datatyp som är basklass (arv kommer i ett senare kapitel) kan referera till ett objekt av en härledd datatyp. Alla klasser i CTS är härledda från klassen Object, direkt eller indirekt i flera generationer. En Object-referens kan alltså referera till precis vad som helst! En vektor av Object-referenser blir då en mycket generell parameter, och en metod med en Object-vektor som parameter kan anropas med vad som helst, bara det förpackas i en sådan vektor. Den mekansimen – att förpacka en rad parametrar i en vektor – fås med helautomatik med ordet params. static void SuperFunc(params Object[] vec) { foreach ( Object obj in vec ) ... } static void Main() { SuperFunc(”Hej”, 100, 1.2, ’X’, new MyClass()); } Anropet i exemplet ger en vektor av fem Object-referenser, som egentligen refererar till en string, en int, en double, en char och en MyClass. Om vektorn är en Object-vektor blir det å andra sidan svårt att göra något vettigt av parametrarna. Det finns möjligheter att under84
Kopiering av kurslitteratur förbjuden. © Studentlitteratur
3 Klasser
söka klasstillhörighet vid exekvering, men realistiska tillämpningar är i detta fall svårare att hitta än skolboksexempel. Men vektorn måste inte vara en Object-vektor. Ordet params har samma effekt på vilken vektortyp som helst. En params int[] exempelvis, kan vara användbar för anrop med valfritt antal intparametrar.
3.5 Egenskaper Inte så sällan vill man i klassdesign trots allt göra en eller annan medlemsvariabel public, för att allt blir mera praktiskt så. Samtidigt bör man då alltid överväga att göra en struct i stället. En struct är typiskt ett antal public fält, och kanske några operatorer. Men om någon eller några fält av ett eller annat skäl gör sig bäst public, kan man i en klass bäst göra dem som egenskaper (properties). Poängen med egenskaper är att man faktiskt döljer fältet, samtidigt som man i klientkod låter det se ut som om det vore public. En egenskap är egentligen en metod, men den anropas utan parametrar – och utan parenteser – så att den ser ut som ett public fält. I andra språk är det brukligt att göra ”Get- och Setmetoder” för samma ändamål. Det anses korrekt eftersom det döljer den interna implementationsdetalj som fältet utgör, och ger ett publikt gränssnitt oberoende av implementationen. Det viktiga är att man i vidareutveckling av klassen kan byta fältets datatyp, eller kanske dela upp värdet i flera fält eller något sådant. Egenskaper ger den effekten. Därmed får man det bästa från två synpunkter, en praktisk lösning, och en som samtidigt tilltalar även den objektorienterade fundamentalisten. Syntaxen visas av följande exempel: public string MyProp { get { return myField;
Kopiering av kurslitteratur förbjuden. © Studentlitteratur
85
3 Klasser
} set { myField = value; } } Denna egenskap heter MyProp och kan både läsas och tilldelas. Den används i klientkod som om den var ett fält. obj.MyProp = ”Ny sträng”; // obj är ett objekt av den // klass vari MyProp ingår Console.WriteLine(obj.MyProp); Notera först att det hela ser ut som ett mellanting mellan metod och fält. Innehållet är delat i en get- och en setgren. Det är valfritt huruvida man gör bara get eller både get och set. Getgrenen anropas då värdet läses – som i WriteLine-satsen – och setgrenen anropas då värdet tilldelas. Att göra bara getgren innebär alltså att egenskapen görs skrivskyddad. Det är också tillåtet men ovanligt att göra bara setgren. En sådan egenskap kan följaktligen endast tilldelas men inte läsas från klientkod. I setgrenen har det värde som utgör r-value (det tilldelande värdet) det reserverade ordet value som namn. Det är som om det fanns en osynlig parameter av samma datatyp som egenskapen, och med namnet value. Den eller de fält som döljs av egenskapen behöver inte vara av samma datatyp som egenskapen. Vi har ju som vi tidigare sett en viss implicit typomvandling i språket, och en mera omfattande möjlighet att göra explicit typomvandling. Det är kort sagt fritt fram att göra vad man vill i en egenskapsdefinition, precis som i en vanlig metod. Att använda en egenskap för att dölja ett fält är i själva verket ett lite trivialt fall, liksom klasser med många Get- och Setmetoder också brukar anses lite triviala. Med trivial menar man då att klassens ändamål inte är riktigt genomtänkt. En klass är en paketering av tillstånd och beteende, och den bör motsvara någon företeelse som 86
Kopiering av kurslitteratur förbjuden. © Studentlitteratur
3 Klasser
programmet ska hantera, och som abstraheras och därmed förenklas i klientkod. Vi bör alltså ställa lite högre krav på våra klasser än att låta dem bli triviala. De bör verkligen på ett smart sätt dölja och förenkla – de ska vara svarta lådor att bygga applikationer av. Enkla och stryktåliga utanpå. Smarta, effektiva och kanske komplicerade inuti. Vår Rational-klass har inte många kandidater till egenskaper. Men där finns ju de två privata heltal som utgör täljare och nämnare, och det skulle möjligen vara intressant för klienter att kunna läsa värdena på dem – men absolut inte sätta nya värden i dem. Vi garanterar ju att Rational-objekt alltid är normaliserade, och det kan vi inte göra om klientkod kan sabotera värdena. Så egenskaper med enbart setgren är ändå relevant. Vi gör dem. public int Numerator { get { return this.numerator; } } public int Denominator { get { return this.denominator; } }
3.5.1 Egenskap som förenklad konstruktor Egenskaper används i klassbiblioteket i mycket stor omfattning, och de används ofta för ändamål som kan verka överraskande. Man kan i vissa fall rent av ifrågasätta om de inte används lite för mycket. En vanligt tillämpning för egenskaper är som förenklingar av konstruktoranrop. I en klass med konstruktorer som kräver många parametrar, men där ett överskådligt antal typiska konfigurationer ofta används är detta en rekommendabel design. Man kan exempelvis tänka sig en klass för färger, varav objekt ofta bör vara från någon standarduppsättning kulörer (ex HTML-färger). Klassen kan då förses med en uppsättning statiska egenskaper med Kopiering av kurslitteratur förbjuden. © Studentlitteratur
87
3 Klasser
enbart getgren och namn som beskriver färgen. Getgrenen skapar då och returnerar ett objekt av klassen. class Color { int red, green, blue; public Color(int r, int g, int b) { red = r; green = g; blue = b; } public static Color Red { get { return new Color(255, 0, 0); } } public static Color Green { get { return new Color(0, 255, 0); } } public static Color Blue { get { return new Color(0, 0, 255); } } public static Color White { get { return new Color(255, 255, 255); } } public static Color Black { get { return new Color(0, 0, 0); } } public static Color Fancy { get { return new Color(23, 200, 133); } } ... }
88
Kopiering av kurslitteratur förbjuden. © Studentlitteratur
3 Klasser
Ett exempel ur klassbiblioteket är structen DateTime som har en statisk egenskap som heter Now och som returnerar aktuell tidpunkt i ett nytt DateTime-objekt. Här kan man kanske diskutera huruvida det är bra design att en egenskap returnerar olika värden varje gång den används. Det typiska är att returnera värden som på ett eller annat sätt är beräknade, eller åtminstone har ett samband med objektets tillstånd, eller returnerar ett nytt objekt med känt eller förutsebart tillstånd. I kommersiella klassbibliotek finns exempel på klasser med egenskaper som allokerar systemresurser, exempelvis öppnar en fil första gången egenskapen ens nämns i klientkod. Det visar att det egentligen bara är fantasin som sätter gränser.
3.6 Operatorer Operatoröverlagring finns bara i vissa språk och är valfritt i CLS. Om operatoröverlagring tillämpas kan andra språk i klientkod använda de operatorer det språket känner till, och därför behöver det inte regleras. De flesta språk har ju åtminstone plus och minus, och några andra vanliga operatorer. CLS säger i detta ämne bara att om det implementeras i egna datatyper så ska det ske enligt vissa regler, exempelvis ska operatorerna vara statiska metoder. C# har operatoröverlagring, medan många andra språk inte har det. För att snabbt rekapitulera avsnittet operatorer börjar vi med att dra oss till minnes att operatorer bör ses som metoder som anropas på lite egendomliga sätt. I stället för att anropa med metodnamn och parametrar inom parenteser anropas de med vissa tecken som namn och parametrarna placerade före och/eller efter. Det är helt enkelt mera traditionellt att addera genom att skriva a + b än att skriva +(a, b). Därför finns i språket en uppsättning operatorer definierade för operander av vissa datatyper. Att skriva flera metoder med samma namn som befintliga men med andra parametrar kallas ju överlagring, så nu talar vi följaktligen om operatoröverlagring. Om man vill att ”+” ska ha någon betydelse då operanderna är objekt av vår klass gör vi en +-operator i klassen. Kopiering av kurslitteratur förbjuden. © Studentlitteratur
89
3 Klasser
Den avgörande syntaxuppfinningen är att en operator skrivs genom att göra en statisk och public metod med namnet operatorxx där xx är operatorns symbol i språket. Minst en av operanderna/parametrarna måste vara av den aktuella klassen. Det räcker alltså med en i de fall operatorn tar två operander. public static X operator+(X left, X right) {...} Metodens namn är alltså ”operator+”. Just +-operatorn tar ju två operander, så metoden ska ha två parametrar. Den vänstra operanden kommer att vara den första parametern. När klientkod innehåller a + b blir det alltså ett anrop till operator+(a, b) i de fall a och b är av den aktuella klassen. Några regler till: Nya operatorer kan inte uppfinnas, vi kan endast överlagra befintliga. Reglerna för prioritet och associativitet (beräkningsordning vid exekvering) gäller även för överlagrade operatorer och kan inte ändras. Om < överlagras måste > också överlagras. Om == överlagras måste också != överlagras. Och så en viktig princip: tilldelningsoperatorn (=) kan inte överlagras. Vi har sagt det tidigare – tilldelning av en referens är alltid tilldelning av identitet.
3.6.1 Design Det finns ytterligare en del regler att lära sig här, men vi ska först diskutera lite designfrågor. För det första så brukar man i samband med operatoröverlagring påpeka att det ska användas sparsamt. Om man då tar en titt i klassbiblioteket så ser man att många klasser har väldigt många operatorer. Vadå sparsamt? Med sparsamhet menar man främst att endast självklara – intuitiva – överlagringar ska göras. Klasser skulle ju vara svarta lådor att bygga applikationer av. Enkla och idiotsäkra utanpå, raffinerade inuti. Användare (det vill säga andra utvecklare) vill inte läsa dokumentation för att förstå och kunna använda en alltför smart klass. De vill kunna gissa hur man använder den, och då ska deras gissningar stämma. Objekt ska uppföra sig så som användare hoppas.
90
Kopiering av kurslitteratur förbjuden. © Studentlitteratur
3 Klasser
Gör alltså bara de operatorer som användare förutsätter, och framförallt – implementera dem med en logik som användare intuitivt accepterar. ”Keep it simple and stupid”. Om vi tar +-operatorn som ett designexempel, så bör man alltså fråga sig först om klientkod har någon nytta av att kunna göra a + b. Om så är fallet, finns det då en självklar logik i uttrycket a + b? Ända sedan tidernas begynnelse har a + b betytt att något nytt av samma typ som a och b skapas, och det nya är summan av a och b. Ett plus ett är det nya värdet två. Vi bör då hålla fast vid den logiken. En överlagrad +-operator bör alltså skapa och returnera ett nytt objekt av samma datatyp som parametrarna – av denna klass. Om operatorn alls är relevant kommer implementationen att vara självklar. Om implementationen ger ett antal designproblem ska operatorn antagligen inte överlagras alls. class Rational { int numerator, denominator; ... public static Rational operator+(Rational left, Rational right) { // beräkna ... return new Rational(...); } } I en klass för rationella tal är +-operatorn ganska självklar och implementeras enligt matematiska regler. Där skulle också ett antal andra aritmetiska operatorer vara relevanta. Vad gäller returtypen blir det med resonemang som detta ofta så att returtypen blir denna klass, alltså att operatorerna returnerar ett nytt objekt av den aktuella klassen. Men det är inte någon regel överlag, returtypen i de operatorer som normalt returnerar true eller false bör naturligtvis göra det även när de överlagras. Operatorer med två operander överlagras med två parametrar, operatorer med en operand överlagras med en parameter. Plus- och
Kopiering av kurslitteratur förbjuden. © Studentlitteratur
91
3 Klasser
minusoperatorerna är speciella genom att de finns i båda varianterna, och kan överlagras i båda varianterna. Ett litet extra problem när syntax för ++ och -- skulle uppfinnas är att de ju har två betydelser, beroende på om de står prefix eller postfix. Problemet är överraskande nog inte löst i språket. Överlagring kan ske endast med syntaxen operator++/operator-- och returtypen måste vara aktuell klass. Dessutom får operatorn inte förändra tillståndet i det anropande objektet, utan måste i praktiken returnera ett nytt objekt. Oavsett hur operatorn står i klientkod kommer samma operator att anropas. Därmed följer dessa två operatorer inte riktigt den logik de normalt har, vilket vi ju designmässigt sagt att vi ville.
3.6.2 Indexoperatorn En annan lite udda operator är indexoperatorn ([]). Vi är vana vid att använda den med vektorer, men den finns i klassbiblioteket i alla möjliga sammanhang där någon slags traversering av en samling kan ske. Den används dessutom i vissa samlingar för sökning, och kan då – hör och häpna – ha flera parametrar av olika datatyper. I klassen Hashtable finns en indexoperator som både kan användas för sökning och för insättning av nya element i samlingen! Hashtable tbl = new Hashtable(); tbl.Add(”Andersson”, 44); tbl.Add(”Karlsson”, 39); tbl[”Svensson”] = 41; // samma logik som Add! Console.WriteLine(tbl[”Andersson”]); // skriver ”44” Exemplet skapar ett samlingsobjekt av klassen Hashtable (en avancerad samlingsklass som beskrivs utförligare i senare kapitel). Därefter läggs tre objektpar in i samlingen. Varje par är ett namn och ett heltal (skonummer, kanske). Se nu noga på den sats som lägger in Karlsson i samlingen. Det sker med indexoperatorn och är ett mysterium – eller hur?
92
Kopiering av kurslitteratur förbjuden. © Studentlitteratur
3 Klasser
Indexoperatorn är som alla andra operatorer en metod. Här står alltså en metod till vänster om ett tilldelningstecken. Ett metodanrop tilldelas ett värde. Den varianten finns ju inte! Mysteriets lösning är att man valt en annan syntax för just indexoperatorn, för att den ska kunna stå som l-value. Tricket är att indexoperatorn görs med syntax som liknar egenskapers. Egenskaper har ju den speciella egenskapen (!) att de har en separat set- och getgren där setgrenen anropas om egenskapen står som l-value. Värdet som ska tilldelas har då det reserverade namnet value. På ganska likartat sätt skriver man en indexoperator: public object this[object key] { get { // returnera objektet associerat med key } set { if(...) Add(key, value); } } Ungefär så ser indexoperatorn i Hashtable ut. Den är som en egenskap med namnet this (åtskilliga implementationsdetaljer utelämnade här). Getgrenen är sökmetoden och setgrenen är en parallell till Add-metoden. I dokumentation kallas indexoperatorn ofta ”indexerare”, och i standarddokumentet beskrivs den tillsammans med egenskaper. Men logiskt sett är det operatoröverlagring det handlar om, så därför beskrivs den här. Ytterligare en särskild grupp av operatorer är kombinationerna med tilldelning, alltså +=, *=, och 0; } 134
Kopiering av kurslitteratur förbjuden. © Studentlitteratur
4 Arv och dynamisk bindning
public static bool operator= 0; } Vi såg att C#-uttrycket using använde IDisposable-interfacet. Ytterligare ett interface som används av språkkonstruktioner i C# är IEnumerable – även den med en enda metod som heter GetEnumerator. Den metoden ska returnera en ”enumerator”, ett objekt som i sin tur implementerar interfacet IEnumerator med metoder och egenskaper för att traversera samlingar. Det är de interfacen som är hemligheten bakom foreach-satsen! Exempel på standardinterface som rekommenderas att använda överallt där det är möjligt är IList och IDictionary i namnrymden Collections. De är implementerade i samlingsklasser i standardbiblioteket och har de grundläggande metoderna för insättning och sökning i samlingar. Genom att använda dem i klientkod blir man helt oberoende av vilken samlingsklass som faktiskt används. Fritt fram att prova olika klasser för att mäta tidskomplexitet etcetera!
4.7 Arvet från Object Den allestädes närvarande basklassen Object har nio medlemmar, samtliga metoder. Object är implicit basklass för alla klasser man skapar i C#. Om man anger någon basklass har man indirekt arv, om man inte anger någon basklass har man direkt arv. Structer ärver också Object, och samtliga enkla värdetyper ärver Object via klassen ValueType.
Kopiering av kurslitteratur förbjuden. © Studentlitteratur
135
4 Arv och dynamisk bindning
Tre av de nio metoderna är virtuella, och det är närmast obligatoriskt att omdefiniera dem i egna klasser. Några andra är statiska, några gäller jämförelser av objekt. Hela klassdefinitionen ser ut så här: public class Object { public Object() {...} ~Object() {...} public virtual string ToString() {...} public virtual int GetHashCode() {...} public virtual bool Equals(object obj) {...} public static bool Equals( object objA, object objB) {...} public static bool ReferenceEquals( object objA, object objB) {...} public Type GetType() {...} protected object MemberwiseClone() {...} } Här är mycket att titta på, och framförallt ska vi strax slutföra ett ämne som lämnades halvdant i ett tidigare kapitel – jämförelser av objekt. Operatorerna == och != bör i många klasser överlagras, och typiska sätt att göra det är att använda några av Object-metoderna. Det finns åtskilliga designmönster att ta till sig. Först ett par enkla konstateranden. Klassen har som ses ovan en public defaultkonstruktor. Det går alltså utmärkt att skapa objekt av Object. Vi har ännu inte sett något scenario där man behöver ett sådant, men det kommer åtminstone något fall senare i boken. Klassen är alltså inte abstrakt, vilket man kanske hade väntat. Det finns också en destruktor, men egentligen är den i klassen en metod som heter Finalize. Finalize är dock helt dold i C#, den kan inte anropas och inte omdefinieras. Om en C#-klass inte förses med destruktor enligt C#-syntaxen kommer skräpsamlaren inte att anropa Finalize och objekt anses bara bestå av minne. Om vi definierar en konstruktor kommer kompilatorn att skapa en omdefinierad Finalize och se till att basklassanrop m.m. sker enligt C#reglerna.
136
Kopiering av kurslitteratur förbjuden. © Studentlitteratur
4 Arv och dynamisk bindning
4.7.1 GetHashCode och ToString Så kommer de tre virtuella metoderna. ToString är avsedd att returnera en stäng som beskriver objektet. GetHashCode är avsedd att returnera ett så kallat hashvärde. Equals är avsedd att jämföra objektet med ett annat objekt för att returnera true om de har identiskt tillstånd. Alla tre har basklassimplementation som är nonsens och bör i de flesta fall omdefinieras. Först ToString och GetHashCode. Basklassimplementationen av ToString returnerar en sträng som innehåller klassnamnet (i brist på annat att göra!). I klassbibliotekets klasser är den omdefinierad så att den skapar de strängar vi förväntar oss från exempelvis Console.WriteLine. En int (Int32) med värdet 100 skapar strängen ”100”. Även en del klasser med mera komplext innehåll skapar begripliga strängar – men många har basklassversionen kvar eftersom det helt enkelt inte finns något vettigt sätt att beskriva objektets tillstånd. Åtminstone för debugändamål är det trevligt att kunna skriva ut objekt på terminal eller för loggning i fil, så om det finns något rimligt sätt att omdefiniera ToString – gör det! GetHashCode är allvarligare saker. Samlingsklassen Hashtable är exempel på en klass som anropar de ägda objektens GetHashCode för sin implementation. I verkliga tillämpningar med klasser också från andra bibliotek är det osäkert när och varför GetHashCode kan tänkas bli anropad. Vi bör helt enkelt se till att den ger ett användbart resultat. Vad är den då till för? Den klassiska datastrukturen hashtabell är en av de bästa sätten att skapa en samling av objekt. Idén bygger på att kombinera det bästa från vektorer (snabba på att lägga till och traversera elementen), och länkade listor (kan utökas dynamiskt). Strukturen består kort beskrivet av en vektor med referenser till länkade listor. Objekt läggs in i par i strukturen, den kallas därför ibland också ”dictionary” eller ”associativ vektor”. Det ena objektet – nyckeln – bestämmer var i vektorn objektet ska hamna. Det andra objektet – datat – är det som ska lagras. Ofta använder man ett fält i dataobjektet som nyckel.
Kopiering av kurslitteratur förbjuden. © Studentlitteratur
137
4 Arv och dynamisk bindning
Nu åter till GetHashCode. För att ett objekt ska kunna läggas i en ”bucket”, det vill säga välja plats i vektorn måste ett heltal tas fram ur nyckelobjektet. Heltalet ska vara stort, och givet ett stort antal realistiska objekt ska det ha jämn spridning så att objekten inte klumpar ihop sig i få långa länkade listor. Det är uppgiften för GetHashCode. Basklassversionen returnerar referensens värde (en minnesadress). Om ett objekt i en hashtabell ska kunna sökas, ska ett objekt med identiskt tillstånd kunna användas för att generera samma hashkod som det sökta objektet. Om vi letar efter ”Karlsson” i en hashtabell måste ”Karlsson” i ett objekt generera samma hashkod som ”Karlsson” i ett annat objekt. Det gör uppenbart inte basklassversionen, och den är följaktligen oanvändbar. Ett vanligt sätt att implementera GetHashCode är att använda bitoperatorerna. Med någon fantasifull (för att inte säga vettlös) kombination av fältvärden och bitoperatorer åstadkoms ett bitmönster som utgör ett stort heltal. Man kan naturligtvis göra på andra sätt också, exempelvis multiplicera några värden som tas ur fält i objektet. Huvudsaken är att talet är stort och att det har jämn spridning. Naturligtvis ska det inte ha något inslag av slumpgenerering. Två objekt med identiskt tillstånd måste generera samma hashkod.
4.7.2 Att jämföra objekt Den tredje av de virtuella metoderna är Equals. Basklassversionen jämför det anropande objektet och parametern genom att jämföra referensernas värde. Den returnerar alltså true om två referenser refererar till samma objekt. Observera att detta också är logiken i operatorerna == och != för referenser. Utan särskilda åtgärder har klientkod alltså inga möjligheter att jämföra objekt med avseende på tillstånd. I alla klasser där det har minsta relevans att kunna jämföra objekt bör vi därför vidta de åtgärder som behövs för det. I structer är det allra oftast så att jämförelse med avseende på tillstånd är det mest rimliga. I klasser är grundregeln lite överraskande att jämförelse med == och != bör ske med avseende på identitet. Observera att det är grundregeln, i de fall undantaget är uppenbart 138
Kopiering av kurslitteratur förbjuden. © Studentlitteratur
4 Arv och dynamisk bindning
– och det är inte sällan – är det ofta viktigt att jämförelse sker med avseende på tillstånd och då är det Equals som ska omdefinieras först. I de allra flesta av de fallen bör då också == och != överlagras, men det kan finnas fall då de faktiskt inte överlagras. Vad det gäller är alltså ofta att både överlagra == och !=, samt att omdefiniera Equals. Man skulle då gärna vilja se ett enda standardupplägg för det, men tyvärr finns flera tänkbara sätt. Man kan tänka sig att överlagra operatorerna för att sedan anropa dem inifrån Equals, eller det omvända. Men en intressant variant som ger en enkel och snygg lösning är att använda den statiska version av Equals som finns i Object. Den statiska Equals tar två object-referenser som parametrar och tar hand om en hel del av de trista detaljer som annars måste tas hänsyn till. Vi måste ju i vanlig ordning se till att klara osannolika men fatala parametrar/operatorer. Null-referenser får absolut inte användas för anrop, två identiska referenser måste omedelbart returnera true, två null-referenser likaså. Statiska Equals fixar just det. Så här är den dokumenterad i sammandrag: public static bool Equals(object objA, object objB) { ... } Returnerar true om objA och objB refererar till samma objekt, eller om objA och objB båda är null, eller om objA är skild från null och objA.Equals(objB) är true. I alla övriga fall returneras false. Statiska Equals är alltså en bra början för överlagringen av == och !=. Men själva jobbet måste göras i omdefinitionen av instansmetoden Equals. Den anropas uppenbart av statiska Equals om inga av de särskilda fallen föreligger. Det första att göra i Equals är att kontrollera att referensen inte är null (Equals ska ju också kunna anropas direkt), och att bekräfta huruvida de två objekt som jämförs verkligen är av samma klass. Referensen är ju av typen Object, och här får vi nytta av ytterligare en Object-metod, GetType. GetType är public men inte virtuell, och omdefinieras i sin tur aldrig. Den returnerar en Type – vad är då det? Type är klassbiblioKopiering av kurslitteratur förbjuden. © Studentlitteratur
139
4 Arv och dynamisk bindning
tekets metaklass. Ett Type-objekt innehåller all information om en klass. Där finns en stor mängd medlemmar för att vid exekvering undersöka den klass ett objekt kommer från, men för vårt behov just nu är det enbart ==-operatorn som är intressant. Det Typeobjekt man får genom att anropa GetType eller operatorn typeof är nämligen alltid samma objekt för en viss klass. GetType för två objekt av samma klass ger alltså två referenser till samma objekt och vi kan jämföra referenserna med ==-operatorn. if (this.GetType() == obj.GetType()) ... // this och obj är av samma klass Nästa sak att göra är att ta itu med själva jämförelserna. Sannolikt är den rimliga implementationen att samtliga fält i de två objekten ska ha identiska tillstånd. Men det kan tänkas fall då bara något eller några fält behöver jämföras. Fälten som ska jämföras är ju i sin tur objekt och de har Equals och == och != och jämförelsen ska med med rätt logik. Om allt stämmer returneras true, annars false. Ett typiskt utseende på Equals blir då. public override bool Equals(object obj) { if (obj == null) return false; if (this.GetType() != obj.GetType()) return false; if (this.f1 != obj.f1) return false; if (this.f2 != obj.f2) return false; ... return true; } Nu klarar Equals direkta anrop, och gör vad den ska när den anropas från statiska Equals. Resten blir mycket enkelt: public static bool operator==( MyClass objA, MyClass objB) { return Equals(objA, objB); } public static bool operator!=( MyClass objA, MyClass objB) { 140
Kopiering av kurslitteratur förbjuden. © Studentlitteratur
4 Arv och dynamisk bindning
return !Equals(objA, objB); } Detta upplägg kan användas i de flesta klasser där jämförelse av objekt är relevant. Nästa metod i Object har av namnet att döma också med jämförelser att göra. ReferenceEquals är en enkel sak som helt enkelt returnerar true om de två object-referenser som utgör parametrar är identiska, det vill säga om de refererar till samma objekt. Vid första anblick verkar denna statiska metod oerhört onödig. Både instansmetoden Equals, den statiska Equals, och ==-operatorn gör ju i basklassversionen just det – varför en metod till med samma logik? Jo, om man gör det som nyss rekommenderades med Equals och ==-operatorn, så finns ju faktisk inte längre någon möjlighet att jämföra identitet utifrån två referenser! ReferenceEquals är alltså den enda metod/operator som alltid jämför identitet. I detta sammanhang bör också påpekas att det i klientkod tyvärr inte syns vilken slags logik Equals och ==/!= har. Annars är ju språket i många sammanhang sådant att klientkod tydligt visar exempelvis om värde- eller referensöverföring av parametrar sker, det syns också i härledda klasser om en metod är en omdefinition eller inte, och om den är virtuell eller inte. Men användande av == och Equals ser likadant ut oavsett om de använder tillstånds- eller identitetslogik. Några förklarande kommentarer är därför ofta på sin plats. Klassbibliotekets klasser har oftast inte jämförelse av tillstånd. Men structerna i klassbiblioteket har det alltid. Structer är nämligen alltid härledda från klassen ValueType, och den omdefinierar Equals och språkreglerna säger att jämförelseoperatorerna för structer ger jämförelse av tillstånd. Bland klasserna är det stora undantaget klassen String – den har både Equals och jämförelseoperatorerna definierade för jämförelse av tillstånd.
Kopiering av kurslitteratur förbjuden. © Studentlitteratur
141
4 Arv och dynamisk bindning
4.7.3 Att kopiera objekt Sist tar vi en titt på metoden MemberwiseClone. Den är en protected metod som returnerar en Object-referens till ett objekt som är en bitvis kopia av det anropande. Metoden är inte virtuell och omdefinieras inte. Den ger alltså en exakt kopia av objektet även för härledda klasser, utan omdefinition. Det skapade objektet är alltid av samma datatyp som det anropande. MemberwiseClone gör sannolikt det konststycket genom att använda Type-objektet för det aktuella objektets klass. Där finns nämligen också metoder för att skapa objekt av klassen i fråga. Observera att kopieringen är bitvis, det som är referenser i objektet blir alltså referenser till samma ägda objekt i det nya objektet. Sådan kopiering kallas ofta ytlig kopiering (”shallow copy”). Kopiering av objekt med kopiering också av ägda objekt är liksom jämförelser av objekt något som är valfritt i klassdesign, och ofta gör man det inte. I de fall man inser att användare av klassen kommer att vilja ha möjligheten att kopiera objekt fullt ut med ägda objekt implementerar man interfacet ICloneable, som har den enda metoden Clone. Clone tar inga parametrar och returnerar en object-referens. ICloneable är implementerat i många av klassbibliotekets klasser, framför allt samlingsklasserna. När ICloneable.Clone ska implementeras kan man göra en del av jobbet genom att först anropa MemberwiseClone. Returen blir då ett objekt som är klart förutom de fält som är referenser. Man skapar sedan nya objekt genom att kopiera de ägda objekten och tilldelar dem till referenserna. Det hela kan bli ganska omfattande, och om man hamnar i logiska problem vars lösningar ger behov av att dokumentera för användare hur Clone fungerar, är man sannolikt ute på för djupt vatten. Objektkopiering ska, liksom överlagring av operatorer, vara intuitiv och självklar. class A { ... }
142
Kopiering av kurslitteratur förbjuden. © Studentlitteratur
4 Arv och dynamisk bindning
class B : ICloneable { int x; A obj = new A(); ... public object Clone() { B cpy = this.MemberwiseClone(); cpy.obj = obj.Clone(); // kopian ska ha egen A return cpy; } } Därmed har klassen Object beskrivits ganska utförligt, och det är ju så att allt är Object! Vid utvecklingen av nya klasser bör man följaktligen alltid tänka igenom alla de designalternativ som behandlats i detta avsnitt. Med största sannolikhet bör ToString och GetHashCode omdefinieras. Möjligen kommer klassens objekt att behöva jämföras – då ska Equals omdefinieras och troligen ska också operatorerna == och != överlagras. Behöver någon kunna skapa kopior av objekt? Då ska ICloneable vara interface och Clone definieras, sannolikt med hjälp av MemberwiseClone.
4.7.4 Tillämpning Nu ska vi återvända till den välbekanta klassen Rational för att se hur implementationer där kan se ut. Det krävs bara minimal eftertanke för att inse att alla de tre virtuella Object-metoderna bör omdefinieras, och hur. När vi gjort det kan vi också slutföra operatorerna. Paret == och != hänger fortfarande i luften. Först ToString och GetHashCode. public override string ToString() { return numerator.ToString() + "/" + denominator.ToString(); }
Kopiering av kurslitteratur förbjuden. © Studentlitteratur
143
4 Arv och dynamisk bindning
public override int GetHashCode() { return (numerator 0; }
Kopiering av kurslitteratur förbjuden. © Studentlitteratur
159
5 Structer
public static bool operator= 0; } public static Rational operator-(Rational obj) { return new Rational(-obj.numerator, obj.denominator); } // Typomvandlare public static implicit operator double( Rational obj) { return (double)obj.numerator/obj.denominator; } public static implicit operator Rational(int i) { return new Rational(i); } public static implicit operator Rational(double d) { return new Rational(d); } } Structen blev som synes avsevärt enklare eftersom en hel del av det vi behövde i klassen finns i alla structers basklass ValueType. Att vi behövde allt det i klassen berodde på att klassens karaktär är att hålla ett värde, och den är därmed en kandidat för struct i stället. Samtidigt kan vi se att som struct blev den lite mera primitiv. Fram160
Kopiering av kurslitteratur förbjuden. © Studentlitteratur
5 Structer
för allt har vi i struct-varianten ingen spärr mot det ogiltiga tillstånd som består i nämnaren noll. Structen blev en primitiv men snabbare variant av klassen. Båda kan motiveras.
5.2 Enum En klass i klassbiblioteket heter Enum. Den är härledd från klassen ValueType och är abstrakt. I C# finns nyckelordet enum som ger en struct härledd från Enum, och med några speciella egenheter. Notera att arv från klass till struct ju inte är tillåtet, men sker här återigen i lönndom, i en språkkonstruktion enum MyEnum {First, Second, Third}; Effekten av denna sats är att vi skapat en ny structtyp. Variabler av denna datatyp kan ha värdena First, Second eller Third. First, Second och Third kan ses som konstanta och statiska objekt av datatypen MyEnum. Låt oss nu skapa en MyEnum. MyEnum e = MyEnum.First; e = MyEnum.Second; if (e == MyEnum.Second) ... MyEnum.Second = MyEnum.Third; // Fel! Variabeln e kan tilldelas de tre värdena, och de tre värdena kan inte ändras. Däri ligger hela poängen med enum. Man kan alltid göra samma programlogik på andra sätt – exempelvis en int där vissa värden symboliserar vissa tillstånd – men en enum gör koden begripligare genom att värdena får namn. Typiska tillämpningsområden är då att enum får utgöra flaggor i form av parametrar till metoder där ett litet och begränsat antal värden är möjliga, exempelvis vid filöppning där mode och delning ska specificeras. I större sammanhang är enum särskilt användbara i kod som utgör någon slags tillståndsmaskin (automat). Enum-typen är lite överraskande inte garanterad att alltid ha något av de deklarerade värdena. En enum kan nämligen användas med åtskilliga operatorer. Att de kan jämföras med == och != är uppen-
Kopiering av kurslitteratur förbjuden. © Studentlitteratur
161
5 Structer
bart, och det är naturligt att man också kan jämföra med > och 99) throw new Exception(”Ogiltigt index!”); else return vec[i]; } } ... } Den triviala klassen MyArray innehåller en heltalsvektor och en indexerare som tar ett heltal och returnerar motsvarande element. Förpackningen av en vektor i en vektorklass är förvisso meningslös eftersom en vektor ju redan är ett objekt av klassen Array, och den har redan undantagsgenerering – men det hänger vi inte upp oss på just nu. Nyckelordet är throw. Effekten är ganska lik return, i det att metoden avslutas och stackens aktiveringspost avvecklas. Lokala variabler försvinner. Men det som kastas tillbaka till anroparen är inte ett returvärde, och för anroparen är effekten helt annorlunda, vilket vi strax ska se. Det som kastas tillbaka är i exemplet ett objekt av klassen Exception, och det är en CLS-regel och en C#-regel att det måste vara så. 168
Kopiering av kurslitteratur förbjuden. © Studentlitteratur
6 Undantag
Undantagsobjektet ska närmare bestämt vara ett Exception eller härlett från Exception. Basklassen Exception har uppenbart en konstruktor som tar en string som parameter, och denna string kan som i exemplet vara ett felmeddelande avsett för användaren. Följande konstruktorer finns: public Exception(); public Exception(string message); public Exception( string message, Exception innerException); Förutom den konstruktor som förser Exception-objektet med en string finns en defaultkonstruktor och en konstruktor som tar en string och ett Exception-objekt. Detta andra Exception-objekt blir då vad som kallas ”inner exception”. När vi strax tittar på typiska sätt att hantera undantag ska vi se att man kan vilja koppla ihop två eller flera undantagsobjekt, och då används denna konstruktor. Vidare har Exception-klassen en egenskap Message och en egenskap InnerException som inte överraskande returnerar strängen respektive det andra objektet. Men mycket intressant är också en egenskap StackTrace. Den är en string, och den fylls automatiskt med innehåll när undantaget kastas. Där finns då i klartext hela anropsstacken, det vill säga alla klass- och metodnamn i den anropskedja som ledde fram till undantaget. För debugändamål är den mycket värdefull. Basklassen Exception är alltså helt användbar som den är, men ändå avråds man från att använda den. Man bör i stället använda någon av alla de härledda klasser som finns i klassbiblioteket, eller skapa egna Exception-klasser. Saken är nämligen den att när undantaget ska ”fångas” kommer vi att ange vilket slags undantagsobjekt som ska hanteras, och alla undantagsobjekt är ju Exception! Att fånga Exception betyder alltså att fånga vilket undantag som helst, och vi vill göra undantagshanteringen lite mer specifik. I klassbiblioteket finns inte mindre än 59 stycken klasser härledda från Exception. Där finns exempelvis IndexOutOfRangeException, som verkar vara ett utmärkt val för vår vektorklass. Där finns ArgumentOutOfRangeException för de fall då ett hopplöst paraKopiering av kurslitteratur förbjuden. © Studentlitteratur
169
6 Undantag
metervärde kommer in till en metod, DivideByZeroException för division med noll, ObjectDisposedException för anrop till metoder där objektet har kasserats med Dispose (se tidigare kapitel), InvalidCastException för explicita typomvandlingar (även den använd i tidigare kapitel), FileNotFoundException för filöppning, och många fler. Några av dessa har extra fält och metoder/egenskaper för kompletterande felinformation, men många har ingenting utöver Exception. Poängen med att använda dem är då bara att man genom att kasta ett visst undantag signalerar vilken sorts fel det gäller och ger klienten möjlighet att hantera den sortens fel i ett separat kodblock. Om man härleder egna undantagsklasser bör man följa stilen att namnge dem med ”Exception” sist i namnet. Man bör dessutom härleda från ApplicationException, för att skilja basklassen från den SystemException som alla de tidigare nämnda färdiga Exception-klasserna är härledd från.
6.1.1 Klassen Rational Ett typfall där throw är enda sättet att hantera en omöjlig situation är fel i konstruktorer. I sådana finns ju inget returvärde alls. Ett exempel kan vara konstruktorn i den Rational-klass vi tidigare sett flera gånger. Klassen implementerar rationella tal, vilka består av två heltal, där det ena (nämnaren) aldrig får vara noll. Ett objekt med noll i nämnaren har ett ogiltigt tillstånd. Den intressanta konstruktorn har i tidigare exempel sett ut så här: public Rational(int numerator, int denominator) { if (denominator == 0) denominator = 1; this.numerator = numerator; this.denominator = denominator; Normalize(); }
170
Kopiering av kurslitteratur förbjuden. © Studentlitteratur
6 Undantag
Det är uppenbart ingen bra idé att i tysthet byta ut klientens eventuella nolla som nämnare mot ett. Men vi har heller inte velat acceptera noll som värde. Det rätta här är naturligtvis att helt förhindra skapandet av ogiltiga objekt. public Rational(int numerator, int denominator) { if (denominator == 0) throw new ArgumentException(); this.numerator = numerator; this.denominator = denominator; Normalize(); } ArgumentException är basklass för några andra Exception-klasser för olika ändamål. Man skulle också kunna tänka sig att använda ArgumentOutOfRangeException, och man skulle också kunna tänka sig att initiera undantagsobjektet med något felmeddelande. Det finns också en metod i Rational som kan förses med undantagsgenerering. Metoden CompareTo ser ut så här: public int CompareTo(object obj) { Rational r = (Rational)obj; if (this.numerator * r.denominator < this.denominator * r.numerator) return -1; else if (r.numerator == this.numerator && r.denominator == this.denominator) return 0; else return 1; } I första satsen sker en implicit typomvandling av parametern till en Rational. Det innebär att om klientkod försöker anropa metoden med något helt annat så kastas ett InvalidCastException. Lite bättre vore att kasta ett mera specifikt undantag, eller åtminstone ett med felmeddelande. Dessutom kommer denna explicita typom-
Kopiering av kurslitteratur förbjuden. © Studentlitteratur
171
6 Undantag
vandling faktiskt att fungera om parameterobjektet är härlett från Rational, och det är kanske inte bra logik. Med hjälp av GetType får man säkert besked om typen, och för att ge åtminstone lite specifikare undantag kastar vi även här ett ArgumentException. public int CompareTo(object obj) { if (this.GetType() != obj.GetType()) throw new ArgumentException(); Rational r = (Rational)obj; if (this.numerator * r.denominator < this.denominator * r.numerator) return -1; else if (r.numerator == this.numerator && r.denominator == this.denominator) return 0; else return 1; } Metoder som kastar undantag bör noggrant dokumenteras i det avseendet. Bäst är att använda XML-kommentarer (se avsnitt 2.7). Det finns en standardtagg ... för det ändamålet. Nu över till klientsidan – hur ser det ut då ett undantag genererats?
6.2 Hantera undantag Anrop till en metod som kan kasta ett undantag får ske utan några som helst extra åtgärder. Det som då händer är att om ett undantag genereras så avbryts även den anropande metoden på plats och undantaget kastas vidare. Detta är en extremt central mekanism. Programmet exekverar alltså inte vidare, utan hela anropskedjan avvecklas. Allt tas bort från stacken och alla metoder stoppas – ända upp till Main.
172
Kopiering av kurslitteratur förbjuden. © Studentlitteratur
6 Undantag
Main är ju alltid den första metoden i anropskedjan. Main innehåller normalt någon huvudslinga som anropar andra metoder som anropar andra... Oftast är någon av metoderna som anropas från Main sådan att den blockeras, det vill säga programmet tas bort från processortilldelningen och väntar på någon händelse som gör att operativsystemet åter väcker processen genom att ge den ny tid, och den blockerade metoden returnerar. Om ett undantag genererats långt bort i anropskedjan, och det inte hanterats någonstans på vägen hamnar det alltså i Main och där finns en fördefinierad undantagshanterare som ger användaren ett felmeddelande och stoppar programmet. Icke hanterade undantag blir man alltså mycket brutalt påmind om. För att hantera undantaget ska klientkod placeras i ett try-block. Omedelbart efter try ska i enklaste fall ett catch-block komma. Så här: try { RiskyMethod(); ... } catch (Exception e) { ... e.Message ... e.StackTrace ... } Om nu RiskyMethod genererar (eller kastar vidare) ett undantag kommer try-blocket att avslutas. Observera att lokala variabler i try-blocket därmed upphör. Exekveringen fortsätter i stället i det första catch-block vars undantagstyp matchar undantaget. I detta fall är datatypen Exception, vilket ju matchar alla tänkbara undantag. Om inget catch-block matchar undantaget kastas det vidare. Eventuell kod efter catch-blocket exekverar med eller utan undantag, förutsatt att inte något hopp i form av return, break eller liknande sker i catch-blocket och ett undantag existerar. I try-blocket kan finnas hur mycket kod som helst, även anrop till metoder som inte genererar undantag. Man kan mycket väl omge Kopiering av kurslitteratur förbjuden. © Studentlitteratur
173
6 Undantag
en hel metod med try, och lägga all felhantering som ett eller flera catch-block sist. Det går också att ha återkommande try-catchsekvenser, de kan ingå i slinguttryck och så vidare. Om man gör flera catch-block för olika slags undantag måste de placeras i rätt ordning. Minst härledd måste ligga först, annars nås det blocket ju aldrig. Kompilatorn bevakar det och ger felmeddelande om basklass ligger före härledd. Så den stora frågan: vad göra i catch-blocket? Det går nästan inte att säga något generellt alls om det, antalet tänkbara fall är mycket stort. Väldigt grovt kan man möjligen säga att det blir kombinationer på två teman: åtgärda felet respektive kasta undantaget vidare. Att åtgärda felet är ofta svårt, det ligger ju i sakens natur eftersom undantag används för tämligen hopplösa situationer. Om programmets vidare exekvering förutsätter en databasanslutning eller en socket eller en fil eller något sådant, och vi inte lyckades upprätta en sådan – meddela användaren och ge upp. I vissa fall kan vi klara oss med ett felmeddelande och en återgång till något tidigare tillstånd i programmet. Vidareförmedling av undantaget blir ganska vanligt som åtgärd. Den allra simplaste formen blir då att inte göra något alls. Helt utan try och catch kastas ju undantaget vidare automatiskt. Att bara låta saken ha sin gång anses dock vara lite dålig stil. Om man efter timmars funderande bestämmer sig för att låta undantaget gå vidare så syns ju inte det designbeslutet alls i koden, och näste utvecklare som ser koden blir också sittande att fundera ett par timmar. Markera då i stället att detta beslut är väl genomtänkt genom att explicit kasta undantaget vidare. För det finns särskild syntax: try { RiskyMethod(); ... } catch (Exception e) { throw; } 174
Kopiering av kurslitteratur förbjuden. © Studentlitteratur
6 Undantag
Observera att throw direkt följs av ett semikolon. Om man gör throw e kommer undantagsobjektet att skapas om, och den StackTrace som ingår i objektet kommer att starta på denna metod, inte den där undantaget ursprungligen uppstod. En lite mer komplicerad design är att förändra undantagsobjektet, eller att skapa ett nytt och kasta det. För sådana ändamål finns också möjligheten att skapa ett nytt och haka på det på det befintliga för att kasta tillbaka båda. Vi såg nyss att en konstruktor i Exception tar ett befintligt Exception som parameter, och att detta inre objekt sedan kan tas ut med egenskapen InnerException. På så sätt kan man komplettera undantagsobjektet. I verkliga fall måste man ofta kombinera dessa båda principer och både återställa och rädda det som räddas kan, samt ändra eller komplettera undantagsobjektet och kasta det vidare. En möjlighet som inte finns är att återgå till den sats som utlöste undantaget. Det skedde ju alltid i ett try-block, och av den aktiveringspost det blocket utgjorde på stacken finns intet kvar. Ibland är det så man skulle vilja göra, men det går alltså inte. Om try-catch sker i en slinga (for- while- do- foreach), går det utmärkt att börja på ett nytt varv. Huvudsaken är att try-blocket startar om. I catch-blocket blir det som vi sett ganska ofta ett hopp i exekveringen. Blocket kanske slutar med ett nytt throw, eller med ett return. Samtidigt finns ibland kod efter catch-blocket, som alltså exekverar då inget undantag uppstod, och den kan delvis tänkas vara densamma som i catch-blocket. Kanske ska systemresurser återlämnas oavsett om undantag uppstod eller inte. Att ha samma kodsekvenser på flera ställen är ju opraktiskt, så för ändamålet finns ytterligare ett nyckelord i detta sammanhang: finally. Ett finally-block kan förekomma efter catch, och exekverar då oavsett vad man gör i catch-blocket. Det ser konstigt ut, men inte ens ett return i catchblocket hindrar att koden i finally (efter return!) exekverar. try { RiskyMethod(); ... Kopiering av kurslitteratur förbjuden. © Studentlitteratur
175
6 Undantag
} catch (Exception e) { throw; } finally { Console.WriteLine(”Ständigt denne Vessla!”); } Finally exekverar alltid, med eller utan undantag, med eller utan hopputtryck i catch. Meddelandet i exemplet skrivs till skärm innan undantaget kastas vidare. Ytterligare en variant på try-catch-finally finns – finally i stället för catch! Det är tillåtet att direkt efter try ha ett finallyblock
6.3 Design Undantag ger minst tre påtagliga fördelar framför returvärden som felmeddelande: • De separerar returvärdet och returtypen helt från felmeddelandet. Felet identifieras i ett undantagsobjekt, som kan vara av valfri datatyp, dock alltid härlett från Exception. • De kan inte ignoreras. Ett undantag som inte hanteras fångas automatiskt i Main och ger ett felmeddelande och programavslut. • De behöver inte hanteras där felet uppstod. Att avstå från att hantera undantaget på plats betyder att även anropande metod avslutas och undantagsobjektet kastas vidare bakåt i anropskedjan. Det är alltså en ren designfråga var fel av någon viss typ ska hanteras. Därtill brukar man påpeka att undantagshantering också ger bättre stil i koden genom att separera normal logik från felhantering. Det är tröttsamt att försöka läsa kod där 90 procent består av felhantering, och den grundläggande logiken knappt går att skilja ut. 176
Kopiering av kurslitteratur förbjuden. © Studentlitteratur
6 Undantag
Samtidigt innebär undantagshantering ofta tidsödande designbryderier. Problemen blir egentligen inte besvärliga på så sätt att de är olösliga, utan tvärtom så att flera ungefär likvärdiga lösningar anmäler sig. Det blir svårt att bestämma sig, och svårt att vara konsekvent. En första och riktigt grundläggande fråga blir faktiskt när man ska använda undantag och när det trots allt är rimligt att använda returvärden. Vad är egentligen ett oväntat fel? Gränsfallen blir många. Ta som exempel traversering av en samling, eller läsning av en fil. Att indexera utanför en vektor anser vi vara ett klassiskt fall för undantag, men att stöta på filslutet anser vi vara en normal händelse – fast skillnaden är hårfin. I en samling där indexoperatorn är överlagrad och används som sökmetod, och man söker efter ett obefintligt element anser man att returvärde är det rätta, inte undantag. Var är konsekvensen? Lite diffust formulerat kan man nog säga att situationer som på något sätt är hopplösa implicerar undantag. Det finns inget vettigt värde att returnera när en vektor får ett ogiltigt index. Det finns heller ingen möjlighet alls att returnera något irrelevant från en konstruktor, eller att göra något när en fil som ska öppnas helt enkelt inte finns. Vi står handfallna. Då är undantag det rätta. Men när en sökmetod anropas är det lika rimligt att svara ja som att svara nej. Om det eftersökta saknas innebär det inte en situation som ska göra oss handfallna. Klientkod är nog också beredd på ett svar som betyder nej, och det kan exempelvis vara returvärdet null. Om klient är dumdristig nog att använda en returnerad referens utan att kolla att den är skild från null fås ett undantag där, och det är rimligt och rätt. Den i litteratur vanligaste formuleringen ”oväntade fel” kan med fördel tolkas som ”hopplösa fel”. Ytterligare en aspekt av undantagshantering i designsammanhanget är att principen passar bra in i den allmänna designprincipen att lösa ett problem i varje metod. Om metoder designas så att de gör en väldefinierad och gärna ganska allmän sak är sannolikheten för ett långt liv och möjligheter till återanvändning av klassen Kopiering av kurslitteratur förbjuden. © Studentlitteratur
177
6 Undantag
större. Om metoder görs för ”feta” blir de troligen snabbare överspelade av verkliga behov. Verkligheten och därmed kraven på mjukvara ändras ju som bekant i en besvärande hög takt. Men felhantering i ett program bestående av många små metoder är svårare eftersom man i varje enskild liten metod inte har kontexten – hur allvarligt är ett visst fel sett inom denna lilla horisont, och hur bör det hanteras? Undantagsmodellen gör det ju möjligt att låta undantaget kastas vidare uppåt i anropskedjan till passande logisk nivå, där kontexten ger tillräcklig öveblick och situationen kan bedömas och lämpliga åtgärder vidtas. Låt alltså denna möjlighet inspirera till bättre struktur i koden och därmed bättre återanvändningsgrad.
178
Kopiering av kurslitteratur förbjuden. © Studentlitteratur
7 Delegerare och notifierare
7 Delegerare och notifierare
Delegerare (delegates) och notifierare (events) är utmärkt bra uppfinningar som huvudsakligen klassbiblioteket bjuder på, och som i sig förpackar ganska komplex funktionalitet. Man måste ha någon förståelse för aktiviteten bakom kulisserna för att uppskatta dessa finesser. Vad det handlar om är att åstadkomma callback, och den variant av callback där klienten abonnerar på ett metodanrop för att koppla in sig själv i en anropskedja. I språk med pekare är det inte svårt att deklarera en funktionspekare och tilldela den pekaren adressen till en viss funktion. Lite mer komplext blir det om funktionen är en instansmetod. I exempelvis C++ ska ju den implicita this-pekaren komma med på något sätt när en instansmetod anropas, och där blir syntaxen därför tämligen excentrisk. I C# har vi delegerare, och det är här lika enkelt att skapa en delegerare för en instansmetod som för en statisk metod. Därtill ger delegerare ytterligare logik som gör det möjligt att anropa flera metoder på en gång! Ett delegerarobjekt är i princip ett objekt som associerats med en metod så att det kan användas som om det var ett metodnamn. Det delegerar då metodanropet till den associerade metoden. Ett notifierarobjekt är en sorts delegerarobjekt, med några särskilda egenheter som gör att klientkod på ett generellt sätt kan prenumerera på notifiering genom att anmäla en egen metod som callbackmetod. För att ta det hela från början kan man tänka sig ett typfall där en metod i en klass inte kan göra sitt jobb utan att anropa en annan metod, vilken är olika från gång till gång. Ett exempel kan vara en sorteringsalgoritm, där hela algoritmen implementerats i en metod – bortsett från den avgörande detaljen hur två givna element ska rangordnas. Den jämförelsen ska då implementeras i en metod som Kopiering av kurslitteratur förbjuden. © Studentlitteratur
179
7 Delegerare och notifierare
klienten utformar själv, och får anropad genom att skicka med en referens till sin metod i anropet till sorteringsmetoden. Ett renodlat callbackscenario. I C/C++ skulle det ske genom att man gör callbackmetoden statisk och skickar med en pekare som parameter i metodanropet. I Java skulle man typiskt deklarera callbackmetoden i ett interface, implementera interfacet i klientklasser och skicka med en interfacereferens i anropet. I C# skickar vi med ett delegerarobjekt. Ett delegerarobjekt är som en funktionspekare, med skillnaden att den är ett klassobjekt, och att den lika lätt associeras med ett visst objekts instansmetod som med en statisk metod.
7.1 Anrop via delegerare Om vi alltså tänker oss en statisk metod för sortering: class Services { public static void Sort( object CollectionToSort, SortDelegate SortTwoObjects) { while (...) if (SortTwoObjects(objA, objB)) ...; } ... } I denna fragmentariska kod anar vi två centrala saker: En av parametrarna är av en egendefinierad typ ”SortDelegate”. Objektet som är av denna typ används i metoden som om den vore ett metodnamn. Typen är en delegerartyp, och objektet är ett delegerarobjekt. Metoden Sort sorterar en given mängd genom att upprepade gånger anropa delegerarobjektet SortTwoObjects. SortTwoObjects är ett objekt som skickar vidare (delegerar) anropet till en verklig metod, och förmedlar tillbaka returvärdet. Hur ska då den metoden kopplas till SortTwoObjects? 180
Kopiering av kurslitteratur förbjuden. © Studentlitteratur
7 Delegerare och notifierare
Först måste vi se hur typen SortDelegate skapades. Typen är egentligen en klass härledd från standardklassen Delegate. I C# finns förenklad syntax för att göra arvet. delegate bool SortDelegate(object A, object B); Med denna enkla sats har man skapat en ny klass. Klassen har en konstruktor som tar en parameter, och parametern ska vara ett metodnamn. När man skapar ett objekt av klassen SortDelegate blir objektet därmed associerat till en viss metod. Metoden kan vara en statisk metod eller en instansmetod. Metoden ska ha den returtyp och den parameterlista som antyds i delegerardeklarationen. Notera att delegerardeklarationen liknar en metod, och det är avsiktligt. Returtyp–namn–parameterlista. Anta nu att klienten till Sort-metoden är ett objekt av klassen MyCollection, och att det i den klassen finns en metod som är avsedd att vara callbackmetod för sorteringar. Den har då just det utseende som delegeraren, alltså returnerar en bool och tar två object-referenser som parametrar. class MyCollection { ... public bool Compare(object o1, object o2) {...} } Nu är det klart att koppla ihop det hela. Det ska skapas ett objekt av samlingen MyCollection, det ska skapas ett delegerarobjekt som refererar till samlingsobjektets Compare-metod och den statiska Sort-metoden ska anropas med de två objekten som parametrar. Då sker upprepade anrop tillbaka till samlingens egen Compare, tills samlingen är sorterad. MyCollection coll = new MyCollection(); ... // fyll den med innehåll SortDelegate callback = new SortDelegate(coll.Compare); Services.Sort(coll, callback); //sortering sker De sista kodraderna kan finnas i någon metod som varken ingår i MyCollection eller i Services. Men ingenting hindrar att de gör Kopiering av kurslitteratur förbjuden. © Studentlitteratur
181
7 Delegerare och notifierare
det. Om MyCollection-objektet har en metod som anropar Sort, så kan delegerarobjektet skapas där. Man skriver då ofta this.Compare för att markera att Compare är en instansmetod, men det går då lika bra med bara Compare (förutsatt att metoden heter Compare, förstås). Ett ännu vanligare sätt är att skapa delegerarobjektet direkt i anropet till Sort, så som vi tidigare sett att man ofta skapar temporära objekt som bara behövs som parameter för ögonblicket. Services.Sort(this, new SortDelegate(this.Compare)); Här anropas Sort inifrån någon metod i MyCollection. Första parametern blir då detta objekt, och andra parametern ett delegerarobjekt som skapas med new på ort och ställe och initieras med metoden Compare för detta objekt.
7.1.1 Anrop till flera metoder Den delegerartyp vi deklarerade i detta exempel var avsedd att associeras med en enda metod. Den hade returtypen bool och delegerarmekanismen såg till att anropet via delegerare också slussade returvärdet tillbaka till anroparen. Delegerartypen blev en klass härledd från klassen Delegate. Det kan emellertid finnas en hel del finesser på ett järnspett. Den inledande beskrivningen av Delegate-klassen förteg en viktig egenhet: ett delegerarobjekt är i själva verket en samling referenser till metoder och deras aktuella objekt. Den konstruktor vi använde skapade en samling som innehåller en referens. Om vi skapar ett delegerarobjekt till av samma typ, och associerar det till en annan metod, så kan dessa båda konkateneras med hjälp av den statiska metoden Combine. Combine returnerar då ett nytt delegerarobjekt innehållande referenser till båda metoderna! SortDelegate d1 = new SortDelegate(obj1.Compare); SortDelegate d2 = new SortDelegate(obj2.Compare); SortDelegate both = (SortDelegate)Delegate.Combine(d1, d2);
182
Kopiering av kurslitteratur förbjuden. © Studentlitteratur
7 Delegerare och notifierare
Ett anrop till both ger nu ett anrop till Compare för vart och ett av objekten obj1 och obj2. Det går att konkatenera delegerarobjekt hur många gånger som helst. Nu var det ju så att vår SortDelegate returnerar en bool. Hur kan då flera anrops returvärden tas fram ur ett enda delegerarobjekt? Svaret är att det inte går. Logiken är att det sista anropets returvärde blir delegerarobjektets returvärde. Det går visserligen att förutse vilket objekts Compare som var det sist anropade, men det blir inte någon praktisk lösning. En delegerare avsedd för flera metodanrop görs alltså i praktiken void. Därmed tänker vi oss inte längre en delegerare som skickas som parameter för callback, utan snarare ett delegerarobjetk som är ett bestående fält i något annat objekt. Innan vi går vidare på den linjen bör dock andra tänkbara designlösningar för de callbackfall vi studerat också beaktas. Man brukar lite filosofiskt kunna påstå att en delegerare är som ett interface för en enda metod. Om en metod i klass A anropar en metod i klass B, som i sin tur behöver anropa tillbaka till en metod i A – varför inte då beskriva den metoden i ett interface och skicka med en referens till A-objektet i form av en sådan interfacereferens? B-metoden kan då använda interfacereferensen och anropa dess enda metod. Så gör man i Java. Ja, varför inte? Det upplägget är faktiskt lika anständigt objektorienterat som användandet av delegerare. Array-klassen har en Sortmetod som fungerar så. Men det fungerar bara för callback till en enda metod, inte till flera. Det ger heller inte de mekanismer som snart ska föra oss till notifierare, vilket är en mycket fruktbar teknik, särskilt ofta använd i klassbibliotek för fönstersystem. Combine-metoden nyss är mycket användbar, den är public och static och alltså synlig överallt. Men den som läst kapitlet om operatoröverlagring uppmärksamt anar genast att Combine är en utmärkt kandidat att dubblera med en +-operator. Så är också fallet. Dessutom är det ju så att += är detsamma som ett anrop till plusoperatorn och därefter en tilldelning, så nu kan vi se det typiska sättet att skapa och använda delegerare för flera metodanrop. delegate void MyDelegate(MyClass obj); // typen MyDelegate aDelegate; // objektreferensen, null
Kopiering av kurslitteratur förbjuden. © Studentlitteratur
183
7 Delegerare och notifierare
... aDelegate += new MyDelegate(obj1.Method); // första aDelegate += new MyDelegate(obj2.Method); // andra ... // fler referenser till metoder kan läggas till ... if (aDelegate != null) aDelegate(aMyClass); // anrop till samtliga, med // samma parameter Combine anropas av plus-operatorn, och den förstår också att konkatenera en null-referens med en som verkligen har ett delegerarobjekt, så den första +=-satsen fungerar även om den ser vådlig ut. Nu är frågan var dessa intressanta kodfragment hör hemma – i vilka klasser, och hur dessa klasser hör samman i en verklig design. Här finns som vanligt många tänkbara designmönster, men ett typiskt är att delegerarobjektet finns i den klass vari också anropet via delegeraren sker. Vi kan då kalla den klassen serverklass, eller kanske publicerare. Objektet av den klassen skickar ju ut information genom att anropa någon metod hos mottagarna, och mottagarna kan på eget initiativ beställa den informationen. Som när man prenumererar på en tidning. Mottagarna kallas därför ofta prenumeranter.
7.1.2 Publicerare och prenumeranter För att illustrera delegerarobjekt som fält i serverklassen tänker vi oss en nonsensklass Publisher. Den har en metod som när den anropas i sin tur anropar ett antal instansmetoder för ett antal objekt. De objekten har själva prenumererat på anropen genom att lägga till en enkel delegerare till Publishers befintliga delegerare (som ju antingen inte existerar, eller redan innehåller en eller flera referenser till metoder). delegate void CallEveryone(); class Publisher {
184
Kopiering av kurslitteratur förbjuden. © Studentlitteratur
7 Delegerare och notifierare
private CallEveryone call = null; public void AddSubscriber( CallEveryone newSubscriber) { call += newSubscriber; } public void DoCall() { if (call != null) call(); // alla anropas } ... } Delegerartypen är deklarerad först, utanför klassen. Den skulle också kunna vara deklarerad inuti klassen, och public. Klasser kan ju nästlas. Klassen har referensen till delegerarobjektet private, den initieras med null, och en accessmetod för att konkatenera med nya delegerare. Metoden DoCall kollar om det finns några prenumeranter, och anropar i så fall samtliga på en enda gång. En klass där objekten själva prenumererar kan då vara: class Subscriber { public Subscriber(Publisher subscribeTo) { subscribeTo.AddSubscriber( new CallEveryone(this.CallMe)); } void CallMe() { ... // anropas från Publisher.DoCall } ... } Här är Subscriber sådan att den har en konstruktor som tar en referens till en Publisher som parameter. I konstruktorn prenumererar det nya objektet då genast på anrop till sin metod CallMe. Allt är klart för att skapa objekt och sätta igång med anropen via Kopiering av kurslitteratur förbjuden. © Studentlitteratur
185
7 Delegerare och notifierare
delegeraren. I någon klientkod, sannolikt i någon metod i en tredje inblandad klass kan det ske så här: Publisher pub = new Publisher(); Subscriber sub1 = new Subscriber(pub); Subscriber sub2 = new Subscriber(pub); Subscriber sub3 = new Subscriber(pub); Subscriber sub4 = new Subscriber(pub); pub.DoCall(); // alla anropas I detta renodlade skolboksexempel har koden ingen annan logik än just att åstadkomma anropen till CallMe-metoden för ett antal objekt. I praktiken är det naturligtvis inte ett självändamål, utan ingår i någon design för något vettigt ändamål. Då kan koden bli betydligt snårigare. Men det viktiga att förstå i detta scenario är att delegerarobjektet finns som ett fält i någon klass, och att det utgör den gemensamma knutpunkten för anrop från en metod till ett antal andra. Samt att uppsättningen prenumeranter varierar vid exekvering. Viktigt att förstå är också att det likväl som en delegerare kan anropa flera metoder, kan en och samma metod vara målet för flera delegerare. En till många, många till en och många till många är meningsfulla och vanliga mönster. En annan sak som möjligen kan kännas diffust är sambandet mellan delegerare och trådar: det finns inget. Allt vi sett nu sker i en och samma tråd. Metoderna som anropas via en delegerare anropas en i sänder, i samma tråd. Objektsystemet kan däremot vara ganska komplicerat. Delegerarobjektet kan finnas i en överordnad (ägande) klass, och både prenumeranter och publicerare kan vara objekt av andra klasser. Delegerarobjektet kan ha synlighet som gör att inblandade klasser ser det utan accessmetoder (internal, protected), etc. Delegerarobjektet är ju av en klass härledd från klassen Delegate, och det enda vi sett i den är Combine och +-operatorn. De är visserligen i särklass viktigast, men där finns också en del annat. Först bör påpekas att det naturligtvis går att avbryta en prenumeration, med metoden Remove, vilken föga överraskande är dubblerad med minusoperatorn. Men det går också att anropa de refererade metoderna med metoden DynamicInvoke, och det går att plocka fram 186
Kopiering av kurslitteratur förbjuden. © Studentlitteratur
7 Delegerare och notifierare
enskilda referenser med GetInvocationList. Det går även att ta fram referenser till alla prenumererande objekt.
7.2 Anrop via notifierare Notifierare (events) beskrivs ofta som något helt annat än delegerare (delegates). Så är det inte. Det konkreta förhållandet är att notifierare bygger på delegerare. I förra avsnittet var delegerarobjektet inte public i något exempel, men man anade att allt vore enklare om man gjorde så. Men nu har vi ju med skärpa slagit fast att i C# gör man aldrig fält public. Om man gör en delegerare public kan exempelvis alla klienter undersöka delegeraren och via den plocka fram alla andra klienter – där finns ju metoder för att ta fram varje klient och även att anropa deras metoder (GetInvocationList och DynamicInvoke)! Det vore en riktig groda. För att ge en enkel lösning finns nyckelordet event. Genom att lägga till event i den sats där delegerarobjektets referens skapas får man på ett mycket enkelt sätt en delegerare som utan problem kan vara public. Den kommer enbart att göra synligt två operatorer mot klienter: += och -=. Det enda klienter kan göra är alltså att lägga till eller ta bort prenumerationer. Exempelklasserna i förra avsnittet kan förenklas: delegate void CallEveryone(); class Publisher { public event CallEveryone call = null; public void DoCall() { if (call != null) call(); // alla anropas } ... }
Kopiering av kurslitteratur förbjuden. © Studentlitteratur
187
7 Delegerare och notifierare
class Subscriber { public Subscriber(Publisher subscribeTo) { subscribeTo.call += new CallEveryone(this.CallMe)); } void CallMe() { ... // anropas från Publisher.DoCall } } I ett program med klassmedlemmar flaggade event kan man alltid ta bort ordet event och allt fungerar som förut! Men med en flagrant lucka i den objektorienterade mur som heter inkapsling och datagömning.
7.2.1 Fönstersystem Med det lilla tillägget event till delegerarmekanismen har vi nu all infrastruktur för typisk och klassisk ”händelsebaserad programmering”. Som nämnts är ett stort tillämpningsområde fönsterprogrammering. Ett fönster är en systemresurs som skapas genom några APIanrop. I Windows är API-funktionerna för fönsterhantering inlemmade i det övriga API-et för operativsystemet, i Unix är det separata produkter. En av delarna i den datastruktur som motsvarar fönstret är en pekare till en funktion – fönsterproceduren. När användaren pekar och klickar, eller trycker ner tangenter, kommer operativsystemet att placera ett meddelande i en meddelandekö associerad till den process som skapat det fönster som har fokus. Huvudslingan i den processen hämtar meddelandet och anropar det aktuella fönstrets fönsterprocedur. Om nu fönstret motsvaras av ett objekt är det en utmärkt design att i objektet ha en notifierare. Att utveckla applikationer mot ett sådant fönstersystem består då i att skriva prenumeranter på olika 188
Kopiering av kurslitteratur förbjuden. © Studentlitteratur
7 Delegerare och notifierare
händelser och prenumerera. Den kod som kopplar en metod till en viss händelse, såsom ett musklick, blir en enda rad. Fönsterproceduren ingår i fönsterklassen och den finns i ett klassbibliotek. Utvecklaren behöver bara veta vad notifieraren för en viss slags händelse heter, skriver sin hanterarmetod och prenumererar med en +=. När användaren klickar med musen anropas metoden. Verktyg för utveckling av fönsterbaserade applikationer har dessutom ofta kodgenerering av just sådan kod. Det kan vara så att fönstret ritas visuellt och förses med ”kontroller”, och när man i verktyget klickar eller dubbelklickar på en kontroll genereras koden som skapar en tom metod och registrerar denna som prenumerant via någon notifierare för någon typisk händelse, ofta ett musklick.
7.2.2 Notifierare i interface Några bakomliggande fakta för den extra intresserade läsaren: När man använt delegerare och notifierare så som beskrivits här undrar man kanske vad skillnaden egentligen är. Den ser mycket liten ut. Efter kompilering är skillnaden faktiskt avsevärd. Ett delegerarobjekt är ett fält, en medlemsvariabel. En notifierare är dels ett delegerarobjekt, dels en metod! När en notifierare ingår i ett interface (vilket ju överraskande är tillåtet) eller deklareras abstract utgörs den enbart av metoddelen. Metoden anropas aldrig explicit, utan har den särskilda syntaxen att anropas via += och -=. Dessa två teckenkombinationer är förvillande lika kombinationen av plusoperatorn respektive minusoperatorn och tilldelningsoperatorn, men är alltså inte alls det. Man kan inte låta bli att jämföra med egenskaper (properties). De är också metoder som ser ut som fält. Om man vill kan man omdefiniera += och -= för en notifierare. Syntaxen liknar egenskapers. Högra operanden har precis som hos egenskaper namnet value, men en märklighet är att den bakomliggande delegerarens namn inte är standardiserat. Det måste letas fram i dokumentationen för den aktuella kompilatorn.
Kopiering av kurslitteratur förbjuden. © Studentlitteratur
189
7 Delegerare och notifierare
public event MyDelegate MyEvent { add {...} // högra operanden från klient heter remove {...} // här liksom i egenskaper value } Ändamålet med denna konstruktion är att notifierare ska göras public och ändå se ut som fält (precis som egenskaper). Ytterligare en poäng är att notifierare kan modifieras med samma modifierare som metoder. De kan ingå i interface, göras abstract, virtual/ override eller new. Man blir då lätt fundersam när man i kodexempel, även från de bästa familjer, ser event flaggade private och dolda bakom metoder som sätter/avlägsnar prenumeranter. En design mycket lik den gamla principen att förvara pengarna i madrassen och för säkerhets skull låsa in madrassen i bankfacket. Slutligen en liten men anmärkningsvärd detalj. Om notifierare ingår i interface eller deklareras abstract kan de implementeras så som exemplet ovan med add och remove, men mera vanligt är naturligtvis att man vill ha standardimplementationen. Den i sin tur ser exakt likadan ut som den abstrakta deklarationen! // i ett interface: event MyDelegate MyEvent; // i implementerande klass: public event MyDelegate MyEvent; Utan den upprepade deklarationen i implementerande klass fås ett kompileringsfel som kan vara nog så svårt att tolka.
7.3 Parametrar Hittills har i alla exempel delegerartypen deklarerats explicit – vi har så att säga designat delegeraren varje gång. Men efter tillräckligt många tänkbara fall inser man att delegerartypen får ett ofta förekommande utseende.
190
Kopiering av kurslitteratur förbjuden. © Studentlitteratur
7 Delegerare och notifierare
Att returtypen på en notifierare skulle vara void blev tidigt klart, eftersom den ska kunna anropa flera prenumeranter, och då är returvärde ganska meningslöst. Men vad är typiska behov i parameterlistan? Det första som ofta uppstår behov av är att i klientkod kunna identifiera avsändaren. Om notifieringarna från tio olika kontroller i ett dialogfönster alla ger anrop till en och samma prenumererande metod behöver vi sannolikt i varje enskilt anrop kunna se vilken kontroll som anropade just denna gång. En parameter bör alltså vara det avsändande objektet. Men i övrigt blir parametrarna ganska olika. I fönstersystem blir de ofta koordinater eller teckenvärden. I andra sammanhang blir de kanske kompletterande data av allehanda slag. Om man ändå vill något lite standardisera delegerartypen kan man tänka sig en basklass med grundläggande funktionalitet, avsedd att göra härledda klasser av. I härledda klasser kan utvecklaren själv lägga till fält och metoder för kompletterande information om notifieringen. Standardbibliotekets klass EventArgs är just en sådan klass, och standardbibliotekets delegerartyp EventHandler är just en sådan delegerartyp. Delegeraren ser ut så här: delegate void EventHandler( object sender, EventArgs e); Det är naturligtvis ingen lag att man måste använda denna delegerartyp och denna parametertyp, men klassbiblioteket gör det i hög grad, och kommersiella klassbibliotek för fönstersystem gör det också i hög grad, så varför inte? I anropet via en notifierare blir då den första parametern oftast this. Det är ju det aktuella objektet som ska identifiera sig. I de fall man inte har någon kompletterande information att skicka med i EventArgs-parametern finns en särskild EventArgs-konstruktor i form av en egenskap, den heter Empty. Om vi modifierar det tidigare kodexemplet ytterligare med dessa nya insikter blir det då: class Publisher { Kopiering av kurslitteratur förbjuden. © Studentlitteratur
191
7 Delegerare och notifierare
public event EventHandler call = null; public void DoCall() { if (call != null) call(this, EventArgs.Empty); // alla anropas } ... } class Subscriber { public Subscriber(Publisher subscribeTo) { subscribeTo.call += new EventHandler(this.CallMe)); } void CallMe(object sender, EventArgs e) { ... // anropas från Publisher.DoCall } } Jämfört med tidigare finns nu ingen egen delegerartyp deklarerad, standardtypen EventHandler används. Därmed måste den anropade metoden ta de två parametrarna object och EventArgs. I anropet är object-parametern this, och EventArgs-parametern ett tomt EventArgs-objekt. EventArgs är en mycket blygsam klass. Den har faktiskt inga riktiga medlemmar alls. Det enda som finns är en defaultkonstruktor och den nämnda egenskapen Empty – som anropar defaultkonstruktorn. Samt naturligtvis det som ärvs från Object. Man använder den alltså när inga kompletterande data ska skickas till prenumeranten. För de typfall då data ska skickas finns i klassbiblioteket bara ett par härledda klasser, AssemblyLoadEventArgs och UnhandledExceptionEventArgs. Vi går här inte in på de fall då de är relevanta, utan konstaterar bara att de är exempel på klasser som härletts från EventArgs och försetts med fält och metoder.
192
Kopiering av kurslitteratur förbjuden. © Studentlitteratur
7 Delegerare och notifierare
I kommersiella klassbibliotek finns exempel på mycket omfattande klasser härledda från EventArgs. Man kan tänka sig att i EventArgs-parametern vara hjälpsam nog mot klienten genom att paketera diverse sådant som klienten sannolikt behöver, alltså inte enbart råa data om den händelse (ett musklicks koordinater, etc) det gäller. Om den anropade metoden normalt behöver ytterligare systemresurser kan man tänka sig att de skapas och förpackas i EventArgsobjektet så att utvecklingen av metoden blir maximalt enkel och säker. I en notifiering för anrop till ritkod som ritar om ett fönster kan exempelvis i EventArgs-parametern finnas de objekt som motsvarar systemresurser som krävs för anrop till grafik-API.
Kopiering av kurslitteratur förbjuden. © Studentlitteratur
193
7 Delegerare och notifierare
194
Kopiering av kurslitteratur förbjuden. © Studentlitteratur
8 Attribut
8 Attribut
I CLI är mycken möda lagd på att undvika behov av dokumentation. Erfarenhetena från världshistoriens mjukvaruutveckling är att om ett stycke återanvänd kod innehåller fallgropar och slamkrypare för utvecklaren, så att denne måste läsa på noga för att inte göra en bugg, så har vi en bugg. För att mjukvaruutveckling ska bli säker och effektiv måste möjligheterna till felaktigt återanvändande minimeras. Separat dokumentation, bristande typsäkerhet, särskilda gränssnittfiler och exportfiler – allt sådant är källor till fel. I CLI är behovet av sådana ”disconnected” resursfiler och dokumentation minimerat eller helt eliminerat. Vi har sett att kompilerade datatyper med minimal ansträngning görs till självbeskrivande komponenter. Det självbeskrivande består i att en mängd information om alla ingående datatyper i ett assembly alltid skapas och läggs i samma fil, i den del som kallas manifestet. Manifestet kan läsas vid exekvering, framförallt genom att skapa ett objekt av klassen Type. För varje datatyp finns ett Type-objekt med nästan allt om alla medlemmar i datatypen. Att ta fram information om en datatyp vid exekvering kallas allmänt ”reflection”. Vi har också sett att man förutom vanliga kommentarer i källkod kan förse källkod med XML-formaterade kommentarer som dessutom kan kompileras till en separat dokumentation, vilken kan läsas och användas på ett systematiskt sätt (avsnitt 2.7). Nu ska vi se ytterligare ett paket av mekanismer för att integrera information. Attribut är, mycket grovt uttryckt, kommentarer eller metainformation som kompileras in i assemblyts manifest och kan Kopiering av kurslitteratur förbjuden. © Studentlitteratur
195
8 Attribut
läsas vid exekvering. Attribut har i sig ingen funktionalitet, de exekverar inte som en del av programmet. Men eftersom VES i vissa fall läser attribut i de klasser som laddas och exekverar, och gör saker på olika sätt beroende av attributs värden så har de i den bemärkelsen funktionalitet. Attribut är alltså sättet att själv utöka informationen i manifestet. Det i särklass vanligaste attributet heter CLSCompliant, och det kan tjäna som ett inledande exempel. Alla standardklasser har attributet CLSCompliant, och attributet har ett fält av datatypen bool. Det är true om den aktuella datatypen är CLS (följer CLS-reglerna), annars false. Fältet kan läsas med en egenskap som heter IsCompliant. Attributet kan kontrolleras när en klass laddas, och kan också kontrolleras av kompilatorn. public class MyClass { ... [CLSCompliant(false)] public void Method(UInt32 x) { ... } } I denna klass är en metod försedd med attributet CLSCompliant med värdet false. Om assemblyt är markerat CLSCompliant(true) och en public metod i en public klass har en icke-CLS parameter ger det ett kompileringsfel. Så är det med parametern av typ UInt32 i exemplet. Med metoden markerad med attributet CLSCompliant(false) kan VES läsa attributet, och kompilatorn är därmed nöjd. Logiken i exemplet är en smula komplex, men det intressanta är att konstatera att attributet kan läsas och ha effekt både vid kompilering och vid exekvering. Attribut placeras inom hakparenteser och gäller det efterföljande ”kodelementet”. Kodelementet kan vara en hel klass, en medlem, eller en mycket liten detalj såsom en enstaka parameter i någon metod, eller returtypen från någon metod. I vissa fall kan det bli tvetydigheter när kompilatorn ska avgöra vad attributet gäller, då kan attributet förses med en flagga som förtydligar det. [assembly:CLSCompliant(true)]
196
Kopiering av kurslitteratur förbjuden. © Studentlitteratur
8 Attribut
Här blir effekten att hela det assembly som koden ingår i markeras CLSCompliant. Det är den högsta nivån, eller största räckvidden om man så vill. Andra tillåtna flaggor är type för en hel klass eller annan typ; method, field, event och property för en medlem; return eller param för returvärde respektive parameter. De enda man egentligen behöver är return och param eftersom attributet annars kommer att gälla efterföljande metod i sin helhet. [MyAttribute] MyType Method() { ... } // attributet gäller Method // alternativt: [return:MyAttribute] MyType Method() { ... } // attributet gäller // returvärdet från Method Ett attribut har typ, och typen är en klass. I klassbiblioteket finns en abstrakt basklass för alla attributklasser, den heter Attribute och den är obligatorisk som basklass för attribut. Härlett från den finns 24 stycken attributklasser för olika ändamål. Endast några intressanta ska nämnas här. • CLSCompliantAttribute – markerar CLS-kompatibilitet. Har en enda bool-egenskap. • ConditionalAttribute – användbar för debugging genom att anrop till en markerad metod kan stängas av/slås på. • DllImportAttribute – gör det möjligt att förse en metod med all den information som VES behöver för att slussa vidare anropet till en icke-CLI-funktion i en dll-fil. • ObsoleteAttribute – det markerade kodelementet är en klass eller en medlem som är föremål för skrotning. En bool-egenskap avgör om användande ska ge en varning eller ett kompileringsfel. • SecurityAttribute – abstrakt basklass för flera attributklasser som ger möjlighet att styra behörighet till kodelement. Härledda klasser (direkt eller indirekt) är exempelvis FileIOPermissionAttribute, SocketPermissionAttribute och WebPermissionAttribute. Hur dessa och andra attributklasser kan användas är ämne nog för en hel bok. Ett litet praktiskt och komplett exempel kan ändå vara intressant. Attributet DllImportAttribute löser på ett elegant sätt problemet att inifrån CLI-kod anropa kompilerade C-funktioKopiering av kurslitteratur förbjuden. © Studentlitteratur
197
8 Attribut
ner i dll-filer. Så är ju nämligen många operativsystems API-funktioner levererade. class MsgBox { [DllImport(”user32.dll”)] public static extern int MessageBoxA( int hWnd, string msg, string cap, int type); } ... MsgBox.MessageBoxA(0, ”Hello”, ”Hellobox”, 0); Detta är faktiskt allt som behövs för att anropa Windowsfunktionen MessageBoxA. Även parametrarna, som inte har exakt samma datatyp i C# och i den C-deklarerade MessageBoxA, konverteras så att exempelvis string blir char-pekare. Om ett attribut har datatypen klass så är följaktligen ett attribut ett klassobjekt. Objektet skapas vid kompilering. Det associeras till den aktuella datatypens Type-objekt, och det är genom att ta fram Type-objektet som man kan läsa attribut, vilket vi ska se i nästa avsnitt. Attribut-objektet skapas genom ett explicit konstruktoranrop, men utan new. [Obsolete(”Has been replaced by Y”)] class X { ... } Klass X i detta exempel har försetts med standardattributet ObsoleteAttribute, där attributobjektet har en sträng som initieras i konstruktor. Konstruktoranropet har också ett par andra särskilda syntaxregler. Den klass varav attributobjektet skapas heter ofta inte exakt detsamma som konstruktorn (vilket den uppmärksamme läsaren noterade i exemplet ovan där ObsoleteAttribute plötsligt hette Obsolete)! Dessutom kan konstruktorn anropas med parametrar som inte finns i dess deklaration. Dessa små finesser kunde vi ha levt utan, men det är vedertagen stil att använda dem. Det första var att klass och konstruktoranrop kan ha olika namn. Klassen bör ha ett namn som slutar med Attribute. Vi ska se mera om egna attributklasser i ett följande avsnitt. Om namnet slutar 198
Kopiering av kurslitteratur förbjuden. © Studentlitteratur
8 Attribut
med Attribute kan konstruktoranropet utesluta Attribute. Attributet Obsolete i exemplet ovan är ett objekt av klassen ObsoleteAttribute. Det andra var att konstruktorer kan förses med aktuella parametrar som inte motsvaras av formella. I parameterlistan är det tillåtet att explicit tilldela public-medlemmar värden. Det måste då ske efter ”vanliga” parametrar. De medlemmar som initieras på detta sätt kan vara fält eller egenskaper. Om de är egenskaper måste de ha både get- och setgren. [DllImport("Kernel32", CharSet=CharSet.Auto, CallingConvention=CallingConvention.StdCall, EntryPoint="GetLocalTime")] Detta attribut har en konstruktor som tar en string. Den kan anropas med en sträng och ett antal namngivna parametrar som motsvarar publika fält med namnen CharSet, CallingConvention och EntryPoint. Ytterligare en syntaxmässig anomali är att anrop till defaultkonstruktor kan ske helt utan parenteser. [Obsolete] public void OldMethod(int x) { ... }
8.1 Läsa attribut Det blev många detaljer om hur attributobjekt skapas. Men ändamålet är ju att vid exekvering läsa attribut och agera därefter. Somliga attribut läses också vid kompilering och kompilatorn agerar därefter – det är ConditionalAttribute och ObsoleteAttribute som bevakas av kompilatorn. När attribut för en viss datatyp ska undersökas vid exekvering sker det enkelt med ett anrop till Attributklassens statiska metod GetCustomAttributes. Den tar en Type som parameter, och en Type får man med antingen typeof och typnamn, eller med ett objekts GetType (en Object-metod). Kopiering av kurslitteratur förbjuden. © Studentlitteratur
199
8 Attribut
Type t = typeof(SomeClass); // eller t = obj.GetType(); Attribute[] attributes = Attribute.GetCustomAttributes(t); foreach (Attribute attribute in attributes) { if (attribute is MyAttribute) ... if (... } Först hämtades i exemplet Type-objektet för den aktuella typen med typeof-operatorn. Därefter anropades Attribute.GetCustomAttributes, vilken returnerar en vektor av attribut, som ju alla har basklassen Attribute varför vi sparar den i en Attribute-vektor. Slutligen traverseras vektorn och varje element testas för klasstillhörighet med is-operatorn. Den returnerar ju true om objektet är av angiven klass. Om attributobjektet är det sökta kan man göra en explicit typomvandling och undersöka attributobjektets tillstånd. Om attribut för enskilda medlemmar i en datatyp ska undersökas tillkommer momentet att skaffa en referens till ett slags Typeobjekt för varje medlem. Objektets klass heter MemberInfo. Den är inte en härledd klass utan en annan klass med liknande uppsättning metoder. Den är dock basklass för ett antal särskilda klasser för metainformation för olika slags medlemmar: FieldInfo, ConstructorInfo, MethodInfo, EventInfo, PropertyInfo och ParameterInfo. Type-objektet för en viss datatyp har en metod GetMembers, som returnerar en vektor av MemberInfo-objekt. Vektorn från GetMembers innehåller ett MemberInfo per public medlem. Type t = typeof(SomeClass); MemberInfo[] members = t.GetMembers(); foreach (MemberInfo member in members) { Attribute[] attributes = Attribute.GetCustomAttributes(member); foreach (Attribute attribute in attributes) 200
Kopiering av kurslitteratur förbjuden. © Studentlitteratur
8 Attribut
{ if (attribute is MyAttribute) ... if (... } } Här traverseras alla medlemmar, och alla attribut för var och en. Ytterligare överlagrade varianter av GetCustomAttributes returnerar attributen för ett assembly eller en modul. Man behöver då som parameter ett objekt av klassen Assembly eller klassen Module. Som om inte det var nog finns ett antal överlagrade GetCustomAttributes för en viss Attribute. Man kan alltså snabbt kontrollera om ett assembly, en modul, en typ eller en medlem har ett visst attribut också: Attribute.GetCustomAttributes( members[0], typeof(MyAttribute)); Vektorn members från förra exemplet innehåller objekt av klassen MemberInfo. Returvärdet är ett objekt av MyAttribute, eller null om inget fanns.
8.2 Egna attribut Klassbiblioteket bjöd på att antal attributklasser, och ett par av dem ingick också i språket genom att kompilatorn agerar utifrån deras existens eller tillstånd. Men ingenting hindrar att man utvecklar egna attributtyper och använder dem på liknande sätt. Första regeln är då att klassen ska härledas direkt eller indirekt från Attribute. Det går alltså också bra att göra egna klasser härledda från klassbibliotekets härledda. Vi skulle exempelvis kunna göra en attributklass som kopplar information om utvecklaren, eller extra dokumentation, till en klass. Följande ger ett attribut som kopplar en URL till en klass:
Kopiering av kurslitteratur förbjuden. © Studentlitteratur
201
8 Attribut
public class HelpAttribute : Attribute { string url, topic; public HelpAttribute(string url) { this.url = url; } public string Topic { get { return topic; } set { topic = value; } } public string Url { get { return url; } } } För att illustrera både vanliga parametrar (kallas också positionsparametrar) och namngivna parametrar har klassen också en get/setegenskap Topic, som motsvarar ett fält topic. Attributet kan användas exempelvis så här: [Help(”http://www.company.com/help/class1.html”)] public class Class1 { ... } // eller: [Help(”http://....”, Topic=”Class1”)] public class Class1 { ... } Alla program som använder Class1 kommer därmed att i sitt manifest ha en länk till hjälpfilen. Notera detaljerna hur attributet synes heta Help, fast klassen heter HelpAttribute, och hur Topic initieras med en tilldelning. Här kan också anknytas till avsnitt 1.3, som handlade om metainformationen, närmare bestämt den del som behandlade digitala signaturer. För att åstadkomma säker identifiering och verifiering av ett assembly som laddas dynamiskt (dll-fil) kan man förse manifestet med public key och ett hashvärde. När vi nu vet att sättet att pla202
Kopiering av kurslitteratur förbjuden. © Studentlitteratur
8 Attribut
cera information i manifest är att använda attribut, undrar man naturligtvis om attributklasser för just det ändamålet finns i klassbiblioteket – och svaret är tyvärr nej. Eftersom inte alla implementationsdetaljer är standardiserade finns inga klasser för det ändamålet. Men i kommersiella tillämpningar såsom .NET finns det. I .NET heter attributklasserna för digitala signaturer AssemblyKeyFileAttribute och AssemblyKeyNameAttribute. För att sätta versionsnummer finns AssemblyVersionAttribute. Dessa förutsätter stöd i kompilatorn och är enbart att se som exempel och alltså inte standard.
8.3 Attribut till attribut Attribut är klasser. Klasser kan förses med attribut! Kanske känns det endast tröttsamt att komplicera saken med attribut på detta sätt, men vid närmare eftertanke finner man att om man utvecklar egna attributklasser för generellt bruk så behöver man nästan alltid förse dem med en viss begränsning – deras räckvidd. Med räckvidd menas vilken slags kodelement de är rimliga att användas för. Help-attributet i förra avsnittet var ju avsett för klasser, men inget hindrar användare att markera även andra kodelement med ett Help. I detta fall kanske inte fatalt, men ändå olämpligt. För ändamålet finns en standardklass AttributeUsageAttribute. Den är – förutom Conditional och Obsolete – en sådan som även ingår i språksatndarden så att kompilatorn använder den och ger felmeddelanden utifrån dess tillstånd. Ett AttributeUsage används för att markera räckvidd för en attributklass så här: [AttributeUsage(AttributeTagrets.Class)] public class HelpAttribute : Attribute { ... }
Kopiering av kurslitteratur förbjuden. © Studentlitteratur
203
8 Attribut
Med denna lilla modifiering blir attributklassen HelpAttribute endast användbar för klasser och annan placering ger kompileringsfel. Parametern AttributeTargets.Class är en enumtyp från klassbiblioteket. Dess värden är förutom Class: Assembly, Struct, Enum, Constructor, Method, Property, Field, Event, Interface, Parameter, Delegate, ReturnValue. Värdena är heltal med enstaka bit satt, så att de kan or-as samman till kombinationer. För vissa kombinationer finns också färdiga värden, exempelvis All som motsvarar alla. AttributeUsageAttribute har också ett par ytterligare egenskaper som kan initieras. AllowMultiple har standardvärdet false och det gör det omöjligt att ha flera attribut av denna klass för ett och samma kodelement. Om man vill göra det möjligt sätter man AllowMultiple till true. Den andra egenskapen heter Inherited och har standardvärdet true. Den betyder att attributet ärvs. Vissa kompikationer uppstår om både AllowMultiple och Inherited är true. Vi överlämnar dessa detaljer åt den särskilt intresserade läsaren. Sammanfattningsvis kan man kanske fundera över om inte användande av attribut för vissa enkla ändamål kan kännas onödigt komplext. Det är uppenbarligen frestande i många fall att förse en klass med extra information genom att göra ett statiskt och publikt fält. Resonemanget kan återföras på resonemangen bakom undantagshantering. Ad-hoc-lösningar i form av returvärden i stället för undantag känns enklare i det ögonblick koden skrivs, men ger med tiden i stora kodmassor ett underhålls- och dokumentationsproblem. Metainformation som static-fält blir på motsvarande sätt inkonsekvent och en källa till huvudbry för användare av typen. Konsekventa designidéer ger i längden oftast effektivare programutveckling även när de för ögonblicket kan kännas lite onödigt komplexa. Man får lyssna till sitt samvete...
204
Kopiering av kurslitteratur förbjuden. © Studentlitteratur
9 Pekare och osäker kod
9 Pekare och osäker kod
Pekare är en mycket vanlig datatyp i C/C++. En pekarvariabel är en variabel som innehåller en minnesadress. I ett 32-bitars system exempelvis, är den en 32-bitars heltalstyp. Med pekare kan man bland annat på ett enkelt och mycket effektivt sätt traversera vektorer genom att i en iteration öka/minska pekarvariabelns värde med ++ eller --. De operatorerna räknar upp/ner värdet så många steg som den utpekade variabeln täcker antal bytes i minne. Ett ++ får alltså pekaren att peka på nästa variabel i en vektor. När pekare används på det sättet finns ingen som helst kontroll av att det utpekade minnet är relevant. Att traversera utanför en vektor är fullt möjligt, och beroende på vad som råkar finnas just där i minne kan antingen någon annan variabel, eller kanske exekverbar kod då skrivas över. Resultatet är naturligtvis oftast förödande. I system med virtuell minneshanterare kommer en pekare att innehålla en virtuell adress som omdirigeras till en fysisk av minneshanteraren. En adress som inte alls är allokerad men som ändå refereras ger då odefinierat resultat – oftast ett avslut av processen. Ett annat område för pekare är hantering av portar som är ”memory mapped”. Vissa adresser utgör då egentligen den hårdvara som förbinder systemet med omvärlden – det kan vara alla möjliga slags portar och gränssnitt för övervakning, styrning, mätning och kommunikation. Man sätter ett känt värde i en pekare och påverkar sedan direkt de bitar och bytes som finns på den adressen med hjälp av bitoperatorerna. Därmed läser/skriver hårdvaran enligt sin logik. Så styrs exempelvis traditionella serieportar i en persondator.
Kopiering av kurslitteratur förbjuden. © Studentlitteratur
205
9 Pekare och osäker kod
Buggbenägenheten hos pekare och de programkonstruktioner man ofta gör med pekare har gjort att pekare i C# ersatts av referenser, och att skräpsamlingen gör att pekare aldrig används för att radera minne. Men pekare finns faktiskt i språksyntaxen! När ett CLI-program exekverar hanteras normalt allt minne via VES minneshanterare, och vanligtvis är vi helt nöjda med det. Men för att göra CLI och C# användbart också för inbyggda system finns i C# nyckelordet unsafe. Osäker kod kan accessa hårdvara med hjälp av pekare. Pekare kan också användas för API-anrop, I kod markerad unsafe gäller pekarsyntax mycket lik C/C++. unsafe { int x = 5; int* p = &x; *p = 10; Console.WriteLine(*p); //skriver 10 } Exemplet visar hur en pekarvariabel skapas och de två operatorer som är de viktiga i pekarsammanhang. En lätt förvirrande sak är att tecknet * används för två ändamål. Asterisken efter ett typnamn betyder att typen är pekare-till-typen. Första satsen skapar en vanlig int. Andra satsen skapar en int-pekare – en variabel som kan innehålla adressen till en int. Den initieras med adressen till int-variabeln x. Tredje satsen tilldelar variabeln på adressen p värdet 10 genom att sätta en asterisk före p. Operatorn & kallas adressoperatorn och ger adressen till operanden. Operatorn * kallas avreferensoperatorn och ger det värde som finns på den adress som utgörs av operanden. Det omvända, alltså. Ytterligare två operatorer är speciella för pekare. Om pekaren håller adressen till en struct med medlemmar som vi vill nå, skulle ju uttrycket (*p).mem fungera, och det gör det (när p är en pekare till en struct som har en medlem som heter mem), fast med lite knölig syntax. Ett enklare skrivsätt är då piloperatorn. Exakt samma sak kan nämligen fås med p->mem.
206
Kopiering av kurslitteratur förbjuden. © Studentlitteratur
9 Pekare och osäker kod
Den andra specialaren är överraskande nog indexoperatorn. Uttrycket p[3] är nämligen exakt detsamma som *(p+3)! Detta märkliga skrivsätt kommer som en historisk rest från C/C++, där vektornamn egentligen är pekare, och pekare kan användas som om de var vektornamn. Att indexera i en vektor är i de språken bara att ge en offset på en pekare, vilket kan göras på valfritt sätt – index eller aritmetik på pekare. Logiken är kvar i unsafe C#, men – observera noga – inte för vektorer som är Array-objekt, bara för vektorer som skapats i osäker kod. Därmed kommer vi över på frågan vad pekare kan sättas att peka på, och hur variabler kan skapas i osäker kod. Först och intressantast är att kunna sätta pekare till absolutadresser så att hårdvara kan accessas (om inte operativet stoppar det). För det ändamålet är explicit typomvandling från heltalstyperna tillåtet. uint* p = (uint*) 0x3f8; // COM1 i en standard-PC Här fick vi en pekare med vars hjälp vi kan styra en ports hårdvara direkt. Med bitoperatorerna kan *p användas för att sätta och läsa enstaka bitar i portens register. (unsafe-blocket är underförstått i detta och följande exempel.) En annan möjlighet är att ta adressen till en stackvariabel, och då talar vi uppenbart structer (klassobjekt skapas ju alltid på heap). Reglerna är hårda – den struct vi tar adressen till får inte i sin tur innehålla några referenser, varken direkt eller indirekt. Det var detta vi gjorde i exemplet nyss, vi tog adressen till en lokal int. Ytterligare en möjlighet är att ta adressen till en struct som är en del av ett klassobjekt. Nu blir det lite krångligt. Ett klassobjekt har ju skapats på heap och hanteras av VES minneshanterare. Det kan flyttas och det kan skräpsamlas. Därför krävs att objektet låses och för det finns den särskilda syntaxen: class MyClass { public MyStruct s; ... } MyClass obj = new MyClass(); fixed (MyStruct* p = &(obj.s)) { // endast i detta block kan p användas }
Kopiering av kurslitteratur förbjuden. © Studentlitteratur
207
9 Pekare och osäker kod
Nyckelordet fixed gör att när blocket exekverar lämnar skräpsamlaren objektet som obj refererar till ifred. Efter blocket existerar inte pekaren och därmed inte heller problemet. Behovet av att ta pekare till delar av klassobjekt är väl begränsat, men möjligheten finns alltså. Det går förresten i kraft av detta att ta adressen till första tecknet i en string (en char*), och även till elementen i en Array – om de är av structtyp. För skapande av variabler i osäker kod finns en särskild operator som heter stackalloc. För den som har bakgrund i C/C++ ska påpekas att new i unsafe kod betyder detsamma som vanligt – VES minneshanterare sköter saken. Vi kan alltså inte allokera heapminne och tilldela en pekare i C# överhuvudtaget – och delete existerar inte alls. Att definiera en struct, såsom en int, i exekverande kod betyder ju alltid stackallokering, så varför stackalloc? Jo, med stackalloc kan vektorer såsom i C/C++ skapas, men bara på stack. Vektorer är ju annars objekt av klassen Array (på heap), och det är inget fel på dem, men en stackallokerad vektor kan bli mycket snabb att skapa och traversera med pekarnotation. En varning dock: stacken är begränsad i de flesta system. Ett ”stack overflow” kraschar programmet... double *p, *arr = stackalloc double[100]; for (p = arr; p < arr + 100; p++) *p = 0; Den vektor som skapas här är precis som en gammal hederlig C-vektor. Den består bara av ett sjok minne på stacken, ingen initiering och ingen gränskontroll – men mycket snabb. Studera några detaljer: Vi skapar plats för 100 double-variabler. Datatypen double är 8 byte stor, så vektorn är 800 byte. Pekaren arr innehåller adressen till den första byten i den första double:n. När pekaren räknas upp med ++ i for-satsen räknas den upp med 8 så att den pekar på nästa double. Så sker tills den blir arr + 100, vilket är adressen till första byte efter sista elementet, och egentligen arr + 800. Om vi skulle traversera längre är resultatet odefinierat. Varje element sätts till 0 genom att avreferera pekaren. Notera också att man traverserar med
208
Kopiering av kurslitteratur förbjuden. © Studentlitteratur
9 Pekare och osäker kod
hjälp av en kopia av det ursprungliga pekarvärdet, annars hittar man kanske aldrig tillbaka! Några ytterligare detaljer. En pekare är ett heltal – så varför har en pekare typ? En pekare är en pekare är en pekare. Svaret är att kompilatorn måste veta storleken på de variabler pekaren är avsedd att peka på, annars kulle inte pekararitmetiken fungera. Operatorn ++ exempelvis räknar ju upp pekaren till nästa elements adress. För generella pekare finns typen void*. En sådan kan inte avrefereras men kan typomvandlas till vilken pekartyp som helst, den förekommer därför ofta i C-funktioner i operativsystems API. Det finns ytterligare en operator speciellt för osäker kod. Den heter sizeof och tar ett typnamn som operand och returnerar den typens storlek räknat i antal bytes. Ett objekt består av klassens fält, men det kan tänkas att kompilatorn väljer att fylla ut med tomma bytes för att få jämnt delbara adresser, så enda säkra sättet att veta ett objekts storlek är med sizeof. Slutligen ska påpekas att alla exempel i detta kapitel underförstått finns i unsafe kod, och i inledningen såg vi hur ett block av unsafe kod kunde deklareras. Det går också att göra en hel klass unsafe med ordet unsafe före klassnamnet, och det går också att markera en metod eller egenskap med ordet unsafe före namnet. Osäker kod kräver också ofta en särskild flagga till kompilatorkommandot.
Kopiering av kurslitteratur förbjuden. © Studentlitteratur
209
10 Klassbiblioteket
10 Klassbiblioteket
I kapitel 1 introducerades klassbiblioteket mycket översiktligt, och genom hela boken har sedan datatyper ur biblioteket använts i respektive sammanhang. I detta kapitel ska alla datatyper i biblioteket listas, och i några fall ska en mera utförlig beskrivning av typiska sätt att använda dem också ges. Allt som allt finns 294 datatyper, så alla kan inte ges en fullödig behandling. Biblioteket behandlas utifrån uppdelningen i namnrymder, ett avsnitt per namnrymd. I varje avsnitt finns en tabell med klasser, en med structer och en med interface. Klasserna i sin tur är listade så att exceptionklasser står för sig och attributklasser står för sig. I övrigt gäller inte någon särskild ordning inom varje kategori. I tabellerna med structer är de listade så att enumtyper står för sig. Interface står utan särskild ordning. Indelningen i namnrymder är en av flera indelningsgrunder i biblioteket. Varje datatyp hör också hemma i ett underbibliotek. Det största underbiblioteket heter Base Class Library (BCL) och utgör den absoluta kärnan av datatyper. I en CLI-implementation för en viss plattform är det tillåtet att distribuera ett urval underbibliotek, där ett minimum är BCL och Runtime Infrastructure. Underbiblioteken XML, Networking och Reflection innehåller datatyper för respektive tillämpningsområden och kan utgå helt om de inte behövs. Runtime Infrastructure innehåller datatyper som mest är intressanta för den som ska utveckla en CLI-implementation, alltså skriva klassladdare med mera. Som applikationsutvecklare kommer man inte mycket i kontakt med dem. Men eftersom deras klientkod utgör VES måste de naturligtvis ingå även i en liten implementation. Man kan säga att BCL är minimiuppsättningen datatyper för en applikation, och Runtime Infrastructure är minimiuppsättKopiering av kurslitteratur förbjuden. © Studentlitteratur
211
10 Klassbiblioteket
ningen datatyper för en exekveringsmiljö. Därför utgör de tillsammans profilen Kernel. Underbiblioteket Extended numerics är ett specialfall. Där finns de tre flyttalstyperna Single, Double och Decimal. Det är alltså möjligt att distribuera en CLI utan stöd för flyttal! Dessutom går detta underbibliotek över klassgränser. Alla metoder i ett flertal andra datatyper som har någon flyttalstyp i sitt gränssnitt ingår också i Extended numerics. Ett underbibliotek kan alltså bestå av vissa medlemmar i datatyper vars övriga medlemmar ingår i ett annat underbibliotek. Detsamma gäller underbiblioteket Extended Array, som enbart innehåller medlemmar för klassen Array. Det gäller stödet för flerdimensionella vektorer – utan detta underbibliotek kan Array alltså endast skapa endimensionella vektorer. Compactprofilen XML
Networking
Reflection
Kernelprofilen
Runtime Infrastructure
Extended Array Base Class Library Extended Numerics
Figur 10.1 Sambandet mellan underbiblioteken och profilerna. Underbiblioteken innehåller datatyper, med undantag av Extended Array och Extended Numerics. Extended Array innehåller medlemmar för Array, och Extended numerics innehåller både datatyper och enstaka medlemmar för andra datatyper.
212
Kopiering av kurslitteratur förbjuden. © Studentlitteratur
10 Klassbiblioteket
För varje datatyp i de följande tabellerna har angetts ett underbibliotek, men observera att det kan vara så att typens medlemmar egentligen kan tillhöra olika underbibliotek. Det angivna underbiblioteket är då det som innehåller minimiuppsättningen. Utvecklare av CLI-implementationer uppmanas utöka klassbiblioteket i alla dess indelningar – lägga till namnrymder, lägga till datatyper, och lägga till medlemmar i befintliga typer, samt lägga till underbibliotek och profiler. En rekommendation när det gäller att lägga till datatyper för helt andra ändamål än de som finns i standardbiblioteket (fönsterhantering, databashantering, etc.) är att skapa en ny namnrymdshierarki som har en annan rot än System.
10.1 System System är roten bland namnrymder i klassbiblioteket. Här har man samlat de 100 mest använda datatyperna från alla olika tillämpningsområden. Det är alltså en ganska stor del av hela klassbiblioteket, och därmed kommar man ganska långt i klientkod genom att enbart lägga till using System. Bland klasserna känner vi igen de flesta av de som ingår i BCL. Här finns Object, String, Array, Console och många andra gamla vänner. Random är en användbar sak för slumptalsgenerering, Convert har en stor mängd metoder för typomvandling, Math består av enbart statiska matematikmetoder. Klass
Underbibliotek
Anmärkning
Object
BCL
object
String
BCL
string
CharEnumerator
BCL
Enumerator för String
ValueType
BCL
Basklass för structer
Array
BCL
T[] ger Array-referens
Enum
BCL
Basklass för enum-typer
Bibliotekets basklass
Kopiering av kurslitteratur förbjuden. © Studentlitteratur
213
10 Klassbiblioteket
Underbibliotek
Anmärkning
Math
ExtendedNumerics
Statiska metoder för beräkningar
Random
BCL
Metoder för slumptalsgenerering
Type
BCL
Metaklassen
GC
BCL
Statiska metoder för skräpsamlingen
Console
BCL
Statiska metoder för terminalapplikation
Convert
BCL
Statiska metoder för typkonverteringar
Delegate
BCL
Basklass för delegerare som fås med dele-
Klass
gate EventHandler
BCL
Delegerare härledd från Delegate
EventArgs
BCL
Parametertypen i EventHandler
AssemblyLoadEventHandler
RuntimeInfrastructure
Delegerare härledd från Delegate
AssemblyLoadEventArgs
RuntimeInfrastructure
Parametertypen i AssemblyLoadEventHandler
UnhandledExceptionEventHandler
RuntimeInfrastructure
Delegerare härledd från Delegate
UnhandledExceptionEventArgs
RuntimeInfrastructure
Parametertypen i UnhandledExceptionEventHandler
AsyncCallback
BCL
Delegerare, beskrivs i avsnitt 10.5.2
Environment
BCL
Information om miljövariabler med mera
AppDomain
RuntimeInfrastructure
Notifierare och metoder för instansen
Version
BCL
Versionsnummer
214
Kopiering av kurslitteratur förbjuden. © Studentlitteratur
10 Klassbiblioteket
Klass
Underbibliotek
MarshalByRefObject
BCL
Basklass för objekt som hanteras via proxy
Uri
Networking
Immutable URI
UriBuilder
Networking
Mutable URI
Exception
BCL
Indirekt basklass för alla exception-klasser
ApplicationException
BCL
Basklass för egna exception-klasser
Anmärkning
Exception-klasser:
ArgumentException
BCL
ArgumentNullException
BCL
ArgumentOutOfRangeException
BCL
ArithmeticException
BCL
ArrayTypeMismatchException
BCL
BadImageFormatException
RuntimeInfrastructure
CannotUnloadAppDomainException
RuntimeInfrastructure
DivideByZeroException
BCL
DuplicateWitObjectException
BCL
EntryPointNotFoundException
RuntimeInfrastructure
ExecutionEngineException
BCL
FieldAccessException
RuntimeInfrastructure
FormatException
BCL
IndexOutOfRangeException
BCL
InvalidCastException
BCL
InvalidOperationException
BCL
InvalidProgramException
BCL
Kopiering av kurslitteratur förbjuden. © Studentlitteratur
215
10 Klassbiblioteket
Underbibliotek
Klass MemberAccessException
RuntimeInfrastructure
MethodAccessException
RuntimeInfrastructure
MissingFieldException
RuntimeInfrastructure
MissingMemberException
RuntimeInfrastructure
MissingMethodException
RuntimeInfrastructure
NotFiniteNumberException
ExtendedNumerics
NotSupportedException
BCL
NullReferenceException
BCL
ObjectDisposedException
BCL
OutOfMemoryException
BCL
OverflowException
BCL
RankException
BCL
StackOverflowException
BCL
SystemException
BCL
TypeInitializationException
BCL
TypeLoadException
RuntimeInfrastructure
TypeUnloadedException
RuntimeInfrastructure
UnauthorizedAccessException
BCL
UriFormatexception
Networking
Anmärkning
Basklass för de flesta exception-klasserna
Attribut-klasser: Attribute
216
BCL
Basklass för alla attribut
Kopiering av kurslitteratur förbjuden. © Studentlitteratur
10 Klassbiblioteket
Klass
Underbibliotek
AttributeUsageAttribute
BCL
CLSCompliantAttribute
BCL
FlagsAttribute
BCL
ObsoleteAttribute
BCL
ParamArrayAttribute
RuntimeInfrastructure
Anmärkning Attribut för attribut-klasser
Uppsättningen exception-klasser är som synes betydande. Av det bör vi dra slutsatsen att undantagshantering är väl förberett och att vi bör anamma den principen. Attributklasserna och deras användande beskrevs i kapitel 8. Struct
Underbibliotek
Boolean
BCL
Char
BCL
Anmärkning
bool true eller false char 16-bit Unicode
Byte
BCL
SByte
BCL
byte 8-bit ej teckensatt heltal
sbyte 8-bit teckensatt heltal Ej CLS
Int16
BCL
short 16-bit teckensatt heltal
UInt16
BCL
ushort 16-bit ej teckensatt heltal Ej CLS
Int32
BCL
int 32-bit teckensatt heltal
UInt32
BCL
uint 32-bit ej teckensatt heltal Ej CLS
Int64
BCL
long 64-bit teckensatt heltal
Kopiering av kurslitteratur förbjuden. © Studentlitteratur
217
10 Klassbiblioteket
Struct
Underbibliotek
Anmärkning
UInt64
BCL
ulong 64-bit ej teckensatt heltal Ej CLS
ExtendedNumerics
float
ExtendedNumerics
double
Decimal
ExtendedNumerics
decimal
Void
Reflection
För void* i osäker kod
IntPtr
RuntimeInfrastructure
För pekare eller handle i osäker kod
UIntPtr
RuntimeInfrastructure
För pekare eller handle i osäker kod Ej CLS
DateTime
BCL
Tidpunkt
TimeSpan
BCL
Tidsintervall
RuntimeFieldHandle
RuntimeInfrastructure
Paketerar en IntPtr
RuntimeMethodHandle
RuntimeInfrastructure
Paketerar en IntPtr
RuntimeTypeHandle
RuntimeInfrastructure
Paketerar en IntPtr
AttributeTargets
BCL
Värden för AttributeUsageAttribute
UriHostNameTypes
Networking
Värden för Uri-klassen
UriPartial
Networking
Värden för Uri-klassen
Single Double
32-bit flyttal 64-bit flyttal 96-bit flyttal
Enum-typer:
Structerna i System är också i stor utsträckning välkända. Vi ser alla de enkla datatyperna och vi ser också icke-CLS-typerna såsom de icke teckensatta heltalen. Observera att flyttalstyperna inte ingår i 218
Kopiering av kurslitteratur förbjuden. © Studentlitteratur
10 Klassbiblioteket
profilen Kernel, utan kräver det extra underbiblioteket Extended Numerics. DateTime och TimeSpan är användbara structer för hantering av tidpunkter och tidsintervall. De har upplösningen 100 nanosekunder, vilket är betydligt bättre än de flesta plattformar, så precisionen är god. Internt representeras tidpunkten av ett 64-bitars heltal, vilket gör att intervallet räcker till år 9999! DateTime har metoder för omräkning av tidpunktens heltal till datum på allehanda sätt. Den kompenserar för skottår, tidzoner och sommartid. Information om aktuell tidzon hämtas från operativet. Interface
Underbibliotek
IAsyncResult
BCL
Anmärkning För asynkron programmering. Beskrivs i avsnitt 10.5.2
ICloneable
BCL
För objektkopiering
IComparable
BCL
För sortering
IDisposable
BCL
Metoden Dispose
IFormatProvider
BCL
Se 10.8
IFormattable
BCL
Se 10.8
Bland interfacen känns ICloneable, IComparable och IDisposable igen. De beskrevs i avsnitt 4.6, och IDisposable behandlades i avsnitt 3.8 om destruktorer och metoden Dispose. Interfacet IAsyncResult används i så kallad asynkron programmering, vilket har med trådar att göra. Det, och delegerartypen AsyncCallback beskrivs därför i avsnitt 10.5.2, bland övrig trådproblematik.
10.2 System.Collections Samlingsklasser är verkliga trotjänare i datavetenskap. Att studera teori bakom samlingar – sortering, tidskomplexitet, grafteori m.m. – kan man ägna hela livet åt. Klassbibliotek i allmänhet brukar inneKopiering av kurslitteratur förbjuden. © Studentlitteratur
219
10 Klassbiblioteket
hålla åtskilliga samlingsklasser, men här ser vi endast två. Men de två som finns är å andra sidan mycket användbara. Klass
Underbibliotek
Anmärkning
ArrayList
BCL
Dynamisk vektor
Hashtable
BCL
Hash-tabell
Comparer
BCL
Implementerar IComparer
Det är brukligt i standardiserade samlingsklasser att standarden endast beskriver interface och slår fast tidskomplexitet. Implementationen är sedan en sak för varje leverantör. Här är det tvärtom så att implementationen är fastställd och någon garanterad tidskomplexitet nämns inte. ArrayList ska vara en dynamisk vektor, och Hashtable ska vara en hashtabell. Namnet ArrayList har detta klassbibliotek gemensamt med Javabiblioteket, och namnet är lite av en ordlek – som äpplepäron ungefär. De två traditionella och konkurrerande sätten att implementera en endimensionell samling är vektor (array) och länkad lista (list). Vektorn består av ett antal element lagrade i sammanhängande minne, och listan består av noder (objekt) som hänger samman med referenser åt ena eller båda hållen, i en kedja. Vektorns styrka är snabbhet i att lägga in och hitta element via index – så länge vektorn inte är full. När den är full måste en helt ny vektor allokeras, med fler element, och hela den gamla kopieras över till den nya. Genom att göra det med exempelvis dubbla storleken får man ändå en tidskomplexitet som brukar kallas ”amorterad konstant”. Det betyder att utslaget på ett stort antal fall av utökning så blir tidsåtgången i medeltal ändå nära den verkligt konstanta. En länkad lista har styrkan att alltid (så länge det finns minne) kunna utökas med nya element i konstant tid. Ett nytt objekt skapas i minne och dess referenser sätts att peka in i en befintlig lista. Eventuellt sätts några befintliga elements referenser också om. Men traversering av en länkad lista är långsammare. Elementen är inte indexerade så att ett visst element kan hittas i konstant tid. Listan 220
Kopiering av kurslitteratur förbjuden. © Studentlitteratur
10 Klassbiblioteket
måste genomsökas och tidsåtgången blir linjärt beroende av listans längd. Man kallar den tidskomplexiteten linjär. ArrayList är en dynamisk vektor. Vi behöver inte bry oss om hur den är implementerad, vi bara använder den och förväntar oss amorterad konstant tidskomplexitet. Hashtable är alltså en hashtabell. Idén beskrevs hastigt i ett tidigare kapitel (på tal om Object-metoden GetHashCode). I korthet består hashtabeller av en vektor av länkade listor. Nya element kan då läggas in med konstant tidsåtgång, precis som i länkade listor. Frågan blir i vilken lista. För det ändamålet måste ett hashvärde räknas fram utifrån objektets innehåll, och det är det GetHashCode ska göra (se avsnitt 4.7). Det heltal som GetHashCode returnerar får utgöra index till vektorn, vilket ju bestämmer vilken länkad lista elementet hamnar i. Därmed blir objektet också sökbart. Beräkna hashvärdet, använd det som index och traversera sedan den listan. I bästa fall är en hashtabell så stor att endast ett element ligger i varje länkad lista (”bucket”). Då blir inläggning och sökning lika snabb som i en vektor, alltså konstant tid oavsett antal element i samlingen. Om vektorn är för liten eller elementen ligger hopklumpade i få länkade listor blir tidskomplexiteten mer och mer åt länkade-list-hållet, alltså linjär. Klassen Hashtable har den mycket värdefulla finessen att vektorn är dynamisk, vi behöver alltså inte bekymra oss om det förstnämnda problemet. Problemet med ojämn fördelning består i en dåligt implementerad GetHashCode. Det är viktigt att den metoden ger ett stort heltal med jämn spridning. Ytterligare en viktig sak att förstå med Hashtable är att det värde (objekt) som används för beräkning av hashvärdet inte behöver vara det lagrade objektet. När ett nytt element läggs till sker det med två parametrar: Hashtable tbl; MyClass obj = new MyClass(...); tbl.Add(”Anders”, obj); Den första parametern kallas Key och den andra kallas Value. Båda dessa objekt lagras i samlingen, men det är Key som avgör var, genom att dess GetHashCode anropas. Logiskt kan man därför se hashtabellen som en vektor, där index inte behöver vara ett heltal Kopiering av kurslitteratur förbjuden. © Studentlitteratur
221
10 Klassbiblioteket
utan får vara ett värde av valfri typ. Därför kallas denna typ av samling också associativ vektor eller dictionary. Men observera att en dictionary också kan byggas på andra sätt än med en hashtabell (exempelvis ett binärt träd). Key och Value kan alltså vara olika objekt, men ett mycket vanligt upplägg är att Key är ett fält (kanske bakom en egenskap) i Value. Hashtable tbl; MyClass obj = new MyClass(...); obj.ID = ”Anders”; tbl.Add(obj.ID, obj); En fallgrop i det upplägget är att obj.ID i exemplet inte får ändras för det objekt som ligger i hashtabellen – då kan det inte längre sökas. Key bestämmer ju i vilken bucket elementet hamnar, och ändras sedan motsvarande fält i objektet så ger sökning i hashtabellen träff på ett objekt som hade det värdet då det lades in! Om ID ändras till ”Bertil” så kan objektet inte hittas med sökning efter ”Bertil”, det ligger fortfarande med Key ”Anders”. Sökning i hashtabellen sker med indexoperatorn. object found = tbl[”Anders”]; if (found != null && found is MyClass) ... Sökning med konstant tidskomplexitet – samma tid i en samling med fem element som med en miljon! Förutsatt som sagt en bra GetHashCode för Key-typen. Indexoperatorn till vänster om tilldelning kan faktiskt också användas för att lägga in objekt, då med Key som parameter och Value som tilldelat värde. Den tredje klassen i denna namnrymd heter Comparer och är en implementation av interfacet IComparer med metoden Compare. Den används av samlingsklasserna för sortering av elementen, och gör det genom att anropa elementens IComparable.CompareTo, vilken vi behandlat i avsnitt 4.6 om interface. I utökade klassbibliotek kan i denna namnrymd finnas betydligt fler samlingar som ofta är implementerade med hjälp av Hashtable eller ArrayList. Att implementera en klass med hjälp av en mera generell och ofta betydligt mera omfattande klass är ett
222
Kopiering av kurslitteratur förbjuden. © Studentlitteratur
10 Klassbiblioteket
designmönster som kallas ”adapter” (enligt Gamma m.fl: Design patterns). Det består i all sin enkelhet (när vi talar samlingsklasser) i att klassen har en medlem som är ett mera generellt samlingsobjekt, och att klassens metoder ger ett lite annorlunda och kanske förenklat interface mot det ägda objektet. Så kan exempelvis en Queue-klass eller en Stack-klass lätt implementeras med en ArrayList-medlem. Metoderna i en Stack-klass kan heta Push och Pop, i en Queue-klass kanske Enqueue och Dequeue, fast de bara anropar Add- och Remove-metoder för det ägda objektet. Struct
Underbibliotek
DictionaryEntry
BCL
Anmärkning Två Object för bl.a. Hashtable
Den enda structen i denna namnrymd är DictionaryEntry. Den är en obetydlig sak, men användbar för att skapa objektpar att lägga in i en Hashtable. Interface
Underbibliotek
Anmärkning
ICollection
BCL
För alla samlingar
IList
BCL
För alla endimensionella samlingar
IDictionary
BCL
För samlingar som lagrar key-value-par
IEnumerable
BCL
Metoden GetEnumerator returnerar IEnumerator
IEnumerator
BCL
Metoder/egenskaper för traversering av samlingar
IDictionaryEnumerator
BCL
Metoder/egenskaper för samlingar med DictionaryEntry
IComparer
BCL
Metoden Compare
IHashCodeProvider
BCL
Alternativ GetHashCode
Interfacen här är i några fall intressanta. Några har tidigare behandlats i avsnittet om interface (avsnitt 4.6). IEnumerable och IEnuKopiering av kurslitteratur förbjuden. © Studentlitteratur
223
10 Klassbiblioteket
merator implementeras i alla samlingar som kan traverseras med foreach, och de bör därför implementeras också i egna samlingsklasser. IList är också bra att implementera i egna samlingar, den ger utseendet på de vanligaste metoderna såsom Add, Remove och indexoperatorn för sökning. I egen klientkod där ArrayList används kan man använda IList-referens för att därmed lätt kunna byta ArrayList-objektet mot annan samling om man vill.
10.2.1 System.Collections.Specialized Denna namnrymd har i standarden endast en klass, NameValueCollection. Den är en associativ vektor av String till String. En egenhet är att nya strängar kan läggas till med en befintlig Keysträng. Value-strängen blir då konkatenerad med kommaseparering. Just den logiken kan vara användbar i vissa sammanhang då headers ska skapas vid kommunikation med textbaserade protokoll. Klassen är därför placerad i biblioteket Networking, och utgör basklass för WebHeaderCollection i namnrymden System.Net. Klass NameValueCollection
Underbibliotek
Anmärkning
Networking
Dictionary av sträng/strängar
Avsikten med denna namnrymd är annars att i utökade klassbibliotek erbjuda fler samlingsklasser där elementens typ är definierad, alltså något annat än Object. Saken är ju den att alla samlingsklasserna i System.Collections har typen Object för elementen, vilket gör att man vid användande måste typomvandla mycket flitigt för att lägga till och ta bort element. Typomvandling är kostsamt eftersom det alltid sker med typkontroll. Detsamma gäller i annu högre grad boxing och unboxing. En samling avsedd för en viss typ blir då effektivare i mått av prestanda. NameValueCollection är ett exempel på en sådan specialiserad klass. Observera att när man utvecklar sådana specialiserade klasser är det ju ingen idé att implementera dem med en generell 224
Kopiering av kurslitteratur förbjuden. © Studentlitteratur
10 Klassbiblioteket
samling – det enda man gör då är att dölja prestandaförlusten genom att göra typomvandlingarna i klassen. Man måste alltså bygga specialiserade samlingar från början. I C++ finns en språkmekanism för att undvika just detta problem, den kallas typparameterisering och gör det möjligt att enkelt kompilera klasser där någon viss typ specificeras som parameter. Ingen typomvandling behövs då vid exekvering. Java hade från början ingen sådan mekanism, men den har tillförts i senare versioner. C# har den alltså inte heller. Vi ser framtiden an.
10.3 System.IO Denna namnrymd innehåller klasser för dataströmmar till och från filer, strängar och minne. I våra vanligaste operativsystem är också andra slags periferienheter möjliga att accessa via filhandtag, och det är fritt fram för en CLI-implementation (och rekommenderat) att använda klasserna för streams (dataströmmar) även för sådana ändamål. Det kan vara portar, pipes och liknande. Man kan tycka att namnet IO är lite missvisande. Med IO brukar menas Input/Output mot periferienheter, och arbetsminne och strängar är ju inte periferienhet. Ett stort IO-område är däremot trafik mot terminal, men det finns i klassen Console, i namnrymden System! I ett standardbibliotek som ska passa vilken målplattform som helst kan man inte förutsätta mycket om plattformen. Brukligt i andra bibliotek är att filsystemet förväntas bestå av något som identifieras med en sträng (filer), och att de är en endimensionell samling av bytes. Dessa bytes kan möjligen läsas och skrivas i sjok så att de motsvarar inbyggda typer. Punkt slut. I detta bibliotek har man förutsatt betydligt mer, men ändå med viss försiktighet. Filer förutsätts kunna vara organiserade i en hierarkisk katalogstruktur, de förutsätts ha attribut för delning, för skapandets tidpunkt, tidpunkt för senaste läsning och tidpunkt för senaste ändring. Alla metoder för dessa attribut ingår i BCL, och för
Kopiering av kurslitteratur förbjuden. © Studentlitteratur
225
10 Klassbiblioteket
plattformar utan motsvarande funktionalitet sägs bara att metoderna då ska vara utan effekt. Klasserna kan logiskt delas i tre kategorier: för hantering av filer och kataloger, för läsning/skrivning av bytevärden, samt för läsning/ skrivning av text. Klasserna Directory och File är av den första kategorin. Stream-klasser är andra kategorin, och Reader/Writerklasser är tredje kategorin. Klass
Underbibliotek
Path
BCL
Statiska metoder för hantering av sökväg i String
Directory
BCL
Statiska metoder
File
BCL
Statiska metoder
Stream
BCL
Basklass
FileStream
BCL
Läs/skriv bytes
MemoryStream
BCL
Läs/skriv bytes
TextReader
BCL
Basklass
Anmärkning
TextWriter
BCL
Basklass
StreamReader
BCL
Läs textfil
StreamWriter
BCL
Skriv textfil
StringReader
BCL
Läs från sträng
StringWriter
BCL
Skriv till sträng
Exception-klasser: DirectoryNotFoundException
BCL
EndOfStreamException
BCL
FileLoadException
BCL
FileNotFoundException
BCL
IOException
BCL
PathTooLongException
BCL
Mest central är klassen File. Den innehåller enbart statiska metoder för att göra saker med en fil i sin helhet, alltså inte läsning/skrivning. Man kan undersöka en fils tidpunkter (skapad, öppnad, ändrad), man kan kopiera, radera eller flytta en fil.
226
Kopiering av kurslitteratur förbjuden. © Studentlitteratur
10 Klassbiblioteket
Klassen Directory är motsvarigheten för kataloger. En hel katalog med filer och underkataloger kan flyttas, raderas etc. Där finns också mycket enkla metoder för traversering av kataloger. Metoden GetFiles ger en string-vektor av filnamn, GetDirectories en motsvarande vektor av underkatalognamn, och GetFileSystemEntries ger både fil- och katalognamn. Det går också att söka med en mask för fil/katalognamn. File-klassen har därtill en uppsättning metoder för att öppna en fil. Samtliga tar ett filnamn och returnerar en Stream eller en Reader/Writer. Olika varianter finns för öppnande av befintlig fil, skapande av ny fil och huruvida filen ska skrivas över, läggas till, öppnas för läsning eller skrivning o.s.v. Det finns flera överlagrade Create-metoder och flera överlagrade Open-metoder. Dessa är ett sätt att öppna fil, ett annat är att skapa Stream- eller Reader/Writer-objekt direkt. De klasserna har konstruktorer som tar filnamn. Möjligheterna är onödigt många. Ett exempel på användande av File och Open-metod: FileStream fs = File.Open( ”readme.txt”, FileMode.Create, FileAccess.ReadWrite, FileShare.None); Denna Open tar parametrar för filnamnet, mode, access och delning. I exemplet skapas filen om den inte finns och skrivs över om den finns, öppnas för både läsning och skrivning, och öppnas exklusivt så att ingen annan tråd kan öppna samma fil samtidigt. De tre sista parametrarna är enum-typer som också finns i namnrymden IO. Metoden returnerar en FileStream. Exakt samma sak kan göras med en FileStream-konstruktor: FileStream fs = new FileStream( ”readme.txt”, FileMode.Create, FileAccess.ReadWrite, FileShare.None);
Kopiering av kurslitteratur förbjuden. © Studentlitteratur
227
10 Klassbiblioteket
Underbibliotek
Anmärkning
FileAccess
BCL
Read/ReadWrite/Write
FileMode
BCL
Append/Create/CreateNew/Open/OpenOrCreate/Truncate
FileShare
BCL
None/Read/ReadWrite/ Write
SeekOrigin
BCL
Begin/Current/End
Struct Enum-typer:
Stream- och Reader/Writer-klasserna har lite diffusa namn som gör det svårt att komma ihåg riktigt vilken som gör vad. Stream gäller läsning och skrivning på lägsta nivå – byte för byte eller vektor av bytes. Stream har också finessen att kunna läsa och skriva asynkront. Asynkron filhantering kan avsevärt förbättra ett programs prestanda, men det har mera med trådar än med filhantering att göra, så hela den idén beskrivs i avsnitt 10.5.2. Reader/Writer gäller läsning respektive skrivning av text i form av char eller char-vektor. De har finessen att direkt kunna konvertera mellan olika teckenuppsättningar. Stream är basklass i en hierarki, TextReader och TextWriter i varsin. Samtliga är abstrakta basklasser.
228
Kopiering av kurslitteratur förbjuden. © Studentlitteratur
10 Klassbiblioteket
Stream
FileStream
MemoryStream TextReader
StreamReader
StringReader TextWriter
StreamWriter
StringWriter
Figur 10.2 Streams-klassernas och Reader/Writer-klassernas arvsstruktur. Kursiva klassnamn betyder abstrakt klass.
FileStream är grundläggande när det gäller datautbyte mot fil. Ett FileStream-objekt motsvarar en öppen fil, och den kan öppnas för läsning eller skrivning eller båda. Det finns en Read-metod och en Write-metod för byte-vektor. Det går att förflytta sig i filen med
Kopiering av kurslitteratur förbjuden. © Studentlitteratur
229
10 Klassbiblioteket
en Seek-metod och läsa/skriva på valfri plats. Filen måste stängas med klassens Close-metod. Ett enkelt exempel: FileStream fs = new FileStream("\\readme.txt", FileMode.Open); byte[] vec = new byte[fs.Length]; fs.Read(vec, 0, (int)fs.Length); fs.Close(); FileStream-objektet skapas med den enklaste konstruktorn. Den sätter FileAccess till ReadWrite och FileShare till Read. Detta objekt kan alltså både läsa och skriva till filen, men om en annan tråd eller process öppnar samma fil samtidigt kan de bara öppna den för läsning. Därefter används en egenskap i FileStream-objektet som ger filens längd, och vi skapar en bytevektor med den storleken. Med ett enda Read-anrop läses hela filen till vektorn, och slutligen stängs filen med Close. Klassen MemoryStream implementerar samma abstrakta basklass Stream, men med logiken att arbeta mot minne i stället för mot fil. Det är ibland en mycket användbar logik. Om man exempelvis ska ta in data via någon port i okänd hastighet och i okänd mängd, för att senare parsa eller på annat sätt bearbeta den, är en vanlig fil kanske inte en bra idé. Trafiken mot en fil kan ju bli långsam och ryckig, i synnerhet om filen accessas via nätverk. Om data då fortsätter komma in svämmar buffertar kanske över. Bättre är då att arbeta mot det snabbare primärminnet. En MemoryStream är egentligen en dynamisk bytevektor, med gränssnitt som en fil. Bara att skriva till den med Write! TextReader och TextWriter har inget släktskap med Stream, men är påtagligt tänkta att implementeras med en Stream-medlem. De har exempelvis konstruktorer som tar en Stream som parameter. Men Readers och Writers kan också skapas direkt från filnamn. De är ganska tunna paketeringar av Stream-objektet. Det som tillförs är möjlighet att läsa och skriva char, char-vektor och rader av 230
Kopiering av kurslitteratur förbjuden. © Studentlitteratur
10 Klassbiblioteket
text, det vill säga hantering av radslutstecken. Dessutom har de inbyggd konvertering mellan några av de vanligaste teckenuppsättningarna. De har rent av automatik för att känna av teckenuppsättningen i en fil. TextReader och TextWriter är abstrakta basklasser. Implementationen för filer heter (mindre väl formulerat) StreamReader/ StreamWriter. Vissa konstruktorer tar som parameter en System.Text.Encoding, en klass som alltså finns i en annan namnrymd. Den har egenskaper som returnerar Encoding-objekt för konverteringen. De heter exempelvis ASCII, Unicode, UTF-8. Standard i StreamWriter är att skapa filer i UTF-8 teckenuppsättning. UTF-8 är en smart kombination av gammal hederlig ASCII och Unicode. Unicode är 16-bit och ASCII är ursprungligen 7-bit med enbart de vanligaste amerikanska tecknen (A..Z, 0..9, etc). Tricket i UTF-8 är att tecken som finns i ASCII blir 8 bitar och nationella tecken blir 16. En fil i UTF-8 innehållande exempelvis källkod eller HTML blir därmed åtminstone läsbar även för system som bara läser ASCII. StringReader och StringWriter är adapterklasser som paketerar en StringBuilder och ger den ett filliknande gränssnitt. Intelligensen är inte överväldigande, men de kan vara ett enkelt sätt att skapa och bygga en omfattande sträng, eller att läsa radvis från sträng. I System.IO finns inga interface.
10.4 System.Net Denna namnrymd innehåller datatyper för applikationsprotokoll. Under System.Net finns en namnrymd med klasser för sockets, vilket ju traditionellt är ett API för nätverksprotokollet IP och de två transportprotokollen TCP och UDP. Namnrymderna är alltså en indelning som i viss mån motsvarar en protokollstack.
Kopiering av kurslitteratur förbjuden. © Studentlitteratur
231
10 Klassbiblioteket
Protokollen på dessa tre nivåer (nätverk, transport, applikation) är emellertid inte vilka som helst. De enda som hanteras av datatyperna i biblioteket är IP, TCP/UDP och HTTP. Men designen är mycket öppen, och utökade klassbibliotek kan förses med datatyper för andra protokoll. Med tanke på TCP/IP-stackens utbredning och penetration i världen är det inte ett dåligt val att implementera just dessa protokoll. I namnrymden System.Net finns alltså främst klasser för HTTPprotokollet. HTTP är text. Till server skickas en fråga som innehåller ”GET” eller ”POST”, samt en hel del fält med information om vad som efterfrågas. Servern svarar med en sträng som innehåller en sifferkod och ett ord, som i bästa fall är ”200 OK”. Ett antal fält ger kompletterande information och det data som efterfrågats. HTTP är som bekant det protokoll som används för att hämta HTML-sidor från en webserver. En egendomlighet med HTTP är att det transporteras med det underliggande protokollet TCP, vilket är ett förbindelseorienterat protokoll. Förbindelsen upprättas genom att klient ber server om en förbindelse, får det och därefter transporteras data med felhantering. HTTP skapar en TCP-förbindelse, men är i sig förbindelselöst – ett rent request-response-protokoll. När svaret levererats kopplas TCP-förbindelsen genast ner och affären är avslutad. Tabellen nedan visar klasserna i System.Net, och en närmare titt visar att där finns klasser som kan kategoriseras i tre nivåer: de som är abstrakta basklasser för applikationsprotokoll, de som är härledda och implementerar dem med HTTP, samt (lite överraskande) hjälpklasser för sockets. HTTP-klasserna har en socket i sin implementation, och därför krävs ibland exempelvis objekt som innehåller IPadress för deras behov. Placeringen av allt socketrelaterat i egen namnrymd blev därmed inte helt genomförbar och indelningen lite oklar. Klass
Underbibliotek
AuthenticationManager
Networking
Authorization
Networking
CredentialCache
Networking
232
Anmärkning
Kopiering av kurslitteratur förbjuden. © Studentlitteratur
10 Klassbiblioteket
Klass
Underbibliotek
Anmärkning
Dns
Networking Statiska metoder för DNS
DnsPermission
Networking
EndPoint
Networking Abstrakt basklass för IPEndPoint
GlobalProxySelection
Networking
HttpWebRequest
Networking Skapar och skickar en HTTP-fråga. Skapar en HttpWebResponse
HttpWebResponse
Networking Svaret från en HTTPfråga
HttpVersion
Networking
IPAddress
Networking En IP-adress
IPEndPoint
Networking IP-adress och port
IPHostEntry
Networking En URL och alla dess associerade IP/port
NetworkCredential
Networking
ServicePoint
Networking
ServicePointManager
Networking
SocketAddress
Networking Serialiserad EndPoint
SocketPermission
Networking
WebClient
Networking Paketerar en WebRequest och kopplar till en Stream
WebHeaderCollection
Networking Ingår i WebRequest/ WebResponse, är en NameValueCollection
WebPermission
Networking
WebProxy
Networking
WebRequest
Networking Abstrakt basklass för HttpWebRequest
WebResponse
Networking Abstrakt basklass för HttpWebResponse
HttpContinueDelegate
Networking
Exception-klasser: ProtocolViolationException
Networking
Kopiering av kurslitteratur förbjuden. © Studentlitteratur
233
10 Klassbiblioteket
Klass
Underbibliotek
WebException
Networking
Anmärkning
Attribut-klasser: DnsPermissionAttribute
Networking
SocketPermissionAttribute
Networking
WebPermissionAttribute
Networking
En central klass är HttpWebRequest. Den är härledd från den abstrakta WebRequest, vilken implementerar de flesta metoderna. En HttpWebRequest används för att skapa och skicka en HTTPfråga, och då genererar den en HttpWebResponse som innehåller svaret. Observera att båda är avsedda för klientsidan, inte serversidan. Ett objekt av HttpWebRequest skapas genom att WebRequest.Create anropas, inte med något konstruktoranrop. Objektet som returneras är avsett för engångsbruk, alltså för en enda fråga/svar-session. Adressen till servern utgörs av parametern till Create. Den ges som en URI, alltså en sträng bestående av protokoll, host, eventuell resurs och eventuell query. Exempelvis http:// www.grafpro.se/bok?titel=csharp. Alla övriga delar av HTTP-frågan genereras av Create, så att det returnerade objektet kan skicka frågan med ett enda metodanrop, vilket heter GetResponse. Om frågan ska modifieras finns den separerad i ett stort antal egenskaper i HttpWebRequest-objektet. GetResponse returnerar en HttpWebResponse innehållande svaret. Även svaret har ett antal fält i sin header, och de parsas till en uppsättning egenskaper i objektet. Det returnerade datat fås genom att anropa GetResponseStream, som returnerar en Stream, och via den läser man som från en fil. HttpWebRequest myRequest = (HttpWebRequest) WebRequest.Create("http://www.microsoft.com"); HttpWebResponse myResponse = (HttpWebResponse) myRequest.GetResponse(); Stream myStream = myResponse.GetResponseStream();
234
Kopiering av kurslitteratur förbjuden. © Studentlitteratur
10 Klassbiblioteket
// läs från stream myResponse.Close(); Exemplet blev ganska enkelt genom att inga av alla de egenskaper som motsvarar fält i frågans och svarets headers användes alls. För de fall man inte behöver modifiera eller ens se några av de fält som utgör HTTP-header varken i fråga eller svar, utan bara hämta en websida så som man som användare gör det i en webläsare finns proceduren extremt förenklad i klassen WebClient. Den kan skicka en HTTP-fråga och omedelbart returnera en stream där vi enkelt läser returnerat data. Alla ovidkommande detaljer är osynliga! WebClient myWebClient = new WebClient(); Stream myStream = myWebClient.OpenRead(”http://www.ecma.ch”); StreamReader sr = new StreamReader(myStream); // läs från sr sr.Close(); Om man å andra sidan manuellt vill göra några av de moment som doldes i dessa klassers metoder kan vi först se på klassen Dns. Den har enbart statiska metoder, för ändamålet att via DNS-tjänsten omvandla en URL till en eller flera IP-adresser och vice versa. IPAddress ipAddress = Dns.Resolve("www.xyz.com").AddressList[0]; IPEndPoint ipEndPoint = new IPEndPoint(ipAddress, 80); Resolve ger en samling IP-adresser i en IPHostEntry. Den har en vektor AddressList. Den första adressen tilldelas en IPAddress, och den används för att skapa en IPEndPoint. Den innehåller både IP-adress och portnummer. Det är vad som behövs för att skicka frågan. De senaste klasserna här har ju påtaglig koppling till sockets, ändå har de alltså placerats i System.Net eftersom de också förekommer i bland annat Dns-klassen. Själva socketklassen däremot finns i System.Net.Sockets.
Kopiering av kurslitteratur förbjuden. © Studentlitteratur
235
10 Klassbiblioteket
Klassen Socket är också en påtaglig men osynlig del i logiken i HTTP-klasserna. Vi ska strax se närmare på den. Först bara en titt på återstående typer i System.Net: Struct
Underbibliotek
Anmärkning
Enum-typer: HttpStatusCode
Networking
NetworkAccess
Networking
TransportType
Networking
WebExceptionStatus
Networking
Structerna tabellen ovan är utan undantag hjälptyper som utgör parametertyper eller returtyper för metoder i namnrymdens klasser. De är alla enumtyper. Interface
Underbibliotek
IAuthenticationModule
Networking
ICredentials
Networking
IWebProxy
Networking
IWebRequestCreate
Networking
Anmärkning
Interfacet IAuthenticationModule anropas från WebRequest, via ett objekt i WebRequest av typen AuthenticationManager. Det hela går ut på att skapa ett objekt av klassen Authorization när en webserver kräver inloggning. Authorization-objektet innehåller hela strängen för inloggning, alltså användarnamn och lösenord. Interfacet ICredentials implementeras av NetworkCredential och CredentialCache. De innehåller användarnamn/lösenord, respektive en samling sådana par och används för att skapa en Authorization. Hela mekanismen är omfattande och konfigurerbar för inloggning med olika autentiseringsmetoder. IWebProxy är interface för klasser som används av WebRequest för en fråga som skickas via proxyserver. Den implementeras i klassen WebProxy.
236
Kopiering av kurslitteratur förbjuden. © Studentlitteratur
10 Klassbiblioteket
IWebRequestCreate är interface för klasser som ska implementera Create, så som i WebRequest. Create är den enda metoden, och den ska returnera en WebRequest. Observera att WebRequest själv inte implementerar detta interface, dess Create är statisk! I nästa avsnitt sänker vi oss en nivå i protokollstacken, till transportnivån.
10.4.1 System.Net.Sockets En Socket paketerar en IP-adress och ett portnummer, och har metoder för att upprätta förbindelse med en annan IP/port enligt TCP samt att skicka och ta emot data. Den kan också skicka och ta emot data enligt UDP, vilket inte kräver någon förbindelse. När ett socketpar väl har upprättat förbindelse är dataöverföring mycket enkelt för klientkod. Data ska alltid paketeras i byte-vektorer och det som skickas med ett anrop till Send är det som läses med ett anrop till Receive. Allt underliggande krångel med paketstorlek, väntan och omsändningar vid fel är osynligt. Arbetsgången på serversidan består i att skapa en Socket där konstruktorns parametrar bland annat anger protokoll. Därefter förses objektet med värdmaskinens IP och en portadress att lyssna på, genom ett anrop till Bind. Lyssnandet aktiveras med anrop till metoden Listen, och därmed läggs förfrågan om förbindelse från klienter i kö. Ett anrop till Accept returnerar en ny socket som är helt klar för kommunikation med en klient. Om inga väntande förfrågningar finns blockeras tråden i anropet till Accept och väcks när en förbindelse upprättats. På klientsidan skapas en Socket på motsvarande sätt, men inget Bind behövs. Man anropar direkt metoden Connect med IP/port i en EndPoint till servern och får i bästa fall omedelbart objektet initierat med allt som behövs för kommunikationen. Send och Receive tar alltså byte-vektor som parameter. Det innebär ett litet problem när man vill överföra text, och det är ju så att de vanligaste applikationsprotokollen är textbaserade. Text i CLI är som bekant alltid Unicode, och Unicode är 16-bit. För konvertering Kopiering av kurslitteratur förbjuden. © Studentlitteratur
237
10 Klassbiblioteket
mellan Unicode och ASCII finns en utmärkt färdig klass. ASCII är det rätta för HTTP, XML med mera. byte[] sendBuffer = Encoding.ASCII.GetBytes(”Hello”); Klassen Encoding har en statisk egenskap ASCII som ger ett objekt med metoden GetBytes. Den tar en string och returnerar en bytevektor i ASCII. string rec = Encoding.ASCII.GetString(myBuf)); Samma slags objekt har metoden GetString som gör det omvända. En mottagen byte-vektor blir till en string i Unicode. Observera att sockets är systemresurser med begränsad tillgång. Klassen Socket är därmed en resursallokerande klass av den typ som kan ställa till problem i och med skräpsamlarens egenhet att inte garantera någon tidpunkt för destruering och anrop till destruktor. Här gäller alltså att vara noga med att stänga sockets när man är klar med dem, de kan inte bara lämnas åt skräpsamlaren. Socket implementerar därför interfacet IDisposable så som tidigare beskrivits i avsnitt 3.8. Var alltså noga med att anropa Dispose innan en Socket överges. Det finns också en metod Close som gör precis samma sak. En god regel är dessutom att först anropa metoden Shutdown som väntar tills allt data sänts och förbindelsen avslutats. Klass
Underbibliotek
Anmärkning
Socket
Networking Kommunikation med TCP/UDP
LingerOption
Networking
MulticastOption
Networking
NetworkStream
Networking Ger Streaminterface åt en Socket
Exception-klasser: SocketException
Networking
Förutom den centrala klassen Socket finns tre klasser i System.Net.Sockets. NetworkStream är härledd från Stream och 238
Kopiering av kurslitteratur förbjuden. © Studentlitteratur
10 Klassbiblioteket
förpackar en Socket så att den kan användas med Stream-interfacet. Läsning sker då med Read och skrivning med Write. LingerOption och MulticastOption används som parametrar i anrop till en metod i Socket som heter SetSocketOptions. Struct
Underbibliotek
Anmärkning
Enum-typer: AddressFamily
Networking
ProtocolType
Networking
SelectMode
Networking
SocketFlags
Networking
SocketOptionLevel
Networking
SocketOptionName
Networking
SocketShutdown
Networking
SocketType
Networking
Structerna i denna namnrymd är enbart enumtyper som utgör parametrar till olika klassmetoder. Slutligen ett någorlunda komplett exempel på serverkod och klientkod för en TCP-förbindelse och överföring av en sträng: // Serversidan: static void Main() { // Skapa en IPEndPoint för servern IPHostEntry ipHostInfo = Dns.Resolve(Dns.GetHostName()); IPAddress ipAddress = ipHostInfo.AddressList[0]; IPEndPoint serverEndPoint = new IPEndPoint(ipAddress, 3456); // Skapa Socket som lyssnar på TCP-förbindelse Socket listener = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp ); listener.Bind(serverEndPoint); listener.Listen(1);
Kopiering av kurslitteratur förbjuden. © Studentlitteratur
239
10 Klassbiblioteket
Console.WriteLine(”Väntar på anrop...”); Socket handler = listener.Accept(); // blockerar // Nu har någon upprättat förbindelse byte[] bytes = new Byte[1024]; handler.Receive(bytes); Console.WriteLine( Encoding.ASCII.GetString(bytes)); // Stäng båda sockets handler.Close(); listener.Close(); } // Klientsida i samma maskin: static void Main() { // Skapa en IPEndPoint för servern IPHostEntry ipHostInfo = Dns.Resolve(Dns.GetHostName()); IPAddress ipAddress = ipHostInfo.AddressList[0]; IPEndPoint serverEndPoint = new IPEndPoint(ipAddress, 3456); // Skapa en Socket för TCP. Socket s = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp ); // Be servern om en förbindelse s.Connect(serverEndPoint); if (!s.Connected) { Console.WriteLine(”Ingen förbindelse!”); return; } // Skicka något till servern byte[] sendBytes = Encoding.ASCII.GetBytes(”Hello!”); s.Send(sendBytes, SendBytes.Length, SocketFlags.None); // Stäng socket 240
Kopiering av kurslitteratur förbjuden. © Studentlitteratur
10 Klassbiblioteket
s.Close(); } Här ska också påpekas att flera metoder i klasserna i detta namespace och i System.Net har namn som börjar med Begin respektive End. De är avsedda för så kallad asynkron programmering och beskrivs därför i avsnittet om trådar. Se avsnitt 10.5.2. System.Net.Sockets har inga interface.
10.5 System.Threading En process är – brukar man säga – ett program som exekverar. En process består av ett spår av exekvering, samt diverse systemresurser såsom minne, filer, processortid, etcetera. Ett spår av exekvering kallas också en tråd. Den primära tråden kan tänkas starta flera sekundära trådar. Varje tråd får då processortid, i en och samma processor eller i varsin. I vilket fall är det stora problemet med trådar att vi inte har en aning om i vilket relativt tempo de exekverar. Varje tråd lever sitt eget liv. Om mer än en tråd någonstans under sin exekvering ska accessa ett och samma objekt måste det ske på ett ordnat sätt. Bara de enklaste operationerna består av en enda instruktion, och byte av tråd i en processor (kontextbyte) kan ske var som helst mellan två instruktioner. Ändring av ett objekts tillstånd kan alltså avbrytas för att därmed lämna objektet i ett ogiltigt tillstånd. Det kan ske där en och samma metod exekverar i två trådar samtidigt (reentrant code), eller där två olika metoder accessar samma variabel. I CLI används inte begreppet process. Man har inte velat låsa VESimplementationer till att exekvera varje instans i egen process, utan har i stället ett ad-hoc-begrepp: AppDomain. En AppDomain är den omgivning för exekvering som innebär att gränsöverskridande till annan AppDomain kräver marshaling. Alltså – en minnesadress i en AppDomain gäller inte i en annan. I allt väsentligt kan vi tänka en
Kopiering av kurslitteratur förbjuden. © Studentlitteratur
241
10 Klassbiblioteket
AppDomain som en process (fast den kanske egentligen är en tråd i VES!). Trådar exekverar i en och samma AppDomain. De delar allt som en process normalt delar – framför allt minne – men har egen stack. Varje tråd är ju en egen anropskedja och den måste följaktligen ha egen stack. För att skapa och hantera trådar behövs metoder för att starta tråden, övervaka tråden och kanske avsluta tråden. Därtill behövs metoder för att synkronisera trådar i de kodsekvenser då hantering av objekt kan tänkas krocka (kritiska sektioner). Allt finns i System.Threading, och i några mycket enkla konstruktioner i C#syntaxen. Klass
Underbibliotek
Interlocked
BCL
Statiska trådsäkra metoder för enkla beräkningar
Monitor
BCL
Synkronisering
Thread
BCL
Metoder för trådhantering
Anmärkning
Timeout
BCL
Timer
BCL
WaitHandle
BCL
Abstrakt basklass tänkt för operativspecifika klasser
TimerCallback
BCL
Delegerare
ThreadStart
BCL
Delegerare
Exception-klasser: SynchronizationLockException
BCL
ThreadAbortException
BCL
ThreadStateException
BCL
Struct
Underbibliotek
Anmärkning
Enum-typer: ThreadPriority
242
BCL Kopiering av kurslitteratur förbjuden. © Studentlitteratur
10 Klassbiblioteket
Struct
Underbibliotek
ThreadState
BCL
Anmärkning
Den centrala klassen för trådhantering är Thread. Klassen har en enda konstruktor, och den tar en ThreadStart som parameter. ThreadStart är en delegerare. Delegerare i sin tur har ju konstruktor som tar en metod som parameter. ThreadStart har följande typ: public delegate void ThreadStart(); Nu ser vi mönstret: ett Thread-objekt är ett objekt som har en association (via en delegerare) till en metod som är void och utan parametrar. Det är den metoden som drar igång en ny tråd. class X { void MethodRunningInNewThread() { ... } public void Method() { Thread t = new Thread( new ThreadStart( this.MethodRunningInNewThread())); t.Start(); // anrop i ny tråd // befintliga tråden fortsätter } } När Thread-objektet skapats är det alltså bara att anropa Start så börjar trådmetoden exekvera i en ny tråd. Start returnerar direkt och nästa sats exekverar i den befintliga tråden. När trådmetoden exekverat klart avslutas tråden. I Thread-klassen finns också flera metoder för att styra och övervaka tråden. Man kan från huvudtråden undersöka om sekundärtråden fortfarande exekverar, man kan blockera viss tid, vänta på en eller flera trådar och även brutalt avsluta trådar. Trådmetoden är i exemplet en metod i samma klass som den metod som startar tråden, men så behöver det inte vara. Trådmetoden kan
Kopiering av kurslitteratur förbjuden. © Studentlitteratur
243
10 Klassbiblioteket
vara en metod i en annan klass eller för ett annat objekt. Den kan också vara en statisk metod. Ett vanligt problem i andra miljöer är hur man får över parametrar till trådmetoden. Här såg vi att trådmetoden skulle vara helt utan parametrar. Men finessen här är att trådmetoden kan vara en instansmetod. Den börjar exekvera med ett aktuellt objekt, vilket ju eliminerar behovet av parametrar. class Y { double string public public }
a; b; Y(double a, string b) { ... } void MethodRunningInNewThread() { ... }
class X { public void Method() { Y obj = new Y(10.5, ”Hit the road”); Thread t = new Thread( new ThreadStart( obj.MethodRunningInNewThread())); t.Start(); // anrop i ny tråd // befintliga tråden fortsätter } } I detta utökade exempel är det en metod i klass X som startar tråden genom att skapa ett objekt av klass Y och initiera det objektet med diverse värden, för att sedan initiera Thread-objektet med en metod i den klassen (Y) och för det objektet (obj). Trådmetoden har nu alla data att arbeta med i form av det aktuella objektet. Parametrar behövs inte tack vare möjligheten att starta instansmetoder i ny tråd.
244
Kopiering av kurslitteratur förbjuden. © Studentlitteratur
10 Klassbiblioteket
10.5.1 Synkronisering För synkronisering finns klassen Monitor. Traditionellt löser man synkroniseringsproblemet genom att ha ett separat objekt med metoder som låser/låser upp, och som är sådana att det avgörande momentet för låsning är ”atomic” – sker i en enda instruktion, så att alltid den ena tråden entydigt låser den andra och ett ogiltigt tillstånd i synkroniseringsobjektet inte kan uppstå. Man kan sedan tänka sig att ha ett sådant objekt i varje objekt, eller att ha ett gemensamt för synkronisering av flera objekt, kanske för alla objekt som accessas av mer än en tråd. Metoden som låser har logiken att första tråd som anropar får en omedelbar retur, medan följande trådar blockeras i anropet. Metoden returnerar inte förrän första tråd anropar metoden som släpper objektet. I Monitor-klassen heter metoderna Enter respektive Exit. Att ha ett synkroniseringsobjekt i varje klass och anropa metoden för att låsa inifrån varje enskild annan metod i klassen brukar kallas trådsäkring på klassnivå. Det gör klientkod enkel men ger prestandaförlust eftersom ett stort antal onödiga låsningar då sker. Att trådsäkra med separata objekt skapade i klientkod är i princip bättre men ger det mycket besvärliga problemet att man måste förutse alla fall då synkronisering behövs. Det tankearbetet är påfallande tungt och den designen brukar bli buggig. Klassen Monitor tar ett delvis nytt grepp. Den har statiska metoder för låsning/upplåsning, vilka tar en Object som parameter. Här krävs en stunds eftertanke. Metoden Enter låser access till det objekt som utgör parameter. Metoden Exit släpper låset och släpper därmed eventuell väntande tråd. Var är då synkroniseringsobjektet? Faktum är att det behöver vi inte veta. CLI standardiserar inte hur det ska se ut på bitnivå, bara att det ska fungera. Det är alltså som om det i varje Object finns några odokumenterade bitar som utgör synkroniseringsobjekt. Men Object själv har inga metoder för att sätta dem, det gör man med Monitor.Enter och -Exit.
Kopiering av kurslitteratur förbjuden. © Studentlitteratur
245
10 Klassbiblioteket
Den minnesgode läsaren noterade att Object faktiskt har en konstruktor, att det går att skapa Object-objekt. Det enda skäl som nämns i dokumentationen för att ha en konstruktor i en (till synes) tom klass är att man kan använda ett Object som synkroniseringsobjekt. Där finns alltså ändå något... Slutsatsen blir att det med detta upplägg blir extremt enkelt att trådsäkra objekt. Problemet att göra det precis där det behövs kvarstår förstås – men syntaxmässigt blir saken enkel. Omge kritiska sektioner med Monitor.Enter och Monitor.Exit. Gör det i klassen och använd då parametern this, eller gör det i kientkod med objektet i fråga som parameter. Vi utökar förra exemplet ytterligare lite för att illustrera: class Y { double a; string b; public Y(double a, string b) { ... } public double A { get { return a; } set { a = value; } } public void MethodRunningInNewThread() { this.A = 200.0; // problem! } } class X { public void Method() { Y obj = new Y(10.5, ”Hit the road”); Thread t = new Thread( new ThreadStart( obj.MethodRunningInNewThread())); t.Start(); // anrop i ny tråd // befintliga tråden fortsätter 246
Kopiering av kurslitteratur förbjuden. © Studentlitteratur
10 Klassbiblioteket
obj.A = 100.0; // problem! } } Nu har klassen Y en egenskap som heter A och som kan läsa/sätta fältet a. Huvudtråden startar den nya tråden genom att anropa MethodRunningInNewThread, och den gör genast en ändring i A:s värde. Detsamma gör huvudtråden direkt när Start returnerat – alltså sannolikt ungefär samtidigt. Vi har ett synkroniseringsproblem. I detta fall är synkronisering i klassens egna metoder nödvändig (det är ju MethodRunningInNewThread som är det ena fallet), så vi väljer att trådsäkra egenskapen A. ... public double A { get { ... } set { Monitor.Enter(this); a = value; Monitor.Exit(this); } } ... Därmed är problemet löst. Två samtidiga accesser till egenskapen A gör att den ena tråden får vänta. Detta upplägg är inte svårt, ändå finns faktiskt en liten förenkling. I C# har det reserverade ordet lock precis samma effekt. Det är en smaksak vilket skrivsätt man föredrar: public double A { get { ... } set { lock(this) {
Kopiering av kurslitteratur förbjuden. © Studentlitteratur
247
10 Klassbiblioteket
a = value; } } } Ett block som inleds med lock innebär Monitor.Enter när blocket börjar exekvera och Monitor.Exit när exekvering når blockets slut. För de fall då man vill omge hela metoder med lock finns dessutom ytterligare ett sätt, nämligen att markera metoden med ett attribut. Vi hänvisar till dokumentationen för System.Runtime.CompilerServices.MethodImplAttribute. Statiska metoder kan ibland behöva trådsäkras. Det går ju inte med ovanstående teknik. Där utgörs ju en parameter av det objekt som ska låsas. Men genom att använda den aktuella typens Type-objekt kan man åstadkomma låsning av statiska medlemmar. Om klassen Y hade haft några statiska metoder kunde vi anropat dem trådsäkert med: lock (typeof(Y)) { Y.SomeStaticMethod(..); } Operatorn typeof ger ju typens Type-objekt, och en låsning på det låser alla statiska medlemmar. För vissa vanliga och enkla beräkningar på heltal och flyttal finns i klassen Interlocked färdiga statiska metoder som gör det trådsäkert. De gör det antingen genom att kompileras till maskinkod där en enda instruktion är den kritiska (atomic), eller genom att synkronisera implicit. De kan vara praktiska att känna till som särskilt enkla att använda. Där finns också ett par metoder för object-referenser. System.Threading har inga interface.
10.5.2 Asynkron programmering I avsnittet om namnrymden System nämndes där några datatyper som egentligen har med trådar att göra, men som ändå placerats i System. För att förklara dem bör man först ha förstått trådhante-
248
Kopiering av kurslitteratur förbjuden. © Studentlitteratur
10 Klassbiblioteket
ringen så som den beskrivits i detta avsnitt, och därför ska vi nu titta på dem här. Datatyperna det gäller är IAsyncResult och AsyncCallback. En titt på deras innehåll ger inte många upplevelser, men de är i själva verket kärnan i ett raffinerat designmönster som används i flera klasser, särskilt i streamklasserna. Idén går ut på, att om man i en klass har en metod som i praktiken kan bli tidsödande, bör man göra två liknande metoder förutom den ursprungliga. Den ena bör heta BeginXxx och den andra bör heta EndXxx om den ursprungliga heter Xxx. Med tidsödande menas att de kan tänkas ta så lång tid att en användare blir otålig och börjar agera mot användarinterfacet trots att det ”frusit”. Alltså någon sekund eller så. Metoden BeginXxx ska anropa Xxx i en ny tråd. Därefter ska den omedelbart returnera ett objekt som implementerar IAsyncResult. IAsyncResult ser ut som följer: public interface IAsyncResult { bool IsCompleted { get; } WaitHandle AsyncWaitHandle { get; } object AsyncState { get; } bool CompletedSynchronously { get; } } Här finns fyra egenskaper, samtliga med enbart getgren. Intressantast är IsCompleted, vilken har typen bool. När BeginXxx returnerat en IAsyncResult ska den anropande tråden närhelst den vill kunna kontrollera om Xxx exekverat färdigt genom att se om IsCompleted är true. När IsCompleted är true kan anropande tråd anropa EndXxx, och den ska då returnera omedelbart. Om EndXxx anropas innan Xxx är klar ska EndXxx vara blockerande tills Xxx är klar. Nu ser vi en del av finessen. Under tiden som Xxx arbetar kan anropande tråd göra något annat, exempelvis ta hand om användarinterfacet (mucklick, tangentbord) så att användaren inte märker den
Kopiering av kurslitteratur förbjuden. © Studentlitteratur
249
10 Klassbiblioteket
tidsödande proceduren alls. Samtidigt finns alla möjligheter att när som helst lägga anropande tråd i väntan genom att anropa EndXxx. En annan möjlighet är att använda egenskapen AsyncWaitHandle, som är en WaitHandle. Klassen WaitHandle har metoderna WaitOne, WaitAny och WaitAll, som gör det möjligt att vänta (blockeras) viss tid eller tills flera trådar avslutats. Med den kan alltså anropande tråd väckas med jämna mellanrum. Att anropa BeginXxx och omedelbart därefter EndXxx blir detsamma som att anropa Xxx direkt och vänta på dess retur. Att anropa BeginXxx, polla IsCompleted och anropa EndXxx ger möjlighet att göra annat mellan pollningarna. Att anropa BeginXxx och låta sig blockeras med AsyncWaitHandle ger ännu flera logikmöjligheter. Men hur hantera parametrar och returvärden i detta mönster? BeginXxx ska ha samma parameterlista som Xxx, men returnera en IAsyncResult. EndXxx ska ta en IAsyncResult som parameter och returnera det som Xxx returnerar! Därmed kan olika anrop till EndXxx identifieras såsom gällande ett visst motsvarande BeginXxx. Flera samtidiga Xxx kan exekvera i flera trådar! Mönstret är implementerat i flera klasser: public abstract class Stream : MarshalByRefObject, IDisposable { public virtual void EndWrite( IAsyncResult asyncResult); public virtual IAsyncResult BeginWrite( byte[] buffer, int offset, int count, AsyncCallback callback, object state); public virtual int EndRead( IAsyncResult asyncResult); public virtual IAsyncResult BeginRead( byte[] buffer, int offset, int count, AsyncCallback callback, object state); ... }
250
Kopiering av kurslitteratur förbjuden. © Studentlitteratur
10 Klassbiblioteket
Metoderna finns också omdefinierade i de härledda klasserna FileStream och NetworkStream. public abstract class WebRequest : MarshalByRefObject { public virtual Stream EndGetRequestStream( IAsyncResult asyncResult); public virtual IAsyncResult BeginGetRequestStream( AsyncCallback callback, object state); public virtual WebResponse EndGetResponse( IAsyncResult asyncResult); public virtual IAsyncResult BeginGetResponse( AsyncCallback callback, object state); ... } Metoderna finns också omdefinierade i den härledda klassen HttpWebRequest. public sealed class Dns { public static IAsyncResult BeginGetHostByName( string hostName, AsyncCallback requestCallback, object stateObject); public static IPHostEntry EndGetHostByName( IAsyncResult asyncResult); public static IAsyncResult BeginResolve( string hostName, AsyncCallback requestCallback, object stateObject); public static IPHostEntry EndResolve( IAsyncResult asyncResult); } public class Socket : IDisposable { public IAsyncResult BeginConnect( EndPoint remoteEP, AsyncCallback callback, object state); Kopiering av kurslitteratur förbjuden. © Studentlitteratur
251
10 Klassbiblioteket
public void EndConnect(IAsyncResult asyncResult); public IAsyncResult BeginSend( byte[] buffer, int offset, int size, SocketFlags socketFlags, AsyncCallback callback, object state); public int EndSend(IAsyncResult asyncResult); public IAsyncResult BeginSendTo( byte[] buffer, int offset, int size, SocketFlags socketFlags, EndPoint remoteEP, AsyncCallback callback, object state); public int EndSendTo(IAsyncResult asyncResult); public IAsyncResult BeginReceive( byte[] buffer, int offset, int size, SocketFlags socketFlags, AsyncCallback callback, object state); public int EndReceive(IAsyncResult asyncResult); public IAsyncResult BeginReceiveFrom( byte[] buffer, int offset, int size, SocketFlags socketFlags, ref EndPoint remoteEP, AsyncCallback callback, object state); public int EndReceiveFrom( IAsyncResult asyncResult, ref EndPoint endPoint); public IAsyncResult BeginAccept( AsyncCallback callback, object state); public Socket EndAccept(IAsyncResult asyncResult); ... } En närmare titt på dessa klassers Begin/End-metoder visar att parameterlistan i samtliga BeginXxx har två extra parametrar. Den ena är en AsyncCallback och den andra är en object med namnet state. Detta avslöjar ytterligare en mekansim i designmönstret. Båda kan ha värdet null i anropet. Men de kan också användas för ytterligare automatik. Om man tänker sig att den tidsödande proceduren är en ren arbetstråd, det vill säga att den ska göra någon databehandling och därefter uppdatera några variabler (eller en databas). Då behöver kanske 252
Kopiering av kurslitteratur förbjuden. © Studentlitteratur
10 Klassbiblioteket
inte primärtråden alls bevaka när det är klart. Men om vi gör anropet till BeginXxx så kommer ju returvärdet från EndXxx. Om inte primärtråden anropar EndXxx, vem ska då göra det? Nu anar man vad delegeraren AsyncCallback ska vara till. Om vi gör ytterligare en metod vars ändamål är att anropa EndXxx och placera returvärdet där det ska vara, så kunde den anropas av delegeraren automatiskt då Xxx är klar! Just så är det tänkt, och delegerartypen AsyncCallback har precis det utseende vi behöver: public delegate void AsyncCallback(IAsyncResult ar); I alla de klasser där mönstret är implementerat fungerar BeginXxx alltså så, att om den parameter som utgör delegeraren är skild från null, så sker ett anrop till den då Xxx returnerat. Det typiska vi bör göra är alltså att göra en metod som matchar delegeraren, och i den metoden anropa EndXxx för att ta hand om returvärdet och i övrigt göra vad som bör göras då proceduren är klar. Observera att den anropskedjan (delegeraren-hanteraren-EndXxx) då också exekverar i sekundärtråden, vilket ju är alldeles utmärkt. Den andra extraparametern, den som har typen object har ingen särskild roll i sammanhanget. Den är till för användarens speciella behov. Man kan exempelvis vilja ha ett synkroniseringsobjekt eller ett objekt med kompletterande information. Den får ha värdet null. Referens till den finns också i den returnerade IAsyncResult, i egenskapen AsyncState.
10.6 System.Text Denna namnrymd innehåller enbart några klasser för texthantering. Vid närmare granskning visar det sig egentligen bara finnas två centrala klasser, StringBuilder och Encoding. Klass
Underbibliotek
Anmärkning
StringBuilder
BCL
Traditionell strängklass
Kopiering av kurslitteratur förbjuden. © Studentlitteratur
253
10 Klassbiblioteket
Klass
Underbibliotek
Encoding
BCL
Decoder
BCL
Encoder
BCL
ASCIIEncoding
BCL
UnicodeEncoding
BCL
UTF8Encoding
BCL
Anmärkning
StringBuilder är en strängklass som kompletterar klassen String. String är ju speciell på flera sätt, framförallt är den immutable. Dess innehåll kan inte förändras, alla metoder returnerar en ny String. Vi har tidigare studerat den i avsnitt 2.1. StringBuilder är mera traditionell. Ett StringBuilder-objekt kan skapas med defaultkonstruktor (vilket inte går med String). Därefter kan den byggas med tilldelningar, plusoperatorn och med formatparametrar så som vi sett i samband med Console.WriteLine i avsnitt 2.1. StringBuilder s = new StringBuilder(); s.AppendFormat(”Ledtext {0} ledtext {1}”, x, y); s.Append(z); I exemplet kan x, y och z vara vad som helst. Deras ToString anropas och strängen komponeras enligt deras formatkods placering. Formatkoderna kan också förses med specificerande koder för ytterligare formatering. För detaljer hänvisas till dokumentationen. Majoriteten metoder i klassen är alla de överlagrade Append-metoderna och likaledes flertaligt överlagrade Insert. I övrigt finns Remove och Replace för att radera respektive byta ut delar av strängen. Några egenskaper finns också, för exempelvis strängens längd. Den andra intressanta klassen är Encoding. Alla de övriga är relaterade till den. ASCIIEncoding, UnicodeEncoding och UTF8Encoding är härledda, och Encoder/Decoder är basklasser för en medlem i Encoding-klasserna. Encoding har tre statiska egenskaper som returnerar en ASCIIEncoding, UnicodeEnco-
254
Kopiering av kurslitteratur förbjuden. © Studentlitteratur
10 Klassbiblioteket
ding respektive UTF8Encoding (en anmärkningsvärd konstruktion: egenskap i en basklass returnerar objekt av härledd klass). Vad gör då dessa? Ändamålet är konvertering av textformat, det vill säga översättning av teckenuppsättningar. Som bekant är all text i CLI Unicode – men verkligheten därute är för överskådlig framtid en fortsatt djup språkförbistring. ANSI, ASCII i åtskilliga varianter, EBCDIC och andra fantasifulla och förvirrade teckenuppsättningar förgiftar samvaron i världen. Placeringen av å. ä och ö brukar vara slumpmässig och sortering med nationella tecken fungerar sällan. Omvandling mellan små och stora bokstäver (gemener och versaler) fungerar sällan heller. Unicode löser problemet CLI-applikationer emellan, men inte mot andra miljöer. Vi har tidigare sett att textbaserade applikationsprotokoll såsom HTTP använder ASCII – närmare bestämt den del av ASCII-tabellen som har värden under 128 (7 bitar). XML använder samma teckenuppsättning, liksom HTML. I samband med sockets omvandlade vi då en sträng till ASCII med Encoding.ASCII.GetBytes(string). ASCII 7-bit har samma tecken för samma värden som i Unicode. Om man tar bara den låga av de två byte som utgör ett Unicodetecken så får man alltså ASCII. Men man måste vid exempelvis läsning av en fil veta om innehållet representerar ASCII eller Unicode, alltså om en eller två byte ska utgöra ett tecken. Det är där UTF8 kommer in. UTF8 lagrar tecken som en byte om tecknet har värde under 128 och två byte om tecknet har större värde. Den höga byten sätter då också sin högsta bit för att markera att två bytes ska användas. Formatet är genialt i all sin enkelhet. En UTF8-fil innehållande enbart a till z, siffrorna och de vanligaste skiljetecknen – så som det är i XML, HTML med flera – ger därmed identiskt resultat oavsett om den tolkas som ASCII (alla varianter), ANSI, alla möjliga andra eller som UTF8. Samtidigt kan en UTF8-fil innehålla text i vilket främmande språk som helst med den mest fantasifulla teckenuppsättning. Då är den Unicode.
Kopiering av kurslitteratur förbjuden. © Studentlitteratur
255
10 Klassbiblioteket
Sättet att använda Encoding är just det vi sett i flera exempel. Skapa objektet med någon av de statiska egenskaperna och använd metoderna GetBytes och GetString. GetBytes returnerar en byte-vektor med den aktuella teckenuppsättningen, och GetString returnerar en String, vilket ju är Unicode. string s = ”Hello”; byte[] AsciiStr = Encoding.ASCII.GetBytes(s); En liten märklighet i standarddokumentationen är att en egenskap som heter Default returnerar ett Encoding-objekt som sägs ge konvertering till/från ANSI. Men någon sådan klass är inte dokumenterad. I Microsofts .NET däremot ger den egenskapen ett ANSIobjekt, eftersom ANSI är teckenuppsättningen i Windows. Möjligen en detalj som följt med av misstag från Microsofts dokumentation. ANSI bör rimligen inte anses vara ”Default” i alla miljöer till tidernas ände. System.Text har inga structer och inga interface.
10.7 System.Xml XML är standardiserade principer för beskrivning av data och datatyper i text. En XML-fil innehåller 7-bit ASCII som formar innehåll och taggar. Taggar omges av < och > och kan i sin tur innehålla attribut. Start- och sluttaggar måste finnas i par, de skapar en hierarki genom att ingå i omgivande par och så vidare. I System.Xml finns klasser för att förenkla skapande och tolkande av XML-filer. De centrala klasserna är XmlTextReader och XmlTextWriter, och deras respektive basklasser XmlReader och XmlWriter.
256
Kopiering av kurslitteratur förbjuden. © Studentlitteratur
10 Klassbiblioteket
Klass
Underbibliotek
NameTable
XML
Härledd från XmlNameTable
XmlConvert
XML
Konverterar teckenuppsättning, datatypnamn etc.
XmlNamespaceManager
XML
XmlNameTable
XML
XmlParserContext
XML
Anmärkning
Abstrakt. Samling av String-objekt
XmlReader
XML
Abstrakt, läser XML
XmlWriter
XML
Abstrakt, skapar XML
XmlTextReader
XML
Härledd från XmlReader
XmlTextWriter
XML
Härledd från XmlWriter
XmlResolver
XML
Abstrakt
XmlUrlResolver
XML
Härledd från XmlResolver
Exception-klasser: XmlException
XML
XmlTextReader är den allra intressantaste klassen. Man skapar ett objekt genom att ge konstruktorn ett filnamn. Därefter kan filen läsas och parsas på ett förenklat sätt genom att varje anrop till Read returnerar en ny nod. Om vi har en XML-fil products.xml med följande innehåll kan den läsas med XmlTextReader:
De Luxe
Anna
Kopiering av kurslitteratur förbjuden. © Studentlitteratur
257
10 Klassbiblioteket
Andersson
899.00
Fancy Extra
Johan Karlsson
225.00
The Killer
Fredrik Svensson
95.00
Ett mycket elementärt program för att parsa filen kan se ut som följer: XmlTextReader reader = new XmlTextReader(”products.xml”); while (reader.Read()) if (reader.NodeType == XmlNodeType.Text) Console.WriteLine(reader.Value); För att välja enbart den information som utgör textnoder jämförs i if-satsen med en enum som heter XmlNodeType, och som har åtskilliga konstanter för olika slags noder. Programmet skriver ut all information i en lista: De Luxe Anna Andersson 258
Kopiering av kurslitteratur förbjuden. © Studentlitteratur
10 Klassbiblioteket
899.00 Fancy Extra Johan Karlsson 225.00 The Killer Fredrik Svensson 95.00 Notera att den information som fanns i attribut inte kom med. För att kontrollera om attribut finns i den aktuella noden kan man använda egenskapen HasAttributes. Om den är true finns attribut. Det finns också en egenskap AttributeCount som ger antalet attribut. Den kan användas för att traversera dem. XmlTextReader reader = new XmlTextReader(”products.xml”); while (reader.Read()) { if (reader.NodeType == XmlNodeType.Element) for (int i=0; i