A D nyelv objektum-orientált programozást tesz lehetővé, amely az osztályokon alapul. Az osztályhierarchia gyökere az Object osztály, amely a minimális szintű működést definiálja, amit minden származtatott osztály örököl.
Az osztályok programozók által definiált típusok. A D nyelv támogatja a beágyazást, öröklődést és a polimorfizmust. A D osztályok támogatják az egyszerű öröklődési paradigmákat, kiegészítve az interface szolgáltatásokkal.
Egy osztály lehet exportált, ez azt jelenti, hogy a neve és az összes nem privát tagja látható lesz egy DLL vagy EXE kiterjesztésű file-ban.
Egy osztály deklarációja a következőképpen van definiálva:
ClassDeclaration:
class Identifier [SuperClass {, InterfaceClass }] ClassBody
SuperClass:
: Identifier
InterfaceClass:
Identifier
ClassBody:
{ Declarations }
Az osztályok a következőkből állnak:
Egy osztály definiálása:
Megjegyezzük, hogy a záró } után nincs ; az osztály definíciója után. Valamint nem lehet a következőképpen változót deklarálni:
Ehelyett
:A D nyelvben a védelmi (láthatósági) viszonyok nem osztály, hanem modul szinten szabályozottak. Egy modul egy fordítási egység, azaz az egy fájlban lévő definíciók egy modult alkotnak. A csomagok (package) pedig az egy könyvtárban található modulok összessége. Tehát a most következő láthatósági szinteket nem csak osztályokon belül, hanem struct-okon belül, vagy akár ezeken kívül, modul-szinten is használhatók az entitások láthatóságának szabályozására.
Ez szokatlan lehet, mivel az elterjedt programozási nyelvek a láthatósági szinteket az osztályok szintjén határozzák meg, így amikor két osztályt teszünk egy fájlba, akkor azok nem láthatják egymás privát változóit, függvényeit. Azonban a D tervezésekor azt az elvet követték, hogy az egy fájlba tett definíciók össze szoktak tartozni, így nyilván használják is egymást. Amennyiben azt szeretnénk, hogy egy osztály privát adattagjait és tagfüggvényeit ne lehessen egyetlen más osztályból sem elérni, akkor az adott osztály legyen az egyetlen definíció a modulban.
Az osztályokban a védelmi szintek megadására használható a C++ szintaxisa, azaz a láthatósági szint után kettőspontot téve a kövektező definíciók az adott láthatósággal rendelkeznek. De használható a Javában szokásos módszer is, amikor minden definíció elé beírjuk az adott entitás láthatóságát.
A D programozási nyelvben a következő védelmi (láthatósági) szintek használhatók.
A private láthatóságú entitások csak a modulon belül érhetők el. Használható class-on és struct-on belül, valamint modul-szinten egyaránt.
A package láthatóság a private-hez hasonlóan alkalmazható a definíciók módosítására. Eredménye, hogy a definíciót tartalmazó modul csomagjában lesz látható a jelölt entitás. Azaz minden olyan fájlban, amely azonos könyvtárban van a definíciót tartalmazó fájllal.
A protected láthatóságnak csak osztályok esetén van értelme, így csak class definíciójában használható. Az ilyen láthatósággal rendelkező entitások az osztályt tartalmazó modulon belül, valamint az osztályból származó osztályokon belül láthatók.
A public módosító szintén használható class és struct definíciójában, valamint modul-szinten is. A nyilvános láthatósággal rendelkező entitások bárhonnan láthatók a programon belül.
Fontos megjegyezni, hogy minden esetben ez az alapértelmezett láthatósága a D nyelvű programban definiált entitásoknak.
Az export láthatóság használható minden lehetséges szinten, eredménye még a public-nál is nagyobb hozzáférési jogokkal rendelkező entitás definiálása. Az exportált definíciók a programon kívülről is elérhetők lesznek. Ennek akkor van értelme, ha dinamikus könyvtárat szeretnénk készíteni D nyelven.
Sajnos a D fordító jelenleg még nem nyújt teljes támogatást az exportálási lehetőségek kihasználására, így nem készíthető vele dinamikus könyvtár, mely exportált függvényei kívülről is elérhetők.
Az osztály tagjaira mindig a . operátorral hivatkozunk, itt nincs :: vagy -> operátor, mint a C++-ban.
A D fordító átrendezheti a mezők sorrendjét egy osztályon belül, hogy optimálisan csomagolja be őket implementácó-definiált módon. Ennélfogva az igazított utasítások, anonymus struct-ok és union-ok nem megengedettek az osztályban, mert ezek adatrétegű szerkezetűek. Vegyük figyelembe, hogy a mezőknek olyannak kell lenniük, mint a lokális változóknak egy függvényen belül.
A C++-ban a megszokott gyakorlat, hogy a mezők definiálását a get és set függvényekkel együtt tesszük:
Ez eléggé sok gépelést igényel, ezenfelül olvashatatlanná teszi a kódot a sok getProperty() és setProperty() hívásokkal. A D-ben a lekérdezések és beállítások annak az elgondolásnak az előnyét tükrözik, hogy egy baloldali érték beállító, míg egy jobboldali érték egy lekérdező szerepet játszik:
amelyeket a következőképpen használunk:
Így a D-ben egy property-t úgy kezelhetünk, mint egy egyszerű mezőnevet. Egy property-t egyszerű mezőnévként indíthatunk, de ha később szükség lenne lekérdező és beállító függvényekre, akkor a kódot nem kell majd módosítani, csak az osztály definícióját.
Az összes osztály a super class-tól örököl. Ha ez nincs külön specifikálva, akkor az Object lesz a super class. Az Object a D öröklődési hierarchiájának a gyökere.
A tagok mindig a típusuk szerinti alapértelmezett kezdeti értéket kapják, ami rendszerint 0 integer és NAN lebegőpontos típusok esetén. Ez kiküszöböli azt a hibalehetőséget, hogy elmulasszuk inicializálni a konstruktor egy tagját. Az osztály definíciójában megadhatunk az alapértelmezettől eltérő kezdeti értéket is:
Konstruktorokat a this függvénynévvel lehet definiálni, és nincs visszatérési értékük:
Az ősosztály konstruktorát egy belőle származtatott osztályban a super-rel lehet meghívni a következőképpen:
Konstruktorok meghívhatnak más konstruktorokat ugyanabból az osztályból azért, hogy megosszák az általános inicializálást:
Ha a konstruktoron belül nincs this vagy super hívás, az ősosztálynak meg van konstruktora, akkor a super() hívás beépül a konstruktor elejére.
Ha egy osztálynak nincs konstruktora, de az ősosztályának van, akkor az alapértelmezett konstruktor alakja a következő:
ami impliciten generálódik.
Az objektumosztály konstruktora nagyon rugalmas, de néhány megszorítás érvényes rá:
Egy objektumosztály egy példányát a NewExpressions: utasítással hozunk létre:
A következő lépések hajtódnak végre:
A szemétgyűjtő meghívja a destruktor függvényt, amikor az objektum törlődik. Ennek szintaxisa:
Csak egy destruktor lehet osztályonként, a destruktornak nem lehet paramétere és nincsenek attribútumai. Mindig virtuális.
A destruktor el kell engedjen minden erőforrást, amit az objektum lefoglalt.
A program pontosan tudja informálni a szemétgyűjtőt arról, hogy egy objektumra már nem hivatkoznak (a delete kifejezéssel), ekkor a szemétgyűjtő azonnal meghívja a destruktort, és az objektum memóriahelyét felszabadítja. Garantált, hogy a destruktor nem hívódik meg kétszer.
Nincs mód arra, hogy a super destruktort expliciten meg lehessen hívni, automatikusan hívódik meg.
Egy statikus konstruktor úgy van definiálva, mint egy függvény, ami elvégzi az inicializálásokat mielőtt a main() függvény eljut az ellenőrzéshez. A statikus konstruktorokat arra használjuk, hogy inicializáljuk a statikus osztály tagjait olyan értékekkel, amelyeket fordítási időben nem lehet kiértékelni.
A statikus konstruktorok más nyelvekben impliciten vannak felépítve, a tagok inicializálását használva, amelyeket nem lehet fordítási időben kiértékelni. A gond ezzel az, hogy nincs megfelelő ellenőrzés alatt, amikor a kód végrehajtódik, például:
Mi lesz a és b értéke a végén, milyen sorrendben hajtódik végre az inicializálás, mi a és b értéke az inicializálás előtt, ez fordítási vagy futási hiba? Ez a zavar nem nyilvánvaló, ha egy inicializáló statikus vagy dinamikus is lehet.
A D ezt egyszerűen oldja meg. Minden tag inicializálónak egyértelműnek kell lennie a fordító számára még fordítási időben, így a kiértékelések sorrendje nem függ a tagok inicializálásától, és nem lehet olyan értéket olvasni, amely nem volt inicializálva. A dinamikus inicializálás egy statikus konstruktor által hajtódik végre, és egy speciális szintaxissal - static this() adjuk meg.
A static this() a kód elején hívódik meg, mielőtt a main() meghívódik. Ha normálisan lefut (nem dob kivételt), akkor a statikus destruktor hozzáadódik azon függvények listájához, amelyek a program terminálásakor hívódnak meg. A statikus konstruktoroknak üres a paraméterlistájuk.
Egy statikus destruktort speciális statikus függvényként lehet defíniálni a static ~this() szintaxissal.
Egy statikus destruktor a program terminálásakor hívódik meg, de csak akkor, ha a statikus konstruktor sikeresen lefutott. A statikus destruktoroknak üres a paraméterlistájuk. És fordított sorrendben hívódnak meg a statikus konstruktorok hívásához képest.
Az osztályinvariánsokat arra használjuk, hogy specifikáljuk egy osztály olyan jellemzőit, amelyeknek mindig teljesülniük kell (kivéve, amíg egy tag függvény fut). Például egy olyan osztály esetén, amely egy dátumot reprezentál, egy invariáns az lehet, hogy a napnak 1 és 31 között kell lennie, az órának meg 0 és 23 között:
Az osztályinvariáns egy szerződés, ami megmondja, hogy bizonyos állításoknak mindig teljesülniük kell. Az invariáns ellenőrzéseket a fordító a konstruktor végére és a destruktor elejére helyezi el, mivel a konstruktornak érvényes objektumot kell létrehoznia és a destruktornak érvényes objektumot kell megszüntetnie. Nyilvános (public) tagfüggvényeknél vagy külső függvényeknél a függvény elején és a végén is szükséges az invariáns ellenőrzés, hogy az interface-n keresztül csak érvényes objektumot adjunk és vegyünk át. Private és protected függvényeknél nincs ellenőrzés, ott megengedjük, hogy az objektum köztes állapotban legyen.
Akkor ellenőrződhet le az invariáns, mikor egy objektumot argumentumként megadunk egy assert() kifejezésnek, például:
Ha az invariáns hibás, egy InvariantException kivétel lép fel.
Az osztályinvariánsok öröklődhetnek. Amikor invariánssal rendelkező ősosztályból származtatunk másik osztályt, vagy a tagosztálynak van invariánsa, ezeket az ős invariánsokat mint a származtatott osztály invariánsának a részét kell ellenőrizni.
Osztályonként csak egy invariant() lehet.
A unit tesztek teszt-esetek sorozatából áll, amelynek segítségével le lehet ellenőrizni, hogy a program pontosan mőködik-e. Ideális esetben a unit teszteknek mindig le kell futniuk, valahányszor a programot lefordítjuk.
A D osztályoknak van egy speciális tagfüggvényük:
A test() függvényt a program minden osztályában a statikus inicializálás után, de még a main hívás előtt hívódik meg. Egy fordító vagy a szerkesztő eltávolítja a teszt kódot a végső felépítésben.
Például, megadjuk a Sum osztályt, amely 2 értéket ad össze:
Osztályonként csak egy unittest függvényt lehet definiálni.
InterfaceDeclaration:
interface Identifier InterfaceBody
interface Identifier : SuperInterfaces InterfaceBody
SuperInterfaces
Identifier
Identifier , SuperInterfaces
InterfaceBody:
{ DeclDefs }
Az interface-ek megadják azon függvényeknek a listáját, amelyeket implementálnia kell olyan osztályoknak, melyek az interface-től örökölnek.
Interface-ek nem származtathatók osztályból, csak más interface-ekből. Osztályok nem származhatnak egy interface-től kétszer.
Egy interface-t nem lehet példányosítani.
Az interface tagfüggvényei a D 1.0-ban nem lehetnek implementálva, ahogy azt más nyelveknél is megszokhattuk.
Az összes interface-től örökölt függvény meg kell jelenjen az iterface-t implementáló osztály definíciójában.:
Az interface-ben definiált függvényeket az implementáló osztály leszármazottai felüldefiniálhatják.
Az interface-eket újra lehet implementálni a származtatott osztályokban:
Egy újraimplementált interface az összes függvényét implementálni kell, azokat nem örökli a super class-tól.
Az interfészek eddig ismeretett tulajdonságai megegyeznek az interfészek általános ismérveivel. Azonban a D nyelv ennél többet megenged az interfészekben.
Egyrészt az interfészekben definiálhatók statikus adattagok, másrészt final függvények implementációi is megadhatók.
Az NVI-k akkor lehetnek hasznosak, amikor az interfészben definiált műveleteket valamilyen előre adott módon szeretnénk felhasználni. Például egy adott sorrendben végrehajtva van értelmük, azonban az egyes lépések az interfészt implementáló osztálytól függnek. Ez gyakorlatilag a sablon művelet tervminta megvalósítása. Definiálhatunk olyan végleges műveleteket, melyek bizonyos lépései absztraktak, így az implementáló osztálynak kell definiálnia azokat.
Ilyen formán az NVI-k gyakorlatilag absztrakt osztályok, azonban nagyon hasznos lehet az interfész-fogalom ilyen kiterjesztése, hiszen a D-ben megengedett egyszeres öröklődés mellett az absztrakt osztályok használata nagyon korlátozott. Hiszen ha a Transmogrifier absztrakt osztály lenne, akkor az implementálása esetén már nem lehetne más osztályból származtatni a megvalósító osztályt.
A fenti interfész esetén mindhárom definiált művelet publikus, azonban lehetséges, hogy csak a sablon műveletet szeretnénk kívülről elérhetővé tenni. A benne használt műveletek önmagukban nem értelmesek, vagy olyan állapothoz vezethetnek, melyek sértik az osztály invariánsát, így azokat el kell rejtenünk. Ekkor a következő interfészhez jutunk.
Így csak a thereAndBack() függvény használható, a privát részben lévő függvényeket még az implementáló osztályból sem érhetjük el, de persze definiálni kell azokat. Ezzel biztosítható, hogy csak az előre eltervezett módon hívódjanak meg a Non-Virtual Interface-ben deklarált absztrakt műveletek.
Az NVI-ben lévő privát metódusok megvalósítására a hivatalos példa a következő:
Mivel a felüldefiniált függvények láthatósága nem változtatható meg, ezért ki kell írni a private kulcsszót. Ekkor az interfész két metódusa nem elérhető, ezért ha valami olyat szeretnénk csinálni bennük, amit a megvalósító osztályon belül is el akarunk érni, akkor azt a tevékenységet egy külön metódusban kell megvalósítani, és azt meghívni az interfészből származó metódus implementációjában. Ahogy ez a doUntransmogrify() metódus esetén látható.
Az NVI-k ezen, privát metódusokat is tartalmazó elképzelése módszertani szempontból egy kérdéses dolognak tűnik, hiszen ha az interfész privát metódusának deklarálunk valamit, akkor azt hogyan valósíthatjuk meg egy leszármazott osztályban. Ezen lehetne vitatkozni, mindenesetre a D definíciója ezt tartalmazza. Azonban a fordító még nem igazán támogatja, aminek oka pontosan az, hogy az interfész privát részeit nem lehet elérni. Legalábbis a szerkesztő erre nem képes. A fordító elkészíti a tárgykódokat, de a program összeszerkesztése már nem sikerül, mert nem elérhetők az interfész privát metódusainak referenciái. Persze, ha egy modulba tesszük az interfészt és az implementációját, akkor ilyen probléma nincs a modulszintű védelem miatt, de ez nem különösebben érdekes eset.
Kicsit gyengébb, de hasonló, és legfőképp működő, védelmet azonban elérhetünk, ha az interfész absztrakt metódusait protected láthatósági szintűnek deklaráljuk. Ekkor a megvalósító osztályon kívülről azokat nem lehet majd elérni. Persze ekkor a megvalósítón belül tetszőlegesen használhatóak lesznek, de ha belegondolunk, ez annyira nagy problémát nem okozhat.
Egy immutable érték olyan mint egy kőbe véset felirat, inicializálást követően változatlan és módosíthatatlan marad a program teljes futási ideje alatt.
Egy minősített típus mintája <minősítő> (T), ahol a minősítő lehetséges változata az immutable, shared és const.
Példaként deklaráljunk egy immutable egészet:
Amennyiben a program kódjában megváltoztatunk egy immutable minősítővel ellátott változót ez fordítás idejű hibát eredményez.
A többi típushoz hasonlóan az immutable(int) szerepelhet alias deklarációban:
Az alias típuskonstrukció továbbítja a módosíthatatlanság tulajdonságot, így a StableInt típusú változó inicializálást követően nem lesznek módosíthatóak. Egy másoltat készítése egy immutable értékről szintén továbbítja a módosíthatatlanság tulajdonságot: