I denne artikel vil jeg fortælle hvordan man laver en klasse (en støbeform til
objekter) og hvordan man kan parse en kommando ind i ord. Der er meget teori
om opbygningen af units og klasser i Delphi.
Denne artikel er en direkte fortsættelse af part 1 "Start med konsolprogrammer".
Hvis du ikke har læst den bør du gøre det nu.
En generisk (ikke genetisk :) kommando-parser er meget
praktisk og kan bruges til mere end bare en kommandofortolker! Et andet
eksempel på hvor sådan en kunne være brugbar ville være en IRC bot/klient (som
nærmest er en kommandofortolker som modtager kommandoer over netværket). Du kan
selv prøve at finde på andre steder sådan en kunne være brugbar.
Siden din første kommandofortolker er temmelig
lille og simpel kan det lige så godt betale sig at skrive den helt om hvis den
skal behandle kommandoer på en helt ny måde. Så før vi går videre med den nye
kommandofortolker, gem den gamle (hvis du vil) og lav endnu et nyt
konsolprogram. Denne gang skriver vi ikke nogen kode i hovedprogrammet endnu,
men vi vil i stedet lave en ny unit for bedre at holde styr på koden. Så når du
har dit nye, tomme konsolprogram, så vælg File -> New -> Unit. Den tomme unit skal se
sådan her ud:
unit Unit1;
interface
implementation
end.
Gem allerede nu det tomme projekt og den tomme
unit. Jeg vil foreslå at du kalder projektet for ”
fortolker2”. Unitten skal du
kalde ”
Kommandoparser”. Delphi ændrer
automatisk unittens navn (det efter
unit nøgleordet) til
Kommandoparser.
De to nøgleord
interface og
implementation
deler unitten op i to logiske dele som det er vigtigt at forstå forskellen på
og sammenhængen mellem. Al din egentlige kode (dvs. funktionskald, if-sætninger
og så videre) vil altid være i implementationen, det er det eneste sted der kan
være eksekverbar kode. Interfacet kan kun indeholde erklæringer (deklarationer)
af symboler/navne og ikke eksekverbar kode. Alting som er erklæret i interfacet
skal implementeres i implementationen i samme unit. Man kan dog også have
erklæringer i implementationen af sin unit, forskellen på om en erklæring
ligger i interfacet eller implementationen er, at alting som er erklæret i
interfacet kan ses og benyttes af andre units der har unitten i sin
uses
klausul, mens ting som er erklæret i implementationen er private for denne
unit. Jeg håber det var til at forstå, men ellers kommer det normalt hen ad
vejen.
Nu skal vi definere en
klasse. En klasse
er en ”skabelon” til et
objekt, som er en variabel som kan have metoder
og egenskaber med værdier. Et eksempel på objekter du allerede kender er forms
og kontroller. Disse klasser implementerer GUI funktionerne i Windows, altså
gør det lettere at arbejde med den grafiske side af Windows. Dette er et
glimrende eksempel på en af de mange anvendelser af klasser: indkapsling af
komplekse ting i nogen logisk opbyggede klasser som gør det nemt at arbejde
med, uden at den som bruger klasserne har behov for at vide hvordan de virker
”bag ved scenen”.
Det er til en vis grad også en indkapsling vi
skal lave med en kommando-parser klasse, vi skal indkapsle funktionerne til at parse
en kommando så man ikke behøver tænke nærmere over hvordan det rent teknisk
foregår når først klassen er erklæret og impålementeret.
Lad os starte med at erklære vores klasse.
Denne erklæring skal stå i interfacet fordi andre units gerne skulle kunne have
gavn af vores klasse.
type
TKommandoParser = class
private
FKommando: string;
FParametre: TStringList;
FKommandolinie: string;
function GetParameterCount: integer;
function GetParameter(Index: integer): string;
public
constructor Create(const AKommandolinie: string);
destructor Destroy; override;
function GetRange(Fra, Til: integer): string;
propertyKommando: string
read FKommando;
property Kommandolinie: string
read FKommandolinie;
property ParameterCount: integer
read GetParameterCount;
property Parameter[Index: integer]: string
read GetParameter;
end;
Første linie,
type, fortæller simpelthen
Delphi at vi skal til at erklære en eller flere datatyper. Den næste linie
definerer
TKommandoParser typen
som en klasse. (Det er god kodeskik at starte navnet på alle sine datatyper med
et stort T for at vise ”dette er en type”.) Denne klasse er delt ind i to dele,
private og
public. Alting som ligger under
private kan kun
ses af klassen selv og er altså skjult for alle som benytter klassen. Det som
ligger i
public kan alle derimod se og benytte sig af.
I den private sektion erklærer vi først tre
feltvariabler,
dvs. private datavariabler. (Navnene på feltvariabler bør altid starte med F.)
Efter de tre variabler erklærer vi to funktioner til at hente værdierne for to
egenskaber (properties).
I den offentlige (
public) sektion er der
først erklæret to specielle
metoder (metode er et fælles navn for
funktioner og procedurer i klasser),
Create og
Destroy. Dem
bruger man når man hhv. instantierer klassen (opretter et objekt af den klasse)
eller destruerer en instans (sletter det oprettede objekt). Efter Create og Destroy
er der erklæret en metode,
GetRange, som skal hente et antal parameter
som følger efter hinanden.
Derefter følger fire egenskaber. En egenskab (property)
ligner (når man bruger den) en variabel på de fleste punkter, men forskellen er
at man grundlæggende kan hente og tildele værdier på to måder, enten direkte
fra en feltvariabel eller vha. en metode i klassen. Egenskaber behøver ikke
både kunne læses og skrives, man kan godt lade være at give en metode til at
læse eller skrive, så bliver egenskaben read-only eller write-only.
I vores klasse er alle egenskaber read-only,
fordi der ikke er defineret en
write klausul for dem. De to første
egenskaber er helt standard read-only egenskaber som får deres værdi fra en
feltvariabel, som der er givet navnet på i
read klausulen.
ParameterCount
egenskaben har i stedet for en feltvariabel en privat metode til at hente
værdien. Den sidste egenskab er
indekseret, dvs. at den ligner en array
(tabel, liste). Indekserede egenskaber skal have en læse-metode som tager et
argument med det givne indeks.
Hvis du prøver at kompilere din kode nu vil
Delphi give en fejl, fordi ingen af de erklærede metoder i klassen er
implementeret. Hvis du har Delphi 4 eller nyere, og mindst en Professional
version kan du placere tekstmarkøren et sted i erklæringen af klassen og trykke
Ctrl+Shift+C. Så vil Delphi automatisk oprettet skeletter til implementationen
af alle metoder i klassen. (Pas på, ICQ bruger også den genvejstast! Hvis du
har ICQ bør du enten lukke det eller omdefinere den tast.)
Hvis du ikke har en version af Delphi med Class
Completion bliver du nødt til manuelt at skrive skeletterne til alle metoderne.
For at gøre det lettere dig har jeg lagt
unitten med
kodeskeletter til download.
Nu kan unitten næsten kompilere, bortset fra en
enkelt ting, Delphi klager over at den ikke kender
TStringList som vi bruger
i
FParameter feltet.
TStringList er defineret i
Classes
unitten, så den skal vi putte i vores
uses klausul. Vi vil også få brug
for ting fra
SysUtils unitten, så den kan vi tilføje på samme tid. Fordi
Classes er nødvendig allerede i interface-delen af vores unit skal vi
sætte
uses klausulen lige under
implementation nøgleordet, så det
kommer til at se således ud:
// ...
interface
uses Classes, SysUtils;
type
// ...
Nu kan vores unit endelig kompilere, så kan vi
begynde at fylde kode i alle metoderne. For at begynde et sted, kan vi tage constructoren
Create som et naturligt sted at starte. Den skal tildele en værdi til FKommandolinie
feltet, oprette et TStringList objekt til FParametre feltet, og parse
kommandolinien ind i kommando og parametre. Vi starter med de to første ting:
constructor TKommandoParser.Create(const AKommandolinie: string);
begin
FKommandolinie := Trim(Akommandolinie);
FParametre := TStringList.Create;
end;
Parseren skal fungere på følgende måde:
- Løb gennem alle tegnene i kommandolinien, for hvert tegn:
- Hvis det ikke er et mellemrum, tilføj tegnet i slutningen af en midlertidig string
- Ellers tilføj den midlertidige string til listen over parametre, og tøm den midlertidige string
- Hvis den midlertidige string ikke er tom (dvs. vi har en del af en parameter, men
der ikke er flere tegn i kommandolinien), tilføj den midlertidige string til
parameterlisten.
- Tag første parameter og gem som kommando, slet første parameter fra listen
Det kræver to variabler: en string og en
tæller-variabel (for at holde styr på hvor langt vi er kommet i
kommandolinien.)
var
tmp: string;
i: integer;
Her giver jeg bare resten af koden til parseren,
den kræver vist ikke yderligere forklaring. Koden skal ind lige før
end
i
Create metoden.
s := '';
for i := 1 to Length(FKommandolinie) do
if (FKommandolinie[i] = ' ') and (s <> '') then
begin
FParametre.Add(s);
s := '';
end
else
s := s + FKommandolinie[i];
if s <> '' then
FParametre.Add(s);
FKommando := LowerCase(FParametre[0]);
FParametre.Delete(0);
Nu til
Destroy. Der skal vi bare rydde
op efter os, i det her tilfælde er der kun vores
FParametre felt som
skal frigøres.
destructor TKommandoParser.Destroy;
begin
FParametre.Free;
inherited;
end;
Læg mærke til at
Free metoden som findes
i alle klasser, kalder
Destroy metoden, men først efter at have tjekket
af objektet ikke allerede er frigjort/slettet.
De sidste tre metoder er trivielle og forklarer
vist sig selv:
function TKommandoParser.GetParameter(Index: integer): string;
begin
Result := FParametre[Index];
end;
function TKommandoParser.GetParameterCount: integer;
begin
Result := FParametre.Count;
end;
function TKommandoParser.GetRange(Fra, Til: integer): string;
var
i: integer;
begin
Result := FParametre[Fra];
for i := Fra+1 to Til do
Result := Result + ' ' + FParametre[i];
end;
Nu hvor vi har en kommando-parser klasse kan vi gå videre
med selve fortolkeren. Meget af koden er den samme som i vores første
fortolker, forskellen kommer der hvor vi skal finde ud af hvilken kommando der
blev skrevet, og hvordan den skal udføres. Her kommer noget kode som skal stå i
projektfilen, ligesom i den første kommandofortolker:
var
kommandolinie: string;
kommando: TKommandoParser;
koerer: boolean;
begin
koerer := True;
Writeln('Velkommen til Kommandofortolker version 2.0');
while koerer do
begin
Write('>');
readln(kommandolinie);
kommandolinie := Trim(kommandolinie);
if kommandolinie <> '' then
begin
kommando := TKommandoParser.Create(kommandolinie);
koerer := UdfoerKommando(kommando);
kommando.Free;
end;
end;
end.
Det, der sker efter vi har tjekket at
kommandolinien ikke er tom, er at vi opretter et nyt
TKommandoParser
objekt med den indtastede kommandolinie.
Derefter kalder vi en funktion der hedder
UdfoerKommando (den kommer vi
til), og til sidst frigør/sletter vi kommandolinie-objektet.
Funktionen
UdfoerKommando skal tage et
TKommandoParser objekt som parameter,
og returnere om vores program stadig skal køre. (Dvs. den skal returnere
False
hvis det var en
quit kommando, og i alle andre tilfælde skal den
returnere
True.)
Lav endnu en ny unit og gem den som ”
Kommandoer”. UdfoerKommando funktionen
er centrum i denne unit, så vi starter med at erklære den:
function UdfoerKommando(AKommando: TKommandoParser): Boolean;
Fordi denne funktion bruger
TKommandoParser klassen skal vi have
Kommandoparser
unitten i vores
uses klausul. Vi skal også bruge SysUtils.
uses KommandoParser, SysUtils;
Delphi har ikke nogen funktion til automatisk
at indsætte kodeskeletter for normale procedurer/funktioner i implementationen,
ligesom det har for klasser, så vi bliver nødt til selv at lave kodeskelettet.
Det ser sådan ud:
function UdfoerKommando(AKommando: TKommandoParser): Boolean;
begin
end;
Nu kan vi gøre næsten som i første version,
bortset fra at koden bliver lettere at læse, og der er mindre redundans. Det
første vi skal sørge for er dog, at funktionen altid har et meningsfyldt
resultat, så det første vi gør i funktionen er at sætte Result til True (så
programmet ikke afslutter). Derefter kan vi bare lave en række if-sætninger der
tester kommandoen.
Det ser sådan ud:
function UdfoerKommando(AKommando: TKommandoParser): Boolean;
begin
end;
Nu kan vi gøre næsten som i første version,
bortset fra at koden bliver lettere at læse, og der er mindre redundans. Det
første vi skal sørge for er dog, at funktionen altid har et meningsfyldt
resultat, så det første vi gør i funktionen er at sætte Result til True (så
programmet ikke afslutter). Derefter kan vi bare lave en række if-sætninger der
tester kommandoen.
function UdfoerKommando(AKommando: TKommandoParser): Boolean;
begin
Result := True;
if AKommando.Kommando = 'quit' then
Result := False
else if AKommando.Kommando = 'info' then
Writeln('Kommandofortolker version 2.0')
else if AKommando.Kommando = 'help' then
Writeln('Kendte kommandoer: quit info help echo')
else if AKommando.Kommando = 'echo' then
Writeln(AKommando.GetRange(0, AKommando.ParameterCount-1))
else
Writeln('Ukendt kommando: ', AKommando.Kommando);
end;
Man kan dog godt se at selv denne metode kan
blive uoverskuelig når man får flere kommandoer, men der findes skal også andre
metoder, som jeg vil komme ind på en anden gang.
Delphis sæt af funktioner til konsolprogrammer er
desværre ret begrænset, så i det øjeblik snart du vil lidt mere end bare
læse/skrive tegn til skærmen (f.eks. tømme skærmen eller skrive oven på noget
gammel tekst) får du brug får et specielt library. Et sådant kan f.eks. være
Win32Crt som du kan downloade her:
http://dwp42.org/dwp/bazaar/snippets/.
Indtil videre må du selv finde ud af hvordan det virker, men der kommer måske
senere en artikel om det.