• No results found

En mycket användbar funktion hos .NET är attribut (attributes) som bygger på att extra metadata läggs till i assemblyt. Denna metadata kan sedan dynamiskt läsas ut och användas av annan programkod. Möjligheten att dynamiskt undersöka egenskaper hos typer — och även skapa instanser och anropa metoder — kallas reflektion. Inget motsvarande existerar i C++ men däremot i Java som var det första ”stora” språk som stödde reflektion.

Det finns flera poänger med att lägga till extra metadata. En är att helt enkelt lägga till extra information om klasser, metoder etc som en typ av dokumentation som alltid med- följer både källkoden och det kompilerade assemblyt. En annan är att genom att dynamiskt undersöka om t ex en klass eller metod försetts med ett visst attribut kan olika beteenden

5.8. Attribut

fås trots att den av kompilatorn genererade MSIL-koden inte påverkas av attributen.5 Ett exempel på detta är attributet Serializable, som används för att markera att en klass kan serialiseras, dvs att dess innehåll kan sparas till fil eller skickas över ett nätverk och sedan användas för att bygga upp objektet igen. Detta används av applikationen Clock (se avsnitt 7.2.3 sid 80).

Genom att skapa en klass som ärver från System.Attribute kan man definiera egna at- tribut. Här ges ett exempel på hur detta kan användas som ett sätt att utöka de deklarativa möjligheterna hos ett programmeringsspråk. Som framgår av avsnitt 7.2.1 sid 72 har VB för händelsehantering en smidigare (mer deklarativ) syntax än C#. Följande är ett enkelt exempel i VB på hantering av ett par händelser i en Form-klass:

Imports System

Imports System.Windows.Forms Public Class EventForm

Inherits Form Public Sub New()

button.Parent = Me End Sub

Private WithEvents button As New Button

Private Sub onWindowClick(ByVal sender As Object, _ ByVal e As EventArgs) Handles Mybase.Click

Console.WriteLine("Window clicked!")

End Sub

Private Sub onButtonClick(ByVal sender As Object, _ ByVal e As EventArgs) Handles button.Click

Console.WriteLine("Button clicked!")

End Sub End Class 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22

VB

Programmet gör inget mer än att skriva ut meddelanden när användaren klickar i fönstret och på en knapp (som kommer hamna i fönstrets övre vänstra hörn). Det intressanta är syntaxen för att hantera Click-händelserna för fönstret och för knappen. Detta görs genom att använda Handles-nyckelordet i definitionen av den metod som ska hantera händelsen. Om händelsen är definierad i en basklass till den aktuella klassen (som för onWindowClick) anges Mybase (motsvarande base i C#) framför händelsenamnet, i annat fall namnet på det fält som innehåller händelsen (som för onButtonClick). Utöver detta krävs också att nyckelordet WithEvents används för de fält som händelser hanteras för. Detta är troligen ett designbeslut baserat på att programmeraren vid läsning av koden ska bli observant detta. Förutsatt att det redan finns minst en händelse som hanteras för fältet behöver man dock bara lägga till en ny metod med Handles-nyckelordet för att hantera en händelse.

I C# måste varje händelsehanterare knytas till sin händelse med en programsats som typiskt exekveras i konstruktorn (se 7.2.1, sid 72 för ett exempel på hur detta ser ut). Medan 5Detta gäller normalt sett, och alltid för egendefinierade attribut. Dock finns attribut som är till just för att

detta inte är någon större olägenhet för en klass som är så liten som i detta exempel krånglar det till proceduren en del för en större klass. Förutom att lägga till en ny metod när en ny händelse ska hanteras måste man leta upp konstruktorn (eller annan lämplig del av koden) och koppla metoden till händelsen. En liknande olägenhet uppträder då man vill ta bort hanteringen av en viss händelse.

Med ett egendefinierat attribut, en hjälpklass och reflektion är det möjligt att hante- ra händelser i C# på ett lika enkelt sätt som i VB! En metod i hjälpklassen (här kallad EventManager) anropas i konstruktorn, varefter händelsehanterare kan läggas till genom att dekorera händelsehanteringsmetoden med attributet. Exemplet ovan kommer då se ut så här:

using System;

using System.Windows.Forms; using Examples;

public class EventForm : Form {

public EventForm()

{

EventManager.RegisterHandlers(this);

button.Parent = this; }

private Button button = new Button();

[Handles("Click")]

private void onWindowClick(object sender, EventArgs e)

{

Console.WriteLine("Window clicked!");

}

[Handles("button.Click")]

private void onButtonClick(object sender, EventArgs e)

{

Console.WriteLine("Button clicked!");

} } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26

C#

Det egendefinierade attributet har det fullständiga namnet HandlesAttribute, men om kon- ventionen att avsluta namnet med Attribute följs behöver bara Handles anges. Det enda kompilatorn gör när den upptäcker attributet är att baka in det i assemblyts metadata; den genererade MSIL-koden påverkas inte. Kopplingen av den dekorerade metoden till den an- givna händelsen görs i den statiska metoden RegisterHandlers i klassen EventManager som använder reflektion för att ta reda på vilka metoder som dekorerats med attributet.

Jämfört med den inbyggda händelsehanteringen i VB är den enda egentliga nackdelen med denna lösning att eventuella fel, t ex att en händelse som inte finns anges eller att den dekorerade metoden har felaktig signatur, inte upptäcks när koden kompileras. Alla sådana fel kommer dock upptäckas så snart RegisterHandlers exekverar och den kommer då kasta ett undantag av typen EventManagerException innehållande en detaljerad beskrivning av vad som är fel. Det finns således ingen risk att sådana felaktigheter i användningen av

5.9. Felhantering

attributet överlever testfasen. Vidare kommer inte prestandan att påverkas negativt bortsett från tiden för anropet till RegisterHandlers.

Jämfört med VB-syntaxen krävs ingen speciell markering av button-fältet. Om en så- dan markering anses vara en önskvärd funktion är det inget som hindrar att man definie- rar ett annat attribut som används för att dekorera de fält för vilka meddelanden hanteras. RegisterHandlers får då också kontrollera att detta attribut används på rätt sätt. Det skulle även gå att förenkla händelsehanteringen ytterligare genom att göra det möjligt att använda Handles-attributet för parameterlösa metoder. Ofta används ändå inte parametrarna (precis som i detta exempel).

Poängen med exemplet är se hur det med egendefinierade attribut finns stora möjligheter att skapa deklarativa utökningar. Attribut är en väsentlig del av .NET.

Källkoden till EventManager, HandlesAttribute och EventManagerException finns tillgänglig via rapportens webbsida, se [12].

5.9

Felhantering

En relativt senkommen finess i C++ är möjligheten till strukturerad undantagshantering (exception handling) med nyckelorden try och catch. Att detta inte fanns i C eller i tidiga versioner av C++ innebär att Win32-API:et, MFC och COM har egna sätt att hantera fel på. Vissa fel rapporteras genom att metoder returnerar felkoder (enligt olika konventioner) och andra genom undantagshantering. Felhantering i traditionell Windowsprogrammering är därför minst sagt rörig och risken är mycket stor att potentiella fel ignoreras eller hanteras felaktigt.

I .NET hanteras alla fel med undantagshantering. Specifikt gäller detta alla typer i klass- biblioteket FCL, och det är därmed vad som (starkt) rekommenderas även för all annan kod. Undantagshantering i .NET fungerar problemfritt över gränsen mellan olika assemblyn och programmeringsspråk, och CLR:en har inbyggda funktioner för ”stackvandring” som gör att det enkelt går att spåra var någonstans ett ohanterat undantag uppstått (även i en icke- debugkompilerad applikation).

En stor fördel med undantag är att de inte (som returnerade felkoder) går att ignorera. Om ett undantag inträffar måste det hanteras i en catch-sats någonstans, annars avslutas applikationen. (I en Windowsapplikation finns normalt en dold catch-sats runt meddelan- deslingan vilket ger möjlighet för applikationen att fortsätta exekvera, men det dyker i alla fall upp ett meddelandefönster som klargör att något gick fel.)

Ofta är det vanligt att det inte finns anledning att fånga ett undantag för att hantera det men att koden behöver veta om ett undantag inträffat för att kunna frigöra resurser som skulle ha frigjorts om den aktuella metoden fått fortsätta exekvera normalt. I och med den automatiska minneshanteringen behövs inte detta för vanliga objektallokeringar men väl för sådant som öppna filer och nätverksanslutningar. För att underlätta detta finns nyckelordet finally/Finally. Det som anges inuti ett finally-block kommer alltid att exekvera, oavsett om ett undantag inträffar eller inte. Ett exempel ges i följande metod:

Function GetContentsAsString(ByVal filename As String) As String Dim reader As StreamReader = Nothing

Try

' Försök skapa en strömläsare för filen

reader = New StreamReader(filename)

Return reader.ReadToEnd()

Finally

If Not reader Is Nothing Then reader.Close()

End Try End Function 1 2 3 4 5 6 7 8 9 10 11 12

VB

Metoden tar ett filnamn som argument och returnerar en sträng med filens innehåll (som text). Metoden ska alldeles avgjort inte fånga och hantera de undantag som kan inträffa, för vad skulle i så fall returneras? Istället är det den kod som anropar metoden som är ansvarig för detta. Däremot är det viktigt att filen stängs om ett undantag inträffar. Ett sätt är att fånga undantaget, se till att filen stängs och därefter kasta ut undantaget (eller ett annat undantag) igen. Denna metod skulle användas i ett språk som saknar Finally-nyckelordet.6 Finally underlättar. Koden i detta block kommer alltid att exekveras oavsett om metoden får köra normalt eller om ett undantag inträffar. Det vanligaste i detta fall är visserligen att undantaget inträffar för att filen inte finns (varvid den aldrig öppnats och inte behöver stängas), men även läsningen på rad 7 kan ge upphov till ett undantag och då måste filen stängas.

Detta mönster med lokala objekt som innehåller resurser som behöver stängas/frigöras oavsett hur metoden returnerar är så vanligt att C# (men inte VB) har en speciell sats just för detta. Den kan användas för alla objekt som följer det så kallade Dispose-mönstret. Klasser som innehåller ohanterade resurser (dvs allt som inte är rena minnesallokeringar och som behöver stängas/frigöras) implementerar gränssnittet IDisposable, med metoden Dispose. (Close i exemplet gör exakt samma sak som den Dispose-metod som lika gärna skulle kunna användas.) Det kortaste sättet att skriva ovanstående kod i C# är:

string GetContentsAsString(string filename)

{

using (StreamReader reader = new StreamReader(filename))

{

return reader.ReadToEnd();

} } 1 2 3 4 5 6 7

C#

Denna version är helt ekvivalent med VB-versionen och blir tack vare using-satsen avsevärt kortare och snyggare. Även om ingen ny funktionalitet tillförs anser författaren att detta är en stor fördel som C# har framför VB. Ju smidigare det är att skriva den här formen av kod desto mindre risk att programmeraren helt struntar i att beakta vad som sker om ett undantag inträffar.

Det bör också noteras att om filen inte stängs explicit kommer den sannolikt inte förbli öppen ända tills applikationen avslutas. Istället kommer den stängas vid den obestämda 6Det kan dock noteras att i C++ är behovet av detta mindre eftersom lokala objekt oftast allokeras på stacken

5.9. Felhantering

tidpunkt då upprensning sker av StreamReader-objektet, detta tack vare att StreamReader innehåller en så kallad finaliserare (som i C# definieras med samma syntax som destruktorer i C++). Det ingår i Dispose-mönstret att det ska finnas en sådan. Detta gör att det inte är lika allvarligt att glömma bort att stänga en fil eller annan resurs som det är glömma bort att frigöra ett ohanterat C++-objekt. Det är dock alltid bättre att anropa Dispose/Close (manuellt eller via using-satsen) istället för att förlita sig på finaliseraren.

I C# och VB.NET måste alla undantag ärva från FCL-klassen System.Exception (även om CLR:en inte kräver detta). För ett exempel på hur ett undantag fångas se exempelappli- kationen Clock (avsnitt 7.2.1, sid 73).

Kapitel 6

Specifikation av testapplikationer

I detta kapitel specificeras funktionaliteten för de fyra testapplikationer som har implemen- terats och använts för att jämföra språken. Sist i kapitlet finns en sammanställning av vilka aspekter varje version är tänkt att jämföra.

6.1

Clock

Clock är en enkel Windowsapplikation som till strukturen har mycket gemensamt med många större Windowsapplikationer. Programmet utgör en analog klocka med följande mi- nimikrav på funktionalitet:

• Klockan ska ha tim-, minut- och sekundvisare och uppdateras minst en gång i sekun- den.

• Klockans storlek relativt det fönster den befinner sig i ska kunna ändras mellan tre fasta lägen.

• Stöd för flera samtidiga klockor med MDI-gränssnitt (Multiple Document Interface). • Alarmfunktion. Händelser ska kunna skapas, sparas och utlösas vid en viss tidpunkt. • Val av storlek och färg samt alarmhändelser ska kunna göras med såväl menykom-

mandon som verktygsfältsknappar (färgen även genom dubbelklick på klockan). • Klockans egenskaper och tillhörande alarmhändelser ska kunna sparas på fil och läsas

in igen.

• Klockan ska vara helt flimmerfri, både då visarna flyttar sig och då fönstret ändrar storlek.

Dessa krav uppfylls av alla versionerna som i övriga avseenden skiljer sig åt i syfte att demonstrera sådant som är enklare att göra i ett visst språk. Att ge varje version exakt samma funktionalitet skulle (med hänsyn tagen till begränsad tid för implementeringen) ha gjort att viktiga aspekter hade missats.

Syftet med applikationen är att studera skillnaderna i kodmängd och struktur för de olika versionerna. Applikationen är i sitt standardutförande inte prestandakrävande och några prestandamätningar kommer inte att göras.

Clock har implementerats i tre versioner: i Visual C++, C# och VB.NET. C# och VB- versionerna är bortsett från syntaxen med några få undantag identiska eftersom de använder samma funktionalitet i .NET-ramverket.

6.2

WordCount

WordCount är en prestandakrävande konsolapplikation. Den går igenom en textfil och tar fram de 20 vanligaste orden (eller så många som finns) sorterade efter antalet förekomster. För ord som förekommer samma antal gånger är ordningen odefinierad.

För att få en enhetlig definition på vad som är ett ord bestäms att allt som separeras av ett eller flera av följande tecken utgör ett ord:

[space] , . ! ? ; " : ( )

Även radslutstecken — både Line Feed, (LF, ASCII 10,’\n’) och Carriage Return (CR, ASCII 13,’\r’) — räknas som separeringstecken.

Denna definition innebär att många teckensekvenser som inte är ord i språklig mening ändå betraktas som ord här och kommer att kallas ord i fortsättningen. En praktisk observa- tion är också att för en stor textfil i naturligt språk kommer de 20 vanligaste orden att vara riktiga ord.

En annan förenkling är att versaler görs om till gemener. Alla jämförelser sker alltså utan hänsyn tagen till gemener och versaler. Omvandlingen tillåts gå till på ett sätt som kan påverka vissa andra tecken (dock inte siffror).

WordCount har implementerats i 20 versioner (13 i C++, fyra i C# två i Java och en i VB.NET). Dessa beskrivs ingående i kapitel 8. Versionerna skiljer sig delvis åt i vilka begränsningar de har och kommer inte att producera identiska utdata för varje tänkbar in- datafil. Givet följande krav på indatafilen blir dock resultatet identiskt med samtliga 20 versioner:

• De 20 vanligaste orden (enligt ovanstående definition) består av bokstäver som ingår i ASCII.

• Inga av de 20 vanligaste orden förekommer exakt lika många gånger.

• Textfilens maximala radlängd uppgår till 255 tecken exklusive radslutstecken. • Textfilen innehåller maximalt 16384 olika ord.

• Hela textfilen kan hållas samtidigt i datorns minne.

För filer som inte uppfyller dessa krav tillåts att applikationens beteende är odefinierat, vilket inkluderar inte bara att den kan ge ett felaktigt resultat utan även krascha eller hänga sig. Om ordräkningen användes som en del av en riktig applikation skulle bättre felhantering implementeras.

De ”bästa” versionerna klarar godtyckliga tecken, radlängder, antal ord och filstorlek, medan andra har offrat detta för enkelhet och/eller bättre prestanda. Exakt vilka begräns- ningar varje version har framgår av tabell 8.1 i kapitel 8.4.1 och kan också utrönas genom att studera respektive versions källkod.

6.3. FFT

Syftet med WordCount är att jämföra såväl komplexitet som prestanda för olika imple- menteringar där inte bara språket varierar utan också val av färdig bibliotekskod (klasser och funktioner) och kompilatorversion. Frågor som kommer kunna besvaras är t ex:

• Kan något av språken betraktas som generellt snabbare än de andra? • Skiljer mängden kod på något systematiskt sätt?

• Hur mycket påverkar valet av olika färdiga klasser? • Skiljer det mellan olika kompilatorversioner?

• Vad vinns respektive förloras på att använda färdiga klasser jämfört med att imple- mentera allt själv? Är detta olika för olika språk?

6.3

FFT

FFT är en förkortning av Fast Fourier Transform (snabb fouriertransform) och är en klass av algoritmer för att snabbt beräkna den diskreta fouriertransformen (DFT, Discrete Fourier Transform) till en sekvens av tal (= en tidsdiskret signal).

Den diskreta fouriertransformen definieras av följande summa:

XN[k] = N−1

n=0

x[n]e−ikn(2π/N), k = 0, 1, . . . , N − 1

där x[n] är en sekvens av komplexa tal och XN[k] de resulterande komplexa fourierkoeffici-

enterna (i =√−1). Transformen är en inverterbar operation som inte ger någon dataförlust. Att använda denna definitionssumma direkt för att beräkna DFT:n är för långa sekven- ser mycket ineffektivt. En FFT-algoritm fungerar genom att successivt dela upp summan i mindre delsummor. Härledningen av detta görs inte här utan se t ex [7].

Applikationen FFT implementerar en FFT-algoritm hämtad från kapitel 12 i [4]1, till- sammans med lite testkod. FFT-beräkningen görs i en funktion (statisk metod) som tar som argument en array av flyttal (med realdelarna i jämna index och imaginärdelarna i udda) samt ett heltal som anger antalet tal (DFT-storleken). Resultatet skrivs in i samma array. Detta innebär att rutinen inte utnyttjar något extra minne men att en kopia av indatan måste göras innan anropet om man inte vill att den ska skrivas över.

Syftet med FFT är att jämföra prestandan för flyttalsberäkningar och i viss mån anrop av trigonometriska funktioner, operationer som är vanliga i många applikationer.

Implementeringar av FFT har gjorts i C++, C#, VB.NET och Java. Java-versionen har tagits med eftersom den är mycket lik C#-versionen.

6.4

Draw

Draw är en mycket enkel applikation vars enda uppgift och förmåga är att rita 500 000 linjer. Linjerna ritas med slumpmässiga koordinater i intervallet [0, 399] (i såväl x- som y-led) och i slumpmässiga färger.

1Den version som implementerats i det här arbetet skiljer sig från den ursprungliga genom att double an-

vänds genomgående istället för float och genom att elementen i in- och utdataarrayerna placeras med start vid index 0 istället för 1.

Syftet med Draw är i första hand att se ett exempel på prestandaskillnaden mellan GDI och GDI+. Även skillnaden mellan hur GDI och GDI+ används kommer beaktas.

Draw har implementerats i tre versioner: i C#, VB.NET och Visual C++ (med MFC). Precis som för Clock är C#- och VB.NET-versionerna i princip identiska bortsett från syn- taxen.

Related documents