A Digitalmars D programozási nyelv

Objektum-orientált programozás

Osztályok

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:

class Foo { ... tagok ... }

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:

class Foo { } var; //hibás

Ehelyett

:
class Foo { } Foo var;

Védelmi szintek

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.

private

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.

package

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.

protected

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.

public

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.

export

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.

Mező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:

class Abc { int property; void setProperty(int newproperty)     {     property = newproperty;     } int getProperty()     {     return property;     } }; Abc a; a.setProperty(3); int x = a.getProperty();

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:

class Abc {     int myprop;     void property(int newproperty)     {     myprop = newproperty;     } // beállító     int property()     {     return myprop;     } // lekérdező }

amelyeket a következőképpen használunk:

Abc a; a.property = 3; // ekvivalens az a.property(3) hívással int x = a.property; // ekvivalens az int x = a.property() hívással.

Í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.

Konstruktorok

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:

class Abc { int a; // a kezdeti értéke 0 long b = 7; // b kezdeti értéke 7 float f; // f kezdeti értéke NAN }

Konstruktorokat a this függvénynévvel lehet definiálni, és nincs visszatérési értékük:

class Foo { this(int x) // a Foo konstruktorának deklarációja { ... } this() { ... } }

Az ősosztály konstruktorát egy belőle származtatott osztályban a super-rel lehet meghívni a következőképpen:

class A { this(int y)     { } } class B : A { int j; this() {  ... super(3); // az alapkonstruktor: A.this(3) meghívása ... } }

Konstruktorok meghívhatnak más konstruktorokat ugyanabból az osztályból azért, hogy megosszák az általános inicializálást:

class C { int j; this() { ... } this(int i) { this(); j = 3; } }

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ő:

this() { }

ami impliciten generálódik.

Az objektumosztály konstruktora nagyon rugalmas, de néhány megszorítás érvényes rá:

  1. Illegális ha a konstruktorok kölcsönösen egymást hívják:
    this() { this(1); } this(int i) { this(); } // illegális, ciklikus konstruktor hívás
  2. Ha egy konstruktor megjelenik egy másik konstruktoron belül, akkor a meghatározott konstruktor pontosan egy konstruktort kell hogy meghívjon:
    this() { a || super(); } // illegális this() { this(1) || super(); } // ok this() { for (...) { super(); // illegális, belső ciklus } }
  3. Illegális impliciten vagy expliciten a this-re hivatkozni egy konstruktor hívást megelőzően.
  4. Konstruktor hívásokat nem lehet elhelyezni címkék után (azért, hogy egyszerű legyen ellenőrizni a goto utasítás előfeltételeit. ).

Egy objektumosztály egy példányát a NewExpressions: utasítással hozunk létre:

A a = new A(3);

A következő lépések hajtódnak végre:

  1. Megpróbál lefoglalni az objektum számára helyet a memóriában. Ha ez hibás, akkor null értéket ad vissza, és egy OutOfMemoryException kivételt dob. Így szükségtelenné válik a null-ra mutató referencia ellenőrzése.
  2. Az alapadatok inicializálódnak, a pointer hozzárendelődik a vtbl-hez. Ez biztosítja azt, hogy a konstruktorok teljesen átadódnak az objektumoknak. Ez a művelet ekvivalens a memcpy()-al, amikor egy meglévő objektumot rámásolunk egy újonnan allokált objektumra.
  3. Ha az osztályban van definiálva konstruktor, akkor a konstruktor a megfelelően behelyettesített argumentumlistával meghívódik.
  4. Ha az osztályinvariáns-ellenőrző be van kapcsolva, akkor ez meghívódik a konstruktor végén.

Destruktorok

A szemétgyűjtő meghívja a destruktor függvényt, amikor az objektum törlődik. Ennek szintaxisa:

class Foo { ... ~this() // a Foo destruktora { } }

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.

Statikus Konstruktorok

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:

class Foo { static int a = b + 1; static int b = a * 2; }

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.

class Foo { static int a; // 0 lesz az alapértelmezett kezdeti érték static int b = 1; static int c = b + a; // hiba, nem konstans inicializáló static this() // statikus konstruktor { a = b + 1; // a értéke 2-re állítódik b = a * 2; // b értéke 4-re } }

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.

Statikus Destruktorok

Egy statikus destruktort speciális statikus függvényként lehet defíniálni a static ~this() szintaxissal.

class Foo { static ~this() // statikus destruktor { } }

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.

Osztály Invariánsok

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:

class Date { int day; int hour; invariant() { assert(1 <= day && day <= 31); assert(0 <= hour && hour < 24); } }

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:

Date mydate; ... assert(mydate); // A Date osztály invariánsának teljesülését ellenőrzi

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.

Unit Tesztek

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:

unittest { ...test code... }

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:

class Sum { int add(int x, int y) { return x + y; } unittest { assert(add(3,4) == 7); assert(add(-2,0) == -2); } }

Osztályonként csak egy unittest függvényt lehet definiálni.

Interface-ek

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.

interface D { void foo(); } class A : D, D // hiba, dupla interface { }

Egy interface-t nem lehet példányosítani.

interface D { void foo(); } ... D d = new D(); // hiba, nem lehet létrehozni az interface egy példányát

Az interface tagfüggvényei a D 1.0-ban nem lehetnek implementálva, ahogy azt más nyelveknél is megszokhattuk.

interface D { void bar() { } // hiba, az implementáció nem megengedett (D 1.0) }

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.:

interface E { void foo(); } class A : E {} //hiba, a foo() nincs implementálva class B : E { int foo() { } // hiba, a foo()-t felüldefiniálná, de az int nem kovariáns típusa a void-nak } class C : E { void foo() { } // ok, biztosítja az implementációt } class D : E { abstract void foo(); //ok, az osztály absztrakt lesz, majd a leszármazott osztályok adják az implementációt }

Az interface-ben definiált függvényeket az implementáló osztály leszármazottai felüldefiniálhatják.

interface D { int foo(); } class A : D { int foo() { return 1; } } class B : A { int foo() { return 2; } } ... B b = new B(); b.foo(); // 2-t ad vissza D d = (D) b; // ok mivel B örökölte az A D-beli implementációját d.foo(); // 2-t ad vissza;

Az interface-eket újra lehet implementálni a származtatott osztályokban:

interface D { int foo(); } class A : D {  int foo() { return 1; } } class B : A, D { int foo() { return 2; } } ... B b = new B(); b.foo(); // 2-t ad vissza D d = (D) b; d.foo(); // 2-t ad vissza A a = (A) b; D d2 = (D) a; d2.foo(); // 2-t ad vissza

Egy újraimplementált interface az összes függvényét implementálni kell, azokat nem örökli a super class-tól.

interface D { int foo(); } class A : D { int foo() { return 1; } } class B : A, D { } // hiba, nincs implementálva a foo()

Non-Virtual Interface (NVI)

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.

interface Transmogrifier { void transmogrify(); void untransmogrify(); final void thereAndBack() { transmogrify(); untransmogrify(); } }

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.

interface Transmogrifier { //Client interface final void thereAndBack() { transmogrify(); untransmogrify(); } //Implementation interface private: void transmogrify(); void untransmogrify(); }

Í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ő:

class CardboardBox : Transmogrifier { override private void transmogrify() { ... } override private void untransmogrify() { doUntransmogrify(); } void doUntransmogrify() { ... } }

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.

Immutable objektumok, metódusok

Immutable minősítő


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:

immutable (int) forever = 27;

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:

alias immutable(int) StabIelnt; StableInt forever = 42;

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:

unittest{ immutable(int) forever = 42; auto andEver = forever; ++andEver; // Hiba! Egy immutable érték nem módosítható }

Tranzitivitás


Az immutable minősítővel bármilyen típust elláthatunk. Például:
struct Point { int x, y; } auto origin = immutable(Point)(0, 0);

Az immutable tulajdonság természetesen egy struct összes mezőire és objektumok minden egyes adattagjára egyaránt közvetítődik.
unittest{ auto anotherOrigin = immutable(Point)(1, 1); origin = anotherOrigin; // Hiba! origin.x = 1; // Hiba! origin.y = 1; // Hiba! }
Ennek megfelelően az alábbi szerződés teljesül:
static assert(is(typeof(origin.x) == immutable(int));

Most hogy a dolog egyre érdekesebbé vállnak nézzük az alábbi példát:
struct DataSample { int id; double[ ] payload; }

Ahogyan már a fenti példák esetén tapasztaltuk egy immutable ( DataSample) mezői nem módosíthatóak. De mit mondhatunk egy indirekt módosításról, amennyiben a payload egyik elemét szeretnénk módosítani?
unittest { auto ds = im utable(DataSample)(5, [ 1.0, 2.0 ]); ds[1] = 4.5; // ? }


Két válasz lehetséges:
a) Az immutable nem engedi a meződ értékének direkt módosítását, de megengedi a mezőkön keresztül indirekt módon elérhető értékek módosítását ( C++ -ban a const minősítő így viselkedik)
vagy
b) Az immutable tulajdonság tranzitív és nem engedélyezi a mezőkön keresztül elérhető értékek módosítását

A jó válasz a b) az immutable tulajdonság tranzitív.
Hol lehet hasznos az immutable –hoz kapcsolódó szigorúság?
a. Párhuzamosság esetén a szálak között megosztott immutable értékek esetén az immutable minősítő garantálja, hogy az érték nem változik, minden szál ugyanazt az értéket kapja meg.
b. Funkcionális programozás esetén az immutable tranzitív tulajdonsága megengedi a fordítónak, hogy ellenőrizze, hogy a funkcionális kódrészlet nem módosíthatja módosítani nem kívánt adatokat.

Típuskonstrukciók az immutable minősítő használatával


Az immutable minősítő használatakor a zárójelezés alkalmazásával komplex adatszerkezeteket típusok konstruálhatóak. Hasonlítsuk össze az alábbi két típust:
alias immutable(int[ ]) T1; alias immutable(int)[ ] T2;
Az első esetben az immutable az int tömbre vonatkozik, míg a második példában az immutable az int-re vonatkozik de a tömbre nem. Amennyiben nem használunk zárójeleket, akkor az immutable a teljes típusra vonatkozik és az első példával ekvivalens deklarációt kapunk:
alias immutable int[ ] T1;
A T1 típuskonstrukció alkalmazása nem nyújt túl sok lehetőséget, a T1 típusú tömbök inicializálás követően csak olvashatóak lesznek:
T1 a = [ 1, 3, 5 ]; T1 b = [ 2, 4 ]; a = b; // Hiba! a[0] = b[1]; // Hiba!
A második típuskonstrukció elegáns, több művelet lehetőségét biztosit:
T2 a = [ 1, 3, 5 ]; T2 b = [ 2, 4 ]; a = b; // Helyes a[0] = b[1]; // hiba! a ~= b; // Helyes

Immutable argumentumok és metódusok


Függvények szignatúrájában az immutable olykor rejtetten jelenik meg. Tekintsük az alábbi példát: string process(string input);
Ez egy tömör jelölése az alábbi deklarációnak:
immutable(char)[ ] process(immutable(char)[ ] input);

A hívó biztos lehet abban, hogy az input string karaktereit a függvény nem fogja megváltoztatni, az input értéke függvényhívás után ugyanaz, mint közvetlenül hívás előtt.
string sl = "piros alma"; string s2 = process(s1); assert(s1 == "piros alma"); // mindig teljesül

A process által visszatérített immutable(char)[ ] értéket a program kódja nem változtathatja meg, így az s2 értéke nem változik.
Egy struct vagy class szintén tartalmazhat immutable metódusokat. Tekintsük az alábbi példát:
class A { int[ ] fun(); // hagyományos metódus int[ ] gun() immutable; // csak immutable objetumok hívhatják immutable int[ ] hun(); // ugyanaz mint az előbbi }
Az A osztály harmadik metódusának szintaxisa első látásra furcsának látszik, és úgy tűnhet, hogy az int[ ]-re. Viszont ahhoz, hogy egy immutable int[ ]-t visszatérítő immutable metódust definiáljunk ehhez az alábbiak szerint kell eljárnunk:
immutable immutable(int[ ] ) iun();

A fenti deklaráció jobban olvasható az alábbi formában:
immutable(int[ ] ) iun ( ) immutable;

Ahhoz, hogy elkerüljük az immutable téves használatát, használhatunk egy egyértelműbb, könnyebben olvasható deklarációt:
class A { immutable { int foo(); int[] bar(); void baz(); } }

Ahogyan fentebb említettük az immutable metódusokat csak immutable objektumok hívhatják meg:
class C { void fun() { } void gun() immutable { } } unittest { auto cl = new C; auto c2 = new immutable(C); cl.fun(); // helyes c2.gun(); // helyes // más függvényhívás nem lehetséges }