5 Grundläggande in- och utmatning För att användaren skall kunna kommunicera med programmet krävs att man inkluderar ett eller flera bibliotek med rutiner. I Ada finns det ett antal paket som gör detta möjligt. De tre mest grundläggande av dessa är Ada.Text_IO, Ada.Float_Text_IO och Ada.Integer_Text_IO. Alla dessa tre läser data från tangentbordet och skriver saker på skärmen om man bara anropar rätt rutiner. I detta kapitel tas de mest grundläggande operationerna man kan göra för att kunna kommunicera med användaren upp. Paketen innehåller mer än det som tas upp här. Vissa delar av dessa paket kommer senare i boken, bl.a. när filhantering tas upp. Teori 5.1 Biblioteket Ada.Text_IO Biblioteket Ada.Text_IO innehåller procedurer, funktioner, typer m.m. som behövs när man skall inhämta data från eller skicka data till externa enheter (t.ex. tangentbord och skärm). Det avslutande IO i biblioteksnamnet är en förkortning för In and Out. Att det är hantering av textinmatning samt textutmatning det handlar om anges också i namnet. De rutiner som finns tillgängliga i Ada.Text_IO är många och några av de väsentligaste tas upp i detta kapitel. Vill du se alla finns de i bilaga C.1 och i referensmanualen för Ada 95. 5.1.1 Allmänt om in- och utmatning internt i datorn Det är väsentligt för en programmerare att veta lite grann om hur datorn arbetar internt. Detta för att bättre förstå hur de olika underprogrammen i paketet fungerar. I bilden som följer ser man schematiskt hur data (de tecken användaren matar in) går från tangentbordet, via tangentbordets inmatningsbuffert, till programmet och hur data går från programmet, via en utmatningsbuffert till skärmen. En buffer är 85
ett minne där man lagrar en mindre mängd data innan det levereras vidare. Put(X); Utmatningsbuffert Programmet Get(X); Inmatningsbuffert Den streckade linjen markerar själva datorns arbetsminne (primärminne) där bl.a. programmet finns då det körs. De delar som är markerade inom bubblan är de delar som programmet har tillgång till. De övriga delarna måste programmet fråga datorns operativsystem om hjälp för att nå. Operativsystemet administrerar datorns interna och externa enheter och körs parallellt med programmet. T.ex. hanteras användarens tangentbordstryckningar. Man kan belysa hela förloppet med ett exempel. Antag att programmet skall skriva ut Mata in ett heltal: på skärmen och sedan låta användaren mata in ett heltal. Därefter skall programmet läsa in talet från tangentbordsbufferten och lagra det i en variabel. Steg ett är att programmet måste utföra en instruktion som skriver ut texten Mata in ett heltal:. Instruktionen är: Put("Mata in ett heltal: "); Det som då händer är att texten skickas från programmet till operativsystemet med en begäran om att skrivas ut på skärmen. 86
Operativsystemet lagrar texten i utmatningsbufferten och sedan händer inget mer. Det är alltså inte givet att texten kommer ut direkt på skärmen. Detta beror på att man använt Put som instruktion. I instruktionen finns inget som säger att man tvingar operativsystemet att skicka informationen vidare till skärmen. Vad är det som kan framtvinga att texten blir synlig på skärmen? En sak som skulle göra att texten kommer ut är att bufferten blir full och något nytt skall lagras däri. Då töms bufferten (av operativsystemet) ut till skärmen och fylls med nya data. Ett annat sätt är att man ger en instruktion som säger att bufferten skall tömmas. Det finns en sådan i Ada och den instruktionen utförs i lite olika sammanhang. Den utförs bl.a. när man anropar Get (som är en begäran om att läsa från tangentbordet). Det kan man förstå då det är väsentligt att veta vad som skrivits innan man låter användaren mata in data. När man går vidare kommer programmet att be operativsystemet om en inmatning av ett heltal. Instruktionen för detta är (givet att X är en variabel deklarerad för typen Integer ): Teori Get(X); I detta läge töms utmatningsbufferten (om det finns något kvar i den) så att man ser texten på skärmen. Dessutom skickas en begäran till operativsystemet om att få data i tangentbordets inmatningsbuffert. Programmet stannar därefter och väntar på att få klartecken från operativsystemet. Operativsystemet kontrollerar om det redan finns data i bufferten och om det är så ges direkt klartecken. Om inte måste operativsystemet vänta på användarinmatning. Operativsystemet lagrar alla nedtryckningar,som användaren gör på tangentbordet, i inmatningsbufferten. Om användaren raderar tecken tas de bort. När användaren bekräftar sin inmatning (med vagnretur) lagras också detta tecken i bufferten och sedan kan klartecken ges till programmet för vidare bearbetning. 87
Innan man går vidare bör också poängteras att användarens alla tangentnedtryckningar också visas (ekas) på skärmen (den streckade pilen i bilden markerar detta) så att användaren ser vad den håller på med. Detta är inget programmet kan styra över så programmets text blandas nu med användarens indata. När nu programmet fått klartecken från operativsystemet kommer programmet att gå igenom inmatningsbufferten, tecken för tecken, för att plocka fram vilket tal det var som matades in. När programmet har hittat det data det söker,avbryts letandet och Get lagrar heltalet i variabeln som skickats som parameter till Get. Exakt hur Get fungerar för heltal,flyttal m.m. förklaras i följande kapitel. Observera att inmatningsbufferten inte måste bli tom efter det att Get gjort sitt jobb. Det kan vara så att användaren matade in fler tecken än det behövdes. Detta gör att operativsystemet nästa gång man vill göra en Get direkt kan ge klartecken och inte behöver vänta på ny inmatning från användaren. 5.1.2 Proceduren Get (för tecken) Proceduren Get för tecken har en deklaration enligt följande: procedure Get(Item : out Character); Proceduren läser ett tecken (det första) från inmatningsbufferten. En läsning av ett tecken innebär att tecknet försvinner från bufferten. Alla vanliga skrivbara tecken är acceptabla och tecknet lagras i Item för vidare befordran till den som anropar. Ett specialfall är att det är odefinierat vad som händer om en vagnretur (radmatning) ligger som första tecken i bufferten. Vad som då returneras via Item är alltså inte att lita på. Att detta är odefinierat beror på att olika datorer har olika representationer av hur vagnretur lagras internt. Om det finns data i utmatningsbufferten skrivs dessa ut innan Get utför sin uppgift. 88
Speciellt är också att man får ett stort problem om man försöker läsa ett tecken om inmatningsbufferten är markerad som slut på data (se funktionen End_Of_File för mer information om detta). Detta fall leder till att ett undantag (eng. exception ), End_Error kastas (eller reses). Om man inte fångar detta undantag i något av de anropande programmen kommer programmet att krascha. Senare finns ett helt kapitel som handlar om undantag. 5.1.3 Proceduren Get (för strängar) Proceduren Get för strängar har en deklaration enligt följande: Teori procedure Get(Item : out String); Denna procedur beter sig inte riktigt som Get för tecken. Denna Get ignorerar alla vagnreturer som dyker upp i inmatningsbufferten tills att den sträng som skall fyllas är fylld med skrivbara tecken. Om man anropar denna Get med en sträng som är 10 tecken lång kommer Get att läsa 10 tecken från bufferten exklusive de ignorerade vagnreturerna. Om användaren matar in för få tecken kommer alltså Get att be operativsystemet om nya data tills det att hela strängen fyllts med tecken. Efterföljande tecken ligger kvar till nästa inmatning precis som för Get för tecken. Speciellt gäller att den vagnretur som användaren avslutade med alltid finns kvar. Lite speciellt är att Get för strängar känner av hur lång den aktuella strängparametern är. Om det finns data i utmatningsbufferten skrivs dessa ut innan Get utför sin uppgift. 89
5.1.4 Proceduren Skip_Line Proceduren Skip_Line har en deklaration enligt följande: procedure Skip_Line(Spacing : in Positive_Count := 1); Proceduren Skip_Line används för att radera resten av en inmatad rad, d.v.s. alla tecken fram t.o.m. en vagnretur. Om inget finns i inmatningsbufferten kommer operativsystemet att fylla på med data och sedan kommer första raden att tas bort. Som man ser i parameterlistan har den formella parametern ett skönsvärde som gör att man inte behöver anropa proceduren med någon aktuell parameter om man endast vill ta bort en rad. Vill man ta bort flera rader på en gång skickar man bara med ett positivt heltal som anger antalet rader som skall tas bort. Om det finns data i utmatningsbufferten skrivs dessa ut innan Skip_Line utför sin uppgift. På samma sätt som för Get kommer End_Error att kastas om inmatningsbufferten är slut. 5.1.5 Proceduren Get_Line Proceduren Get_Line finns bara för strängar och har en deklaration enligt följande: procedure Get_Line(Item : out String; Last : out Natural); Denna procedur är lite speciell och beter sig lite olika i olika situationer. Man kan förenklat säga att denna procedur löser problemet med inmatningar av strängar som man inte vet exakt hur många tecken de består av. Man skickar in en sträng som är dimensionerad till det maximala antalet tecken man får mata in varefter Get_Line läser, från inmatningsbufferten,in så många tecken som det finns före första vagnreturen. 90
Inläsningen sker efter det att operativsystemet givit klartecken att det finns något i inmatningsbufferten. Förutom att skicka tillbaka den inlästa strängen skickas också, i princip, antalet tecken som lästes in och lagrades i strängen tillbaka programmet som anropande Get_Line. Detta via Last -parametern. Ser man mer i detalj på vad som händer,vilket man måste för att kunna hantera denna procedur på ett vettigt sätt, ser man att vagnreturen ibland tas bort av Get_Line och ibland inte. Dessutom kan det vara så att Last -parametern returnerar ett värde som är större än strängens längd. Först vagnreturproblemet. När raden som användaren matat in är kortare än den sträng som programmet skall lagra texten i tas vagnreturen bort från inmatningsbufferten. När det finns minst lika många tecken på den rad som matas in som strängen är lång lämnas de återstående tecknen och vagnreturen kvar i bufferten. Om man sedan tittar på vad som egentligen returneras i Last så är det den position på vilken Get_Line lagt det sista tecknet på i strängen. Det beror alltså på hur strängen är deklarerad i det anropande programmet vad positionerna har för nummer. Antag att man har en sträng som deklarerats enligt följande: Teori Str : String(1.. 10); I detta fall kommer Last att returnera samma position som längden av det antal tecken som matats in. Antar man istället att deklarationen ser ut på följande sätt: Str : String(2.. 11); Om användaren matar in 2 tecken på tangentbordet kommer dessa att lagras i position 2 och 3 i strängen och Last kommer att skicka ut en 3:a till den som anropar. Ytterligare en liten detalj är att den del av strängen som inte fylldes av Get_Line kommer att innehålla samma data som den innehöll innan anropet till Get_Line. 91
Observera också att en tom rad (med endast en vagnretur) kommer att resultera i att programmet får tillbaka samma sträng som det hade innan anropet. Parametern Last blir satt till värdet Item First - 1 (ett mindre än första index i strängen). Om strängen är definierad från index 1 kommer Last att returnera värdet 0 (noll). Dessutom tas det radslut som ligger i inmatningsbufferten bort. Om bufferten är markerad som slut kraschar programmet enligt tidigare beskrivningar för Get -procedurerna. 5.1.6 Proceduren Put (för tecken) Proceduren Put för tecken har en deklaration enligt följande: procedure Put(Item : in Character); Proceduren Put ber operativsystemet om att skriva ut ett tecken på skärmen. Detta tecken kan hamna i utmatningsbufferten i väntan på att skrivas ut. Nästa utmatning kommer på samma rad på skärmen som det tecken som just skrivits ut. Generellt är det så i Ada att Put betyder skriv ut datat utan att göra radbrytning efteråt. 5.1.7 Proceduren Put (för strängar) Proceduren Put för strängar har en deklaration enligt följande: procedure Put(Item : in String); Proceduren Put för strängar utför egentligen så många Put för tecken som det finns i strängen. Varje tecken skrivs ut enligt beskrivningen för teckenutmatningen. Observera att strängen kan vara godtyckligt lång. 92
5.1.8 Proceduren New_Line Proceduren New_Line har en deklaration enligt följande: procedure New_Line(Spacing : in Positive_Count := 1); När man vill sätta in en radbrytning i utskriften på skärmen använder man New_Line. Utan aktuell parameter görs en radbrytning och med aktuell parameter anger denna hur många radbrytningar som skall göras. New_Line ser också till att det som finns i utmatningsbufferten skrivs ut på skärmen. 5.1.9 Proceduren Put_Line Proceduren Put_Line finns bara för strängar och har en deklaration enligt följande: Teori procedure Put_Line(Item : in String); Proceduren Put_Line är en snabbvariant av att anropa först Put för strängen och sedan New_Line utan parameter. Put_Line ser också till att det som finns i utmatningsbufferten skrivs ut på skärmen. 5.1.10 Funktionen End_Of_Line Funktionen End_Of_Line har en deklaration enligt följande: function End_Of_Line return Boolean; Denna funktion används för att tjuvkika på (utan att läsa in) nästa tecken i tangentbordets inmatningsbuffert. Om nästa tecken är vagnretur (radslut) eller markeringen för filslut returnerar funktionen sanningsvärdet True, annars returneras False. 93
5.1.11 Funktionen End_Of_File Funktionen End_Of_File har en deklaration enligt följande: function End_Of_File return Boolean; Denna funktion används för att tjuvkika på tangentbordets inmatningsbuffert. Där kan man se om den är markerad som slut på inmatning (detta kallas för filslut ). Man kan på tangentbordet trycka en tangentkombination som anger att inmatningsbufferten sätts i ett läge så att man har slut på data. Detta kan man alltså detektera med hjälp av funktionen End_Of_File. Sanningsvärdet True returneras om bufferten är slut, annars returneras False. Ett specialfall är att End_Of_File returnerar True även i fallet att inmatningsbufferten innehåller ett radslut följt av filslut. 5.2 Biblioteket Ada.Float_Text_IO Detta är ett komplement till Ada.Text_IO som bara hanterar inoch utmatning av flyttal. Den generiska definitionen som ligger till grund för detta bibliotek finns egentligen i Ada.Text_IO, men då det är mycket vanligt att man använder inmatningar av flyttal finns detta bibliotek fördefinierat utifrån det generella. 5.2.1 Proceduren Get (för flyttal) Proceduren Get för flyttal har en deklaration enligt följande: procedure Get(Item : out Float; Width : in Field := 0); Proceduren läser ett flyttal från tangentbordsbufferten och returnerar detta via parametern Item. Inledande vita tecken (blanktecken, tabulatortecken, vagnreturer) ignoreras (läses bort). Om man skickar med ett heltal till Width -parametern 94
läses maximalt så många tecken även om det skulle kunna vara ett flyttal med fler tecken. Ett flyttal kan matas in på två format,antingen som ett decimaltal eller som ett exponentialtal. Exponentialtal matas in på som en heltalsdel, en punkt, decimaldel, bokstaven E eller e som markerar gånger 10 upphöjt till samt exponentdelen. Punkten och decimaldelen kan tas bort. Det är tillåtet att mata in ett heltal till Get för flyttal då proceduren även klarar detta specialfall. Om bufferten är markerad som slut kastas End_Error. Om det, efter de eventuella inledande vita tecknen, kommer något som inte går att tolka som ett flyttal kastas Data_Error. Ett exempel när detta inträffar är om användaren matar in bokstäver istället för siffror. Alla tecken efter det första flyttalet i inmatningsbufferten lämnas kvar. Om det finns data i utmatningsbufferten skrivs dessa ut innan Get utför sin uppgift. Teori 5.2.2 Proceduren Put (för flyttal) Proceduren Put för flyttal har en deklaration enligt följande: procedure Put(Item : in Float Fore : in Field := Default_Fore; Aft : in Field := Default_Aft; Exp : in Field := Default_Exp); Proceduren Put skriver ut ett flyttal på skärmen. Put kan skriva ut flyttalen på ett par olika format. Dels i exponentialform,dels i decimalform. Observera att punkt används istället för kommatecken som decimalavskiljare. Om man endast anropar Put med det flyttal som skall skrivas ut kommer skönsvärdena på formatbeskrivningsparametrarna att träda in och man får en utskrift i exponentialform. 95
De konstanter som står i parameterlistan, som skönsvärden, har följande värden. Default_Fore : Field := 2; Default_Aft : Field := Float'Digits - 1; Default_Exp : Field := 3; Dessa konstanter gör att man får ett exponentialtal med två siffror före decimalpunkten,så många decimaler som ett normalt flyttal kan ha, enligt Ada:s standard, samt tre exponentsiffror. Mellan decimal- och exponentdelen sätts bokstaven E in för att markera gånger 10 upphöjt till. Om man vill modifiera någon eller några av formatparametrarna skickar man bara in tal som motsvarar det man anser vara rimligt. Vill man t.ex. få en utskrift utan exponentdel skickar man in en nolla till Exp. I fallet att talet man skriver ut inte har så många siffror att det fyller upp antalet positioner som anges i Fore, högerjusteras utmatningen av heltalsdelen i så många positioner som är angivet. De inledande positionerna fylls med blanktecken. Är det istället så att Fore inte räcker till för att skriva ut heltalsdelen kommer denna att skrivas ut i så många positioner som behövs. Några exempel på anrop, som skriver ut ett flyttal som ligger i variabeln F i decimalform med 2 heltalspositioner och 4 decimaler, följer här. Put(F, Fore => 2, Aft => 4, Exp=> 0); Put(F, Aft => 4, Exp=> 0); Put(Item => F, Aft => 4, Exp=> 0); Put(F, 2, 4, 0); Put(F, Exp=> 0, Aft => 4, Fore => 2); Alla anrop är identiska, men de är mer eller mindre läsbara. En rekommendation är att använda den första varianten. Denna är lätt att förstå och ger nästa programmerare mindre arbete med att förstå vad programmet gör. Många tror att variant nummer fyra är att föredra då det går snabbare att skriva, just då man skriver 96
den programraden, men med mycket stor sannolikhet ställer den till det för den som senare skall modifiera programmet. 5.3 Biblioteket Ada.Integer_Text_IO Detta är ett komplement till Ada.Text_IO som endast hanterar in- och utmatning av heltal. Egentligen finns definitionen som ligger till grund för detta bibliotek inuti paketet Ada.Text_IO, men då det är mycket med heltalsinmatningar finns detta bibliotek fördefinierat utifrån det generella. 5.3.1 Proceduren Get (för heltal) Proceduren Get för heltal har en deklaration enligt följande: Teori procedure Get(Item : out Integer; Width : in Field := 0); Proceduren läser ett heltal från tangentbordsbufferten och returnerar detta via parametern Item. Inledande vita tecken (blanktecken, tabulatortecken, vagnreturer) ignoreras (läses bort). Om man skickar med ett heltal till Width -parametern läses maximalt så många tecken även om det skulle kunna vara ett heltal även med fler tecken. Ett heltal kan matas in på två format. Antingen som ett vanligt heltal eller som ett heltal i en annan talbas än 10. De talbaser som Ada klarar är 2, 3,..., 16. Inmatning i en annan talbas än 10 har följande format. Det är givetvis också tillåtet att mata in tal i basen 10 på detta sätt. talbas#tal# Det binära talet 1010101 skulle alltså matas in som 2#1010101#. Om bufferten är markerad som slut kastas End_Error. Om det efter de eventuella inledande vita tecknen kommer något som inte går att tolka som ett heltal kastas Data_Error. Ett exempel 97
när detta inträffar är om användaren matar in bokstäver istället för siffror. Om inmatningen ger ett heltal som inte går att lagra i heltalsvariabeln kastas Constraint_Error som i princip betyder utanför gränserna. Alla tecken efter det första heltalet i inmatningsbufferten lämnas kvar. Om det finns data i utmatningsbufferten skrivs dessa ut innan Get utför sin uppgift. 5.3.2 Proceduren Put (för heltal) Proceduren Put för heltal har en deklaration enligt följande: procedure Put(Item : in Integer; Width : in Field := Default_Width; Base : in Number_Base := Default_Base); Proceduren Put skriver ut ett heltal på skärmen. Put kan skriva ut heltal på ett par olika format. Dels som ett vanligt heltal eller som ett heltal i en annan talbas än 10. Se mer om detta i beskrivningen om Get för heltal. Om man endast anropar Put med det heltal som skall skrivas ut kommer skönsvärdena på formatparametrarna att träda in. De konstanter som står i parameterlistan som skönsvärden har följande värden. Default_Width : Field := Integer'Width; Default_Base : Number_Base := 10; Detta ger att man får ett vanligt heltal utskrivet högerjusterat i Default_Width antal positioner på skärmen. Om man vill modifiera någon eller några av formatparametrarna skickar man bara in tal som motsvarar det man anser vara rimligt. Vill man t.ex. få en utskrift som ett binärt heltal (på det format som angavs i beskrivningen av Get ) skickar man in en tvåa till Base. 98
Om talet man skriver ut inte har så många siffror att det fyller upp det antal positioner som anges i Width högerjusteras utmatningen av heltalsdelen i så många positioner som är angivet och de inledande positionerna fylls med blanktecken. Om det istället är så att Width inte räcker till för att skriva ut heltalet kommer heltalet att skrivas ut i så många positioner som behövs. Några exempel på anrop som skriver ut ett heltal, som ligger i variabeln I, i normal form med fem positioners utrymme på skärmen, följer här. Put(I, Width => 5); Put(I, 5); Put(Item => I, Width => 5); Put(Width => 5, Item => I); Alla anrop är identiska, men de är mer eller mindre läsbara. En rekommendation är att använda den första varianten. Denna är lätt att förstå och ger nästa programmerare mindre arbete med att förstå vad programmet gör. Många tror att variant nummer två är att föredra då det går snabbare att skriva,just då man skriver den programraden, men med mycket stor sannolikhet ställer den till det för den som senare skall modifiera programmet. Teori 99
100 Grundläggande in- och utmatning