13 Het RECORD-datatype

Sleutelbegrippen: het gezamenlijk kunnen behandelen van bij elkaar behorende gegevens; records en record-velden; selectie van recordvelden; invoer/uitvoer van recordgegevens; de WITH .. DO .. –constructie; toekenning van een complete ‘recordvulling’; complexere record-structuren; rijen van records.

 
We hanteren in dit hoofdstuk de volgende paragraaf-indeling:

  1. Inleiding
  2. RECORD-variabelen en RECORD-types
  3. Rijen van RECORDs
  4. Oefenopdrachten

 

13.1 Inleiding

We hebben tot nog toe gewerkt met enkelvoudige gegevenstypen, zoals met INTEGER, REAL, CHAR en STRING, en (in het hoofdstuk over ‘rijen’) met samengestelde rij-typen ( ARRAY [ . . ] OF <type> ). In de praktijk leidt dit echter al heel snel tot ‘ongemakkelijke gevoelens’, omdat we volgens onze intuïtie vaak te krijgen met gegevens die op de een of andere manier toch echt bijelkaar behoren, maar die we tot nu toe steeds apart (lees: in aparte variabelen [vaak: van verschillend type]) moesten opbergen.

Stel je bijvoorbeeld van een persoon (een verenigingslid) voor, dat je van die persoon wilt registreren: de (achter-) naam en de voornaam (beiden een String), de leeftijd (een geheel getal), en het gewicht (decimaal: een Real). Tot nu toe moesten we dat schematisch dan als volgt doen met 4 losse variabelen (er zijn al voorbeeld-waarden ingevuld):

Als we van alle leden van een vereniging op zo’n manier gegevens zouden moeten bijhouden, betekent dat waarschijnlijk: werken met twee String-rijen (een Namen- en een Voornamen-rij) [óf een twéé-dimensionale String-rij], een (Integer) Leeftijden-rij en een (Real) Gewichtenrij. Als we dan bijvoorbeeld de gegevens willen sorteren op naam, betekent dat dat we tegelijk ook gegevens in die voornamenrij, in de leeftijdenrij en in die bedragenrij op een gelijksoortige wijze moeten gaan aanpassen, want we willen natuurlijk niet dat onze ‘Klaas Jansen’ plotseling een heel andere leeftijdswaarde toebedeeld krijgt, omdat ergens bij het gelijktijdig sorteren iets fout is gegaan . . ..

En dan hebben we het hiervoor over een praktisch aspect [waardoor gemakkelijk iets fout kan gaan].

Veel belangrijker is echter, dat we begripsmatig vinden, dat een naam, een leeftijd en zo’n gewicht bij één persoon behoren en dat daarom die combinatie van persoonsgegevens in één keer aangesproken moet kunnen worden. We zouden dus willen werken met een cluster van de volgende structuur (met daarin weer dezelfde voorbeeldwaarden reeds ingevuld):

In [de meeste 3e generatie] programmeertalen zoals Pascal is deze gewenste mogelijkheid voor het zelf definiëren van een samengesteld datatype aanwezig (onder namen als ‘record’ of ‘struct’ e.d.).

 

13.2 RECORD-variabelen en RECORD-types

In Pascal gebruiken we het gereserveerde woord ‘RECORD’ voor [het definiëren van] een uit meerdere gegevensvelden bestaand gegevenstype. Dat ‘samenstellen’ zal over het algemeen zodanig zijn dat ‘natuurlijk’ bijelkaar behorende gegevens inderdaad in één gegevenscluster worden geplaatst. Hoe precies zo’n gegevenscluster zal zijn opgebouwd [lees: uit welke ‘velden’] hangt af van het beschouwde probleemdomein.
Zo zou je (nogmaals: afhankelijk van het precieze probleem) voor het werken met ‘studentgegevens’ de volgende bij een student behorende gegevens bijelkaar in zo’n recordstructuur kunnen plaatsen: studentnummer, voorletters, roepnaam, achternaam, straat, plaats, postcode en geboortedatum.
Verderop in dit hoofdstuk geven we meer concrete voorbeelden van Record-structuren.

Voor het werken met variabelen die zo’n recordstructuur hebben, bestaan 2 mogelijkheden:

  1. We kunnen rechtstreeks variabelen met een bepaalde (Record-) structuur definiëren.
    Voor de in de inleiding besproken situatie van zo’n verenigingslid zou dat kunnen zijn:
    1. VAR Persoon : RECORD

              Naam, Voornaam : STRING[10] ;
              Leeftijd : INTEGER ;
              Gewicht : REAL
            END ;

    Uit deze declaratie valt goed af te lezen, dat de verschillende ‘velden’ van deze samengestelde recordstructuur respectievelijk als veldnamen hebben: ‘Naam’, ‘Voornaam’ (beiden van het String[10]-type), ‘Leeftijd’ (van het Integer-type) en ‘Gewicht’ (van het Real-type).

  2. Vaker zullen we echter vantevoren een nieuw Record-gegevenstype definiëren en pas daarna een variabele van dat recordtype declareren. Algemeen geldt als syntax voor een RECORD-definitie:

      TYPE <identifier > = RECORD

              <lijst van veldnamen + datatype >
            END ;

In het besproken geval kunnen we dit als volgt toepassen:

      TYPE LID = RECORD

              Naam, Voornaam: STRING[10] ;
              Leeftijd : INTEGER ;
              Gewicht : REAL
            END ;
    om vervolgens van dit recordtype een variabele te declareren:

      VAR Persoon : LID ;

    De belangrijkste opmerking bij het voorgaande is waarschijnlijk, dat alle ‘velden’ van een recordstructuur geplaatst moeten worden tussen de gereserveerde woorden ‘RECORD’ en ‘END’. Van ieder veld dient na de veldnaam (na een ':') het type te worden vermeld.

    Binnen zo’n recordcluster worden de verschillende onderdelen op ‘hun eigen manier’ opgeslagen (dus bijvoorbeeld het Gewicht wordt via de Real-codering opgeslagen; dat is een codering die het meest efficiënt is als met Real-waarden gerekend moet worden).

    N.B. De totale omvang van zo’n LID-record is 2x(10 + 1[= het nulde Byte van een Pascal-String, waar in opgeslagen is tot hoe ver de String zinvol gevuld is]) Bytes voor de twee STRING[10]’s + 2 Byte voor een Leeftijd-INTEGER + 4 Bytes voor een Gewicht-REAL, dus in totaal 28 Bytes.

 

13.2.1 Selectie van een enkel gegevensveld uit een recordstructuur (via ‘.’)

Als we daarna van zo’n recordvariabele een bepaald ‘veld’ willen aanspreken (hetzij om er een waarde in te stoppen, hetzij om op te vragen welke waarde erin zit), dan moeten we dat veld ‘selecteren’ door gebruik te maken van de punt (dus: ‘.’) als selector-symbool. Als we bijvoorbeeld het  Voornaam-veld van de hiervoor genoemde variabele ‘Persoon’ willen selecteren, dan kan dat via   Persoon.Voornaam.

 

13.2.2 Invoer/uitvoer van recordgegevens via toetsenbord/beeldscherm

Het is nìet mogelijk om alle gegevens van een recordstructuur in één klap via het toetsenbord in te lezen [er is dus dus géén Readln ( recordvariabele ) mogelijk] en ook kunnen we een gehele recordinhoud nìet in één keer naar het beeldscherm schrijven [er is dus ook géén Write[ln] ( recordvariabele ) mogelijk]. Deze invoer/uitvoer zal per gegevensveld (dus steeds apart) moeten gebeuren.

Indien we bijvoorbeeld van een [hiervoor besproken] persoon de bijbehorende gegevens over naam, voornaam, leeftijd en gewicht via het toetsenbord willen inlezen, dan kunnen we dat als volgt in een programma-deel implementeren (hier doen we dit in procedure-vorm):

    PROCEDURE Voer_Lidgegevens_via_Toetsenbord_in ( VAR Persoon : LID ) ;

    BEGIN

      Write ( 'Naam : ');
      Readln ( Persoon.Naam );
      Write ( 'Voornaam: ' );
      Readln ( Persoon.Voornaam );
      Write ( 'Leeftijd: ' );
      Readln ( Persoon.Leeftijd );
      Write ( 'Gewicht : ' );
      Readln ( Persoon.Gewicht )
    END ;

Een voorbeeld van het tonen van recordgegevens op het beeldscherm is:

    Writeln ( ‘De totale naam is: ‘ , Persoon.Voornaam
              + ‘ ‘ + Persoon.Naam );
N.B. Dit geheel van ‘partiële’ invoer/uitvoer van recordvariabele-gegevens geldt ook voor invoer/uitvoer van of naar TEXT-bestanden.

 

13.2.3 De WITH . . . DO . . . -constructie

Een in Pascal veel gebruikte constructie bij het werken met records is die met de WITH -constructie:

      WITH ... (recordvariabelenaam) DO (samengestelde) opdracht

Bij gebruik van deze ‘WITH’-constructie kun je, na het specificeren van de variabelenaam, via het noemen van alleen de veldnaam de afzonderlijke record-onderdelen benaderen.

De ‘WITH’-variant op het eerder gegeven programmadeel-voorbeeld is:

    PROCEDURE Voer_Lidgegevens_via_Toetsenbord_in ( VAR Persoon : LID ) ;

    BEGIN

      WITH Persoon DO
      BEGIN
        Write ( 'Naam : ' ) ;
        Readln ( Naam ) ;
        Write ( 'Voornaam: ' ) ;
        Readln ( Voornaam ) ;
        Write ( 'Leeftijd: ' ) ;
        Readln ( Leeftijd ) ;
        Write ( 'Gewicht : ' ) ;
        Readln ( Gewicht )
      END { WITH }
    END ;

Ná de uitdrukking  ‘WITH persoon DO’ kan  ‘Persoon.Naam’ kortweg via alleen  ‘Naam’ worden aangeduid. Evenzo staat nu  ‘Gewicht’ ter afkorting van  ‘Persoon.Gewicht’.

Het gebruik van de  WITH-constructie maakt je programma niet alleen leesbaarder; de kans op (tik)fouten wordt ook flink kleiner.

 

13.2.4 Het gebruik van recordvariabelen als één gegevenscluster

Mocht je door het voorgaande [over het veld voor veld waarden toekennen/opvragen] het idee hebben gekregen, dat het werken met records omslachtig is, dan kunnen we je gerust stellen: heel handig is de mogelijkheid om het gehele cluster van gegevens binnen een record via een toekenning te kopiëren naar een ander record (uiteraard is dit alleen mogelijk bij records van hetzelfde type).
Kortom: als je bijvoorbeeld via de volgende aanroep van de hiervoor gegeven procedure:

    Voer_Lidgegevens_via_Toetsenbord_in ( Persoon1 )

waarden laat invoeren voor alle velden van het Persoon1-lid-record, dan is het daarna mogelijk om via de toekenning:  Persoon2 := Persoon1 alle voor  ‘Persoon1’ ingevoerde waarden zo ook te plaatsen in het gehele  Persoon2-record.

Schematisch:

Nu zal het voorgaande niet zo direct erg zinvol kunnen worden toegepast, maar het wisselen van de inhoud van twee recordvariabelen via een ‘hulprecordvariabele’ kan dit aspect, van toekenning van de [gehele] inhoud van een recordvariabele aan een andere recordvariabele, heel zinvol zijn:

    PROCEDURE Verwissel ( VAR Persoon1, Persoon2 : LID ) ;

    VAR Hulprecord : LID ;
    BEGIN

      Hulprecord := Persoon1 ;
      Persoon1   := Persoon2 ;
      Persoon2   := Hulprecord
    END ;

Binnen deze Verwissel-procedure worden record-inhouden in één keer gekopieerd in een ander record.
Deze Verwissel-procedure kunnen we als volgt ergens in een programmadeel (b.v. als we willen sorteren) aan roepen:

    IF Lid1.Naam < Lid2.Naam THEN Verwissel ( Lid1, Lid2 ) ;

Ook bij het werken met rijen-van-records en met getypeerde bestanden (komt allebei nog . . .) kan dit ‘in één klap toekennen’ erg handig en vooral inzichtelijk worden toegepast.

 

13.2.5 Complexere record-structuren

Het is ook mogelijk om bij het definiëren van een recordstructuur gebruik te maken van andere dan de bekende standaard-datatypen. Binnen een record-structuur kunnen we bijvoorbeeld ook een andere record-structuur en/of een rij opnemen.
Als voorbeeld geven we een probleemgebied, waarbij we behalve de naam- en gewichtsgegevens van leden ook nog de -samengestelde- geboortedatum en een aantal betaalde bedragen (Real’s) willen opnemen. Dat aantal betalingen kan van géén tot maximaal 10 gaan. (N.B. het veld ‘Leeftijd’ laten we nu weg, omdat dat afleidbaar is (verschil tussen de huidige datum en de op te slaan geboortedatum.)
We zouden nu als volgt een [beter: twee] recordstructu(u)r(en) kunnen definiëren:

TYPE DATUM = RECORD

          Dag   : 1 .. 31 ;
          Maand : 1 .. 12 ;
          Jaar  : 1800 .. 2100 ;
        END ;

    NAAMSTRING = STRING [ 10 ] ;

    LID = RECORD

        Naam, Voornaam : NAAMSTRING;
        Gewicht        : REAL ;
        Geboortedatum  : DATUM ;
        Aantal_Betalingen : 0 .. 10 ;
        Betalingen     : ARRAY [ 1 .. 10 ] OF REAL
        END ;

Zo’n recordstructuur kunnen we ons schematisch als volgt voorstellen:

 

Let op de wijze waarop we de ‘sub’-velden moeten aanduiden; bijvoorbeeld:

Als voorbeeld geven we hier een mogelijke invulling voor een procedure Toon_Lidgegevens:

procedure Toon_Lidgegevens ( Persoon : LID ) ;

VAR Index : INTEGER ;

BEGIN


    WITH Persoon DO

    BEGIN
      Write ( Voornaam + ‘ ‘ + Naam, Gewicht:5:1, ‘ kilo’’s ’);
      WITH Geboortedatum DO
        Writeln ( ‘ Geboren op: ‘ , Dag, ‘-‘, Maand, ‘-‘, Jaar);

      IF Aantal_Betalingen > 0 THEN
      BEGIN
        Write ( ‘Ontvangen betalingen: ‘, Aantal_Betalingen, ‘x : ’ ) ;
        FOR Index := 1 TO Aantal_Betalingen DO
            Write( ‘ Hfl. ‘, Betalingen [Index]:5:2 ) ;
      END ;
    END ;

    Writeln

END ;

Let vooral op de wijze waarop de afzonderlijke onderdelen van de geboortedatum en de afzonderlijke betalingen worden aangeduid.
 

13.2.6 Het 'Variant'-record

Ter informatie (maar niet om in deze cursus te gebruiken): Het kan in de praktijk voorkomen, dat men twee (of meer) sterk aan elkaar verwante datastructuren heeft en de gegevens van die min of meer vergelijkbare entiteiten in één recordstructuur wil opbergen. Pascal biedt deze mogelijkheid via het definiëren van een zogenaamd 'Variant'-record.
Stel je bijvoorbeeld voor (het voorbeeld is ontleend aan 'Savitch'), dat literatuurverwijzingen bijgehouden moeten worden, waarbij zowel naar boeken als naar in een tijdschrift gepubliceerde artikelen moet kunnen worden verwezen. In beide gevallen zul je auteur en titel en het jaar van uitgave willen vermelden en daarnaast voor boeken en artikelen verschillende gegevens (voor boeken tevens: uitgever en plaats van uitgave en voor artikelen: het tijdschrift, het 'volume-nummer' en het paginanummer van de eerste en de laatste bladzijde).

De definitie van het bijbehorende 'variant'-record kan dan zijn:

TYPE VERSCHIJNINGSVORM = ( Boek, Artikel ) ;

PUBLIKATIE = RECORD

Auteur, Titel :   STRING [20] ;
Jaartal :   1900 . . 2100 ;

CASE Soort : VERSCHIJNINGSVORM OF

Boek: ( Uitgever, Plaats : STRING[15] ) ;
Artikel: ( Tijdschrift : STRING[20] ;     Volume, EerstePag, LaatstePag : INTEGER )
  END ;
Voor een record van zo'n 'variant'-type wordt dan die hoeveelheid geheugenruimte gereserveerd, die overeenkomt met de maximaal benodigde hoeveelheid voor de diverse 'varianten'.

Voor het gegeven voorbeeld is dat als volgt (waarbij de veldlengte in Bytes niet helemaal 'natuurgetrouw' is weergegeven):

 

13.3 Rijen van RECORDs

Net zoals we rijen van standaard-datatypen kunnen maken (via bijvoorbeeld  ARRAY [1 . . 10 ] OF INTEGER)  kunnen we nu ook rijen van zelfgedefinieerde Record-structuren maken.
Als we bijvoorbeeld uitgaan van de eerder in dit hoofdstuk gedefinieerde LID-recordstructuur, dan kunnen we als volgt eerst een rijtype ‘TLEDENRIJ’ voor maximaal 50 LID-records definiëren en vervolgens 2 variabelen van dat TLEDENRIJ-type:

    CONST Max_Aantal = 50 ;

    TYPE  NAAMSTRING = STRING [ 10 ] ;

    LID = RECORD

        Naam, Voornaam: NAAMSTRING ;
        Leeftijd      : INTEGER ;
        Gewicht       : REAL
        END;

        TLEDENRIJ = ARRAY [ 1 .. Max_Aantal ] OF LID ;

    VAR Ledenrij, Hulprij : TLEDENRIJ ;

Zo’n zelfgemaakt datatype als ‘TLEDENRIJ’ kan daarna zonder problemen gebruikt worden als parameter-type in een procedure-header.

Schematisch:

 

De afzonderlijke rij-elementen, die via een indexering aan te geven zijn in zo’n ledenrij, kunnen we als gewone LID-records beschouwen en behandelen. Kortom: in voorgaande figuur is Rij [2] van het Lid-recordtype en levert een selectie via  Rij [2].Voornaam  in dit gevaal de waarde ‘Janny’ op.

Als voorbeeld geven we hier de uitwerking van een procedure  ‘Toon_Rij_inhoud ( Aantal : INTEGER ; Rij : TLEDENRIJ )’ waarmee we van een  TLEDENRIJ die met ‘Aantal’ LID-records [van voren af aan] gevuld is, op het scherm kan worden getoond:

    PROCEDURE Toon_Lidgegevens ( Persoon : LID ) ;

    BEGIN

      WITH Persoon DO
        Writeln ( Voornaam + ‘ ‘ + Naam, ‘ Oud: ‘, Leeftijd,
            ‘ jaren ;‘, Gewicht:5:1, ‘ kilo’’s ’ )
    END ;

     

    PROCEDURE Toon_Rij_inhoud ( Aantal : INTEGER; Rij : TLEDENRIJ );

    VAR Teller : INTEGER ;

    BEGIN

      FOR Teller := 1 TO Aantal DO
        Toon_Lidgegevens ( Rij [ Teller ] )
    END ;

We zien hier duidelijk dat binnen die procedure  ‘Toon_Rij_inhoud’ via de aanroep  Toon_Lidgegevens ( Rij [ Teller ] ) telkens de inhoud van één afzonderlijk LID-record getoond wordt.
Ook is het mogelijk om aan een bepaald element uit de ledenrij de inhoud van een reeds gevuld ‘los’ LID-record toe te wijzen.

Een ietwat onzinnig voorbeeld, waarbij we de rij voor de eerste helft volstoppen met  LID-records met dezelfde inhoud en daarna de gehele rij-inhoud [voor zover gevuld] tonen, hem vervolgens kopiëren in een andere rij (de al eerder gedeclareerde ‘Hulprij’) en ook die daarna tonen, is:

    VAR Persoon : LID ;

      Index, Halverwege : INTEGER ;

    BEGIN

      Voer_Lidgegevens_via_Toetsenbord_in (Persoon ) ;
      Halverwege := Max_Aantal DIV 2 ;       { de halve rij }

      FOR Index := 1 TO Halverwege DO Ledenrij [ Index ] := Persoon ;
      Toon_Rij_inhoud
      ( Halverwege, Ledenrij ) ;

      Hulprij := Ledenrij ;     { in één klap rij-inhoud kopiëren }
      Toon_Rij_inhoud (Halverwege, Hulprij )

    END.

 

13.4 Oefenopdrachten

  1. Verwoord weer de betekenis van alle in het begin van dit hoofstuk aangegeven sleutelbegrippen. Pas ook elk sleutelbegrip toe in een concreet voorbeeld.
  2. Definieer als type een recordstructuur die geschikt is om gegevens van de leden van een door jouzelf bepaalde organisatie in op te slaan. Definieer daarna bovendien het datatype voor een rij waarin de gegevens van maximaal 50 leden van die organisatie kan worden opgeslagen.

 
Zie ook de bijlage over
Sorteren versus indexeren van een rij van Records