Module:
ModuleDeclaration DeclDefs
DeclDefs
DeclDefs:
DeclDef
DeclDef DeclDefs
DeclDef:
AttributeSpecifier
ImportDeclaration
EnumDeclaration
ClassDeclaration
InterfaceDeclaration
AggregateDeclaration
Declaration
Constructor
Destructor
Invariant
UnitTest
StaticConstructor
StaticDestructor
DebugSpecification
VersionSpecification
MixinDeclaration
;
Megj: A fenti BNF egy teljes modul szerkezetét írja le. Ebben a fejezetben csak a modulokkal kapcsolatos deklarációkkal, importálással, valamint a statikus konstruktorokkal, destruktorokkal foglalkozunk. Látszik, hogy a ModuleDeclaration résszel kell kezdődni egy modulnak. Azonban a többi rész sorrendje szinte tetszőleges. A nem tárgyalt részegységeket ld. a megfelelő fejezetekben.
Minden modul egy forrásfájlnak felel meg. Azaz minden forrásfájl egy modul. A modul neve - ha nem adtuk meg (ld. később) - megegyezik a fájl nevével a kiterjesztést és az elérési utat elhagyva.
Egy modul automatikusan egy új névteret deklarál, és a modul tartalma ebben a névtérben lesz elérhető. A modulok látszólag nagyon hasonlítanak az osztályra, azonban nagyon sok különbség van köztük. Ezek:
A modulok viszont hierarchiába szervezhetőek, mégpedig a csomagok (packages) révén.
A modulokkal kapcsolatos szabályok a következőket biztosítják:
A ModuleDeclaration (BNF-ben) megadja a modul nevét, valamint azt, hogy melyik csomagba (package) tartozik. Ha a név hiányzik, akkor a tartalmazó fájl neve lesz kiterjesztés és elérési útvonal nélkül.
BNF:
ModuleDeclaration:
module ModuleName ;
ModuleName:
Identifier
ModuleName . Identifier
A legjobboldalibb Identifier (BNF) a modul neve (ezután már nincs pont). Az azt megelőző Identifier-ek azoknak a csomagoknak (packages) nevei, melyek az aktuális modult tartalmazzák. A csomagok nevei megfelelnek a könyvtárak neveinek, amelyek a modul elérési útvonalában vannak. A csomagnevek nem lehetnek foglalt a nyelv kulcsszavai, ezáltal a könyvtárnevek sem lehetnek kulcsszavak.
Ha meg van adva, a ModuleDeclaration (BNF) résszel kell kezdődni a fájlnak, és csak egy ilyen rész lehet.
Példa:
Konvenció szerint a csomag és modul nevek kisbetűsek. Ennek oka, hogy ezek egy az egyben megfelelnek a fájl és könyvtárneveknek, viszont bizonyos operációs rendszerek nem különböztetik meg a kis- és nagybetűt. Megj: A készítők szerint tartsuk be. Ennek oka a következő: az implementációk nem garantálják az ebből adódó problémák feloldását.
Másik modulból származó Szimbólumokat az ImportDeclaration (BNF) részben adjuk meg.
BNF:ImportDeclaration:
import ImportList ;
static import ImportList ;
ImportList:
Import
ImportBindings
Import , ImportList
Import:
ModuleName
ModuleAliasIdentifier = ModuleName
ImportBindings:
Import : ImportBindList
ImportBindList:
ImportBind
ImportBind , ImportBindList
ImportBind:
Identifier
Identifier = Identifier
ModuleAliasIdentifier:
Identifier
Az ImportDeclaration részt sokféleképpen adhatjuk meg: a teljesen általánostól az egészen részletesig.
Az ImportDeclarations részek megadásának sorrendje tetszőleges.
A ModuleName részeknek az ImportDeclaration-ban teljesen meghatározottnak kell lenniük. Ez a nyelv elvárása, gyakorlatban ez azt jelenti, hogy meg kell adnunk a csomago(ka)t (package), amely (ek)ben az általunk importálni kívánt modul megtalálható. A csomagok megadása abszolút (emlékeztető: ez egy útvonalnak felel meg a fájlrendszerben). Tehát nem számít, hogy melyik modulból importálunk, ugyanazokat a csomagokat kell felsorolni.
A legegyszerűbb módja az importálásnak az, ha egyszerűen felsoroljuk a modulokat, melyeket importálni szeretnénk:
Az egyszerű importálás a tehát a következőképpen működik. Először egy nevet az aktuális névtérben keres a rendszer. Ha itt nem találja, utána veszi csak figyelembe az importokat. Ha pontosan egyet talál a névből, akkor azt használja. Ha egynél több importban találja meg a nevet (vagy triviálisan, ha nem találja), akkor az hiba.
Alapértelmezésben minden import privát (private). Ez azt jelenti, hogy ha az A modul importálja B modult, és B pedig C-t, akkor az A-ban C neveit már nem veszi figyelembe a fordító. Viszont egy importot deklarálhatunk publikusnak is public, ekkor a C úgy fog látszani A-ból, mintha közvetlenül importáltuk volna.
Az egyszerű importálás kétségtelenül jól működik kevés modullal. De ha sok modul van, könnyen előfordulhatnak névütközések, sőt többnyire elő is fordulnak. Főleg olyan gyakori függvényneveknél, mint read, write stb. Egyik megoldás a statikus importálás használata. Statikus importálással ez a probléma egyszerűen (Megj:ez egy kicsit brute force megoldás.)kezelhető, ekkor minden nevet csak abszolút adhatunk meg. Azaz a csomagok és végül a modul neve ponttal elválasztva, és csak ez után a név. Pl:
Egy importnak akár adhatunk lokális nevet is, így kerülve el a névütközést. Ezt az alábbi módon lehet megtenni, és ezután már CSAK az új név használható:
Tanács: nagyon hosszú importneveknél jól jöhet, hasonlóan, mint egy typedef.
Szimbólumokat (azaz pl. függvény nevet) egyesével is importálhatunk és köthetjük az aktuális névtérhez:
Statikus importok nem vegyíthetőek a szelektív importokkal. Megj: Ennek nem is lenne értelme, mert importáljuk az egész modult statikusan, és akkor úgyis minden szimbólumra csak minősített névvel hivatkozhatnánk.
Ezt a kettőt viszont már kombinálhatjuk, a szintaxis (Megj: talán nem túl szerencsés az alábbi:
Megj: a fenti példából látszik, hogy egy szimbólum nem azonos az entitással, amire mutat. Tehát például egy függvénynek lehet több neve is. Képzeljük el úgy, mintha az összes függvény fel lenne sorolva egy táblázatban, sorszámozva, és a nevek ezekre az entitásokra mutatnak. A D nyelv persze nem így van megadva, ez a példa csak a megértést segíti.
A statikus konstruktor arra való, hogy még a main () függvény meghívása előtt inicializáljon egy modult, vagy egy osztályt. A statikus destruktorok pedig a main () visszatérése után futnak le. Általában rendszer-erőforrások elengedésére használjuk.
Csak statikus változókat érhetnek el, de pontosan ez a céljuk. Némely nyelvekben ezt implicite oldják meg:
Szerepelhet közvetlenül egy modul törzsében, vagy egy osztályban.
Egy modulban a statikus konstruktorból és destruktorból több is lehet. A statikus konstruktorok lexikális sorrendben futnak le, a statikus destruktorok fordított lexikális sorrendben. Ezt a lexikális sorrendet a modul, ill. osztály nevek adják.
A paraméterlista a konstruktornál és a destruktornál is üres. Nem is lenne értelme, mivel ezeket a függvényeket semmi sem hívhatja meg, ill. a futtatókörnyezet hívja meg őket.
Példa helyes és helytelen megadási módra:Körkörös függőségek megengedettek. Ha a körön belül több modulnak is van statikus konstruktora vagy destruktora, akkor futási idejű kivétel keletkezik.Megj: ha csak lehet, kerüljük a körkörös függőségeket, mert megnehezítik a karbantartást. Pl: lehet, hogy az egyik modulnak még nincs stat. Konstruktora, de egyik munkatársunk úgy gondolja, hogy legyen. Ez az egész projektre kiterjedő problémákat okozhat.
A D nyelvben alapvetően ugyanúgy lehet függvényeket definiálni, mint C/C++-ban.
Különbség, hogy lehetőség van függvényeket egymásba ágyazni. Ekkor a beágyazott függvény elérheti a befoglaló függvény változóit, valamint a további beágyazott függvényeket. Egy beágyazott függvény természetesen csak azon a függvényen belül használható, melyben a definíciója található, azon kívül nem látható.
Az alprogramok további speciális jellemzőit a következő alpontokban foglaljuk össze.
A D nyelvben alapvetően érték szerinti paraméterátadás van, amit a következő példa szemléltet:
Ahogy a fenti kódrészletből is látható, a függvény paraméterének megváltoztatása nem érzékelhető a függvényen kívül. Egyedül a hun függvényben lévő értékadás észlelhető az eredeti változón is, mivel a tömbök, a C-hez hasonlóan referencia szerint kerülnek átadásra. Pontosabban a tömbök eleve referenciák, és ezek kerülnek átadásra érték szerint. Így a referencia által hivatkozott memóriaterület megváltoztatása látható a függvényen kívül is.
A D nyelvben különböző módosítókkal határozhatjuk meg a paraméterátadás módját. Ezek a következők:
Lehetőség van nem csak a referencia-típusú, hanem az érték-típusú változókat is referencia szerint átadni, erre szolgál a ref módosító. Ekkor a paraméter in-out szemantikájú lesz. Az eredeti értéke elérhető a függvényben, és a függvényen belül megváltoztatott érték látható a függvényen kívül is.
Természetesen ilyen esetben csak balérték lehet az aktuális paraméter, így a következő függvényhívás fordítási hibát okoz:
A ref módosítót nem csak a formális paraméterek megadásánál, hanem a visszatérési érték típusának definiálásánál is használhatjuk. Ekkor a visszaadott érték is balérték lesz. Így megjelenhet olyan helyeken, ahol balértékre van szükség, például egy ref paraméter aktuális értékeként.
Az in módosítóval ellátott formális paraméterek bemenő szemantikájú paramétereket definiálnak. Ez azt jelenti, hogy a paramétert nem változtathatjuk meg a függvényen belül, az konstans érték lesz. Mivel biztosított, hogy az ilyen paramétereket a függvény csak olvashatja, nem szükséges másolatot készíteni róla a függvény számára, így gyorsabbá tehető a függvény meghívása.
Az adat módosítása elleni védelem tranzitív, tehát például egy tömb elemeit sem módosíthatjuk, ha a tömb in módosítóval ellátott paramétere a függvénynek.
Az out módosítóval definiált paraméterek kimenő szemantikával rendelkeznek, azaz ezeken keresztül a függvény nem jut információhoz, hanem információt juttat a külvilágba. Az ilyen paraméterek a függvény lokális változói, melyek ennek megfelelően az adott típus alapértékével inicializálódnak. A függvény végrehajtása során ezeket ugyanúgy lehet olvasni és írni, mint a többi változót. A függvény végén a paraméterben lévő érték bemásolódik a függvény aktuális paraméterébe. Éppen ezért az out módosítóval ellátott formális paramétereknek csak balérték feleltethető meg aktuális paraméterként.
Az kimenő paraméterek helyett általában használhatók lennének a ref módosítóval ellátott paraméterek is, azt figyelembe véve, hogy amíg a függvényen belül nem adunk értéket a paraméternek, addig ne olvassuk, mivel kintről származó információt tárolhat. Azonban a kód megértése szempontjából jobban kifejezi a programozói szándékot, ha minden esetben, amikor a paramétert csak kimenő szemantikával akarjuk használni, akkor out módosítóval lássuk el a ref helyett. Erre egy példa a következő függvény:
A scope módosítóval ellátott paramétereket nem lehet kivinni a hatókörből, azaz például nem rendelhetjük őket egy globális változóhoz. Hasznos lehet, ha egy alprogramnak paraméterként szeretnénk átadni egy erőforrást kezelő objektumot. Ekkor ha az alprogram kivinné a hatókörből az objektumot, bizonytalanná válna, hogy mikor hívódik meg a destruktor, azaz mikor szabadul fel az erőforrás. A scope módosító garantálja, hogy a függvényünk nem okoz erőforrás-szivárgást. (Persze ettől még az őt hívó függvény okozhat problémákat.)
Bemenő szemantikájú paraméterek esetén egy lehetséges módosító a lazy, mely hatására az adott paraméter kiértékelése lusta lesz. Ekkor a paraméter értékét csak olvasni lehet a függvényen belül. Akkor hasznos, ha a paramétert egy komplex számítás eredménye adja, melyre lehet, hogy nem is lesz szükség a függvényben. Ekkor a lustaság miatt a paraméter nem kerül kiértékelésre, csak ha valóban szükség lesz rá.
Egy egyszerű példa a következő: Adott egy naplózó függvény, mely a paraméterként kapott karakter-sorozatot kiírja egy fájlba, de ezt csak akkor kell megtennie, ha a globális logging változó értéke true. Ha a logging értéke false, akkor a szövegre igazából nincs is szükség. Ilyen esetben a következő módon adhatjuk meg a függvényt és egy hívását:
A lazy paraméterek helyett a fordító delegátokat készít, melyek az aktuális paramétert adják értékül, így azok értéke nem kerül meghatározásra a függvény hívásakor. (A delegátokról és alprogram-literálokról jelen fejezet következő részei tartalmaznak részletes ismertetést.) Az előző példából a következőt készíti a fordító a lazy módosító eltávolításakor:
Mivel a lusta paraméterekből delegátok készülnek, lehetőség van bonyolultabb módon is kihasználni ezt a paraméterátadási technikát:
Ebben az esetben a writef(x++) kifejezésből készül egy delegát, melyet count-szor fog meghívni a dotimes függvény. Így a fenti kódrészlet által generált kimenet a következő:
A függvények és delegátok közti különbség megértéséhez induljunk ki egy C++-beli példából. Adott egy globális függvény és egy osztály tagfüggvénye. A globális függvény végrehajtásához elegendő ismerni a függvény kódjának címét, hiszen a paraméterein kívül legfeljebb globális változókat ér el, de azok statikusak, így a kódban közvetlenül megjelennek. Egy osztály tagfüggvényét csak az osztály egy objektumára lehet végrehajtani, tehát beszéljünk egy objektum tagfüggvényéről. Itt nem elég az explicit paraméterek mellett a függvény címének ismerete, hiszen szükség van magára az objektumra is, mely a függvény implicit, this paramétere. Vagyis két mutatóra van szükség egy objektum tagfüggvényének tárolásához: a függvény kódját és az adott objektumot hivatkozó mutatókra. Ez utóbbi esetben beszélhetünk delegátról.
A D-ben a függvények egymásba ágyazhatók, és osztályok is beágyazhatók függvényekbe, így kicsit bonyolódik a helyzet. De az előbbi példát kicsit általánosítva adódik a függvény és delegát (delegate) fogalma.
Az olyan függvényeket, melyek vagy csak globális változókat használnak, vagy tiszta függvények, azaz a paramétereiken kívül nincs szükségük más változók értékére, nevezzük függvénynek. Azokat a függvényeket, melyeknek szükségük van a paraméterükön kívül egyéb dinamikus változók értékeire is, nevezzük delegátoknak. A delegátok lehetnek a C++-os példán bemutatott tagfüggvényeken kívül tipikusan olyan beágyazott függvények, melyek használják a befoglaló függvény lokális változóit.
A függvények és delegátok megkülönböztetése nem csak elméleti szempontok miatt szükséges, hanem, mint a példán is láttuk, tárolásuk különbözősége miatt gyakorlati alapjai is vannak. A függvényeket egyetlen mutató határozza meg, míg a delegátokhoz két mutató tárolása szükséges, a végrehajtandó utasításokra és a végrehajtáshoz szükséges környezetre hivatkozó mutatóé.
Az alprogramokra hivatkozó mutatókat a következő szintaxissal deklarálhatjuk, és használhatjuk:
Típuskényszerítéssel (castolással) a delegátokat értékül adhatjuk függvény mutatóknak, de ez futási idejű hibához fog vezetni, hacsak a vezérlés el nem kerüli a környezetből származó változókhoz való hozzáféréseket. Tehát jól átgondolt módon lehet élni ezzel a lehetőséggel. A függvényekből delegátot viszont nem lehet készíteni, az ilyen típuskényszerítés fordítási hibához vezet.
A fenti deklarációkban látható szintaxissal paraméterként is átadhatunk függvényeket és delegátokat, sőt visszatérési értékként is megjelenhetnek. Ilyen formán a D nyelv magasabb rendű függvényeket (higher-order functions) biztosít a programozók számára.
Az alprogram-literálokkal név nélküli alprogramokat definiálhatunk. Az előző pontban ismeretett megkülönböztetésnek megfelelően két típusuk létezik: függvény-literálok és delegát-literálok.
Az alprogram-literáloknak csak a paramétereit kell megadni, a visszatérési típusukat a fordító kikövetkezteti. A függvény-literálokat a function, a delegát-literálokat a delegate kulcssszó vezeti be, melyek el is maradhatnak. Ha az alprogram-literált a paraméterlistával vezetjük be, akkor a fordító delegátnak fogja tekinteni. Amennyiben kitesszük a function kulcsszót, a fordító ellenőrzi, hogy literál törzsének végrehajtása során valóban nincs szükség a literál környzetére.
A paraméterlistában megadhatók a paraméterek típusai, de teljesen el is hagyhatók. Utóbbi esetben a fordító a paraméterek használata alapján kikövetkezteti azok típusát.
Nézzük a következő példát a literálok használatára:
A fenti példában a második literált nem lehetne a function kulcsszóval bevezetni, mert a lokális z változó értékét is felhasználja.
Függvény- és delegát-literálok esetén elhagyható a bevezető kulcsszó, ahogy azt fentebb is említettük, de ezenfelül üres paraméterlista esetén még a paramétereklistát tartalmazó zárójelpárt sem kell kitenni. Ahhoz hasonlóan, hogy az üres paraméterlistával rendelkező függvény hívásakor sem kötelező kitenni az üres zárójelpárt.
Tehát a következő három literál ekvivalens:
Megjegyzendő, hogy függvény- és delegát-mutatóknak való értékadáskor az alprogram-literálok elé nem kell referencia-operátor (&), ahogy az a fenti példában is látható.
Ha az alprogram-literálunk törzse csak egy return utasításból áll, akkor használhatunk lambda kifejezést. A lambda kifejezésben megadjuk, hogy adott paraméterekhez milyen kifejezést rendelünk, így a return utasítást nem kell kiírnunk. Az alprogram-literálokhoz hasonlóan a paraméterek típusát a fordító kikövetkezteti, ha az egyértelmű.
A példában egy tömb elemeit növeljük a duplájára függvény-literál és lambda kifejezés segítségével.
Tiszta függvénynek az olyan függvényeket nevezzük, melyek matematikai értelemben vett függvények, azaz egy adott paraméterezésre mindig ugyanazt az eredményt adják. A D nyelvben a tiszta függvények: