A Smalltalk programozási nyelv

Osztályok és objektumok

Bevezetés

A Smalltalk teljesen objektumorientált. Ez azt jelenti, hogy minden objektumnak tekinthető: egy ablak, egy program, sőt a Smalltalk fordító is. A nyelvnek objektumoktól független részét a mai objektumorientált nyelvekhez képest is nagyon kicsire húzták össze. Szinte az értékadás az egyetlen művelet, amit nem sikerült belepréselni az objektumokba. A ciklusokat például objektumok valósítják meg, ami nem csak érdekes megoldás, de számtalan lehetőséget is rejt magában.

Osztály, objektum, üzenet

Ha minden objektum, akkor az osztályok is. Ez első megközelítésben azt jelenti, hogy az osztályoknak is vannak adattagjaik és metódusaik (most nem a példányokban megjelenő adattagokról és metódusokról van szó, hanem közvetlenül az osztályhoz kapcsolódó elemekről). Ez a gondolat nemcsak a Smalltalk következetességét mutatja, hanem arra is lehetőséget ad, hogy például egy osztály hozza létre a saját példányait, ami elég jogosan az osztály (mint objektum) művelete.

A metódusok a Smalltalk szóhasználata szerint, a kapott üzenetek hatására lefutó eljárások.

Az osztályok típusai:

subclass Pontosan megfelel a rekordszerkezetnek
variableSubclass Egy olyan tömb, aminek tetszőleges objektumok lehetnek az elemei
variableByteSubclass Olyan tömb, aminek az elemei byte-ok (a stringek hatékony ábrázolásához)
variableWordSubclass Olyan tömb, aminek az elemei word-ök.

Osztályok létrehozása

A futtató keretrendszer objektumok és osztályok gyűjteménye. Ha létrehozunk egy saját osztályt, akkor azt beleépítjük ebbe a rendszerbe, és attól kezdve ugyanúgy működik, mint a korábban beépített osztályok.

Osztályt leggyakrabban az alábbi szintaxis szerint hozunk létre:

<Ősosztály> subclass: #<Újosztály> instanceVariableNames: '<példányváltozó1> <pldv2> ... <pldvk>' classVariableNames: '<osztályváltozó1> <osztv2> ... <osztvl>' poolDictionaries: '<szótár1> <sz2> ... <szm>'

Az osztályok definiálásához küldünk egy üzenetet az új vagy módosított osztály ősosztályához, amelynek az osztály specifikációs információi az argumentumai. Korábban is említettük már, hogy az osztályok is objektumok, ezért az osztálynak is lehetnek saját adattagjai (más nyelvek osztályszintű tagjai). Az instanceVariableNames részben tehát a példányokban megjelenő adattagok neveit soroljuk fel, a classVariableNames részben pedig az osztály változóit. A változók típusát nem kell megadnunk, mert a Smalltalk nem erősen típusos nyelv. A változók felsorolását implementációtól függően | (pipe) vagy ' (aposztróf) jelek között kell megadnunk. A poolDictionaries részben szótárakat (például a színkonstansok nevei) adhatunk meg, amelyeket az osztályban fel akarunk használni.

Új osztályt többféleképpen hozhatunk létre:

  • Objektumtallózóval (System Browser). A megfelelő menüpont kiválasztása után a fenti szintaxis vázát kapjuk meg, amit kitölthetünk az aktuális információkkal.
  • File in művelettel. Egy korábban megírt osztály definícióját tartalmazó fájl szerkeszthető be ezzel a menüponttal a rendszerbe.
  • Üzenettel. Mint a fenti formulából látszik, új osztály létrehozásakor tulajdonképpen az ősosztályunknak küldünk egy kulcsszavas üzenetet, melynek első argumentuma egy szimbólum, a továbbiak pedig String-ek (ezért kell az aposztróf), amelyek szóközökkel elválasztva tartalmazzák a megfelelő változók vagy szótárak neveit.

A specifikációs információkból is látszik, hogy a Smalltalkban csak egyszeres öröklődés van.

Metódusok definiálása

Egy osztály definíciójához hozzátartoznak az osztály és a példányobjektumok műveletei is. Ezeket a Smalltalk fejlesztői környezetben teljesen külön adhatjuk meg (egy külön menüpont segítségével), az osztály elmentésekor (file out) azonban láthatjuk a teljes szintaxist:

<Ősosztály> subclass: #<Újosztály> intanceVariableNames: '<példányváltozó1> <pldv2> ...<pldvk>' classVariableNames: '<osztályváltozó1> <osztv2> ... <osztvl>' poolDicionaries: '<szótár1> <sz2> ...<szm>' <osztály> class methods <metódusnév1> <metódustörzs1> <metódusnév2> <metódustörzs2> ... <osztály> methods <metódusnév1> <metódustörzs1> <metódusnév2> <metódustörzs2> ...
A metódusnév helyén a szelektor áll, az argumentumok helyén formális paraméterek nevei. Típust itt sem kell jelölni, ebből viszont olyan probléma származhat, hogy nem megfelelő adattípussal kerül meghívásra a művelet. Konvenció, hogy az argumentum neve utal annak típusára. Példák metódusfejre:
sin + anInteger ifTrue: trueblock ifFalse: falseblock

A metódustörzs egyszerűen kifejezések sorozata, pontokkal elválasztva. Ha ideiglenes változót akarunk használni, akkor azt az első kifejezés előtt kell deklarálni, két pipe-jel között megadva a nevét. A ^ jel jelöli a visszatérési értéket, ha nincs definiálva akkor az objektum magát adja vissza. A metódus végét egy felkiáltójel jelöli, ami csak file-ba mentett osztálynál látható, hiszen az objektumtallózó elrejti előlünk.

Nincs lehetőség változó hosszúságú paraméterlista átadására, de ez könnyen megvalósítható akár egy tömb segítségével, hiszen a Smalltalk nem erősen típusos (sőt erősen nem-típusos) nyelv.

Minden metódus dinamikusan kapcsolódik az objektumhoz, más objektum-orientált nyelvek virtuális metódusaihoz hasonlóan. Ily módon minden objektumunk polimorfikus, ezért a változók csak referenciákat tartalmaznak.

Metódus deklarációk általánosan (ANSI Smalltalk standard)

Minden metódus deklaráció egy metódus fejléccel-el (method header) kezdődik, melyet azután végrehajható kód (executable code) követ. A végrehajható kód opcionálisan lokális változók deklarációs listájával kezdődik, ezt követi a állítások sorozata.
Egy metódus deklaráció nem tesz semmiféle referenciát az osztályra, mely definiálja a metódust! (Az ilyen összeköttetések megalkotását az egyek futtató környezetk végzik el, maga a nyelv nem!)
A metódus szelektora annak fejlécében kerül specifikálásra. A metódusok szelektoruknak megfelelő üzenetküldés hatására kerülnek végrehajtásra.

Az olyan metódusokhoz melyek argumentumokkal rendelkeznek, azok fejlécében kell deklarálni a formális argumentumokat. Ezen formális argumentumokhoz fognak a megfelelő aktuális argumentomok tartozni, a metódust kiváltó üzenet által meghatározottan.
Egy formális metódus argumentum deklaráció szintaktikusan nem más, mint egy köthető azonosító (bindable identifier) beírása a megfelelő helyre a metódus fejlécébe. (ld. példák)
Egy köthető azonosító olyan azonosító, mely nem egyike a 6 db fenntartott azonosítónak (reserved identifier): niltruefalse,selfsuper vagy thisContext.
Minden egyes különálló végrehajtása a metódusnak (érkezett üzenet hatására), az aktuális üzenet argumentum értéke hozzákötődik a megfelelő formális metódus argumentumhoz. (Teljesen függetlenül minden egyes szeparált végrehajtás során, még ha a metódus rekurzívan újra végrehajtja is magát, vagy ha a metódus párhuzamos szálak által kerül végrehajtásra azonos időben.)
Hasonlóképp, minden egyes különálló végrehajtás különálló példányt használ a megadott lokális változókból, melyek a metódusban deklarálásra kerültek.
Más szóval a Smalltalk metódusok (és blokkok) "újrabelépők". (reentrant).

A formális metódus argumentumok nem változók, tehát értékük nem módosítható, nem használhatók rájuk értékadások értékük újra megadására.("újrakötésére")
Minden metódus végrehajtás kötött az aktuális üzenet-fogadó kontextusához, ami a metódus végrehajtásával járt. Ez azt jelenti, hogy a metódus kódja hozzáféréssel rendelkezik azon példányváltozókhoz, melyekkel az üzenetet fogadó objektum rendelkezik, akárcsak azon osztályváltozókhoz, melyek a fogadó osztály öröklődési hierarchiájában definiáltak, vagy azon megosztott (shared pool) változókhoz, melyeket az osztály importált.


Egy metódusnak akármennyi metódus visszatérési operátorral (^) prefixelt állítása lehet, viszont csak egy lehet közvetlenül a metódusban. A többi csak blokk literálok belsejében jelenhet meg a metódus kódjában. Azon állítás, mely egy metódus visszatérési operátorral prefixelt kell legyen a végső állítás a végrehajtható kód azon törzsében. Azaz annak kell lennie a végső állításnak az adott metódusban vagy blokk literálban. Szóval, bár maga a metódus végrehajtható kódjának törzse maximum egy metódus visszatérési operátort tartalmazhat, azonban minden azon belül megjelenő blokk literál végrehajtható kódjában ugyancsak lehet egy metódus visszatérési operátorral prefixelt végső állítás.
A metódus visszatérési opererátor azt csinálja, amit neve sugall: visszatéri a metódusból egy eredménnyel. Ugyancsak visszatéríti a metóduson lévő irányítási kontrollt, melyet visszahelyez az üzenet küldőjéhez. Nem egy blokk visszatéréi operátor! Egy blokk literálban történő végrehajtás esetén metódus visszatérési operátorhoz érve, nem csak a blokk végrehajtása szakad meg, hanem a teljes metódusé is, amelybe a blokk tartozik.
Abban az esetben, ha a metódus nem tartalmaz metódus visszatérési operátort, vagy a végrehajtási út nem egy metódus visszatérési operátorral prefixelt állítással ér véget, a visszaadott érték az üzenetet fogadó objektum, akinek hatására a metódus végrehajtódott.
Ahogyan szintaktikusan három különféle típusú üzenet létezik, úgy három különböző metódus fejléc is - minden szintaktikus üzenet típushoz egy. Mivel az unáris üzenetekben nincsen argumentumuk, az unáris metódus fejléceknek sincsen. Hasonlóképp mivel a bináris üzenetnek pontosan egy argumentumuk van, így lesz a bináris metódus fejlécekben is pontosan egy argumentum. Kulcsszavas üzenetek és kulcsszavas metódus fejlécek esetén pedig egy vagy több argumentumot is megadhatunk.
Két tényező fontos a következő példákkal kapcsolatban:

  1. Minden egyes metódus deklaráció különálló, független fordítási egység; nincsen standard szintakszis metódusok sorozatának deklarációjára, így az alább található példák teljesen különállóként kezelendők. Az egyes metódusok szintje felett a Smalltalk programok dinamikus, élő objektumok által meghatározottak, nem írhatók le statikus szövegfájlokként.
  2. Az osztályok nem deklaráltak szintaktikailag, tehát nincsen szintaktikus mechanizmus, mely összekapcsolná a megadott metódusokat egy adott osztállyal. Ezeken kapcsolatok, névterek kialakításának feladata minden esetben a futtató környezetekre hárul.

Unáris metódus fejléc

Az unáris metódus fejléc csupán egy unáris üzenet szelektorból áll - ami szintaktikailag egy azonosító.

value  "Answer the value of the receiver's instance variable named value."  ^value

area  "Answer the area of the receiver, by multiplying the value of its two instance variables width and height."  ^width * height

fullName  "Answer the full name of the receiver, formed by concatenating its givenName with its surName (with a space in between.)"  ^self givenName, ' ', self surName

 

Bináris metódus fejléc

A bináris metódus fejléc egy bináris üzenet szelektorból és az azt követő formális metódus argumentum deklarációból áll.

+ aSuffix  "Answer a new SequenceableCollection (of the same type as the receiver)  which has all the elements of the receiver as its prefix,  followed by all the elments of aSuffix as its suffix."  | concatenation |  concatenation := self class new: self size + aSuffix size.  1 to: self size do: [:i | concatenation at: i put: (self at: i)].  1 to: aSuffix size do: [:i | concatenation at: i + self size put: (aSuffix at: i)].  ^concatenation

- aSet  "Answer the set difference between the receiver and aSet."  | setDifference |  setDifference := self shallowCopy.  aSet do: [:each | setDifference remove: each ifAbsent: []].  ^setDifference

/ aDuration  "Answer the Timeperiod whose starting moment is the receiver, and whose duration is aDuration."  ^Timeperiod from: self duration: aDuration

 

Kulcsszavas metódus fejléc

Egy kulcsszavas metódus fejléc deklarál egy vagy több metódus argumetumot. A szelektora egy kulcsszavas metódusnak szintaktiailag kulcsszavas sorozata.
Egy formális metódus argumentum van kulcsszavanként, és egy kulcsszó argumentumonként.
Egy kulcsszavas metódus fejléc szelektorának első kulcsszavával kezdődik, ezt követi az első formális metódus argumentum. A forma ismétlődik minden kulcsszó és argumentum párosra: az i.-ik kulcsszó majd i.-ik formális metódus argumentum kerül kiírásra.
A kulcsszavakat követő #: karakter egyszerűvé teszi az egyes kulcsszavak és hozzájuk tartozó argumentumok elválasztását.

setValue: newValue  "Set the value of the receiver's instance variable value  to be the value of the argument (newValue.)"  value := newValue

max: comparand  "Answer whichever Magnitude is greater: the receiver, or the argument (comparand).  If equal, answer the receiver."  ^self >= comparand ifTrue: [self] ifFalse: [comparand]

ifTrue: trueBlock ifFalse: falseBlock  "If the receiver represents the value true, answer the result of evaluating trueBlock.  If the receiver represents the value false, answer the result of evaluating falseBlock."  "Since the receiver is the sole instance of False, the result of evaluating falseBlock is answered."  ^falseBlock value

inject: initialValue into: twoArgBlock  "Evaluate twoArgBlock once for each element of the receiver.  For each evaluation of twoArgBlock, the first argument is  the currentValue, and the second argument is each element  of the receiver in succession. Initially, the value of currentValue  is initialValue. Subsequently, the value of currentValue  is the value that resulted from the previous evaluation of twoArgBlock.  Answer the final value of currentValue."  "For example, to sum a collection, use:  collection inject: 0 into: [:sum :each | sum + each]."  | currentValue |  currentValue := initialValue.  self do: [:element | currentValue := twoArgBlock value: currentValue value: element].  ^nextValue

year: year day: dayOfYear hour: hour minute: minute second: second  "Anwser a new instance of the receiver (a new instance of DateAndTime)  representing the specified date and time-of-day."  ^self new  setYear: year dayOfYearOrdinal: dayOfYear;  setHour: hour minute: minute second: second;  canonicalizeFromLocalTime;  beImmutable

Self és super

Van két pszeudováltozónk, amelyek mindig az aktuális objektumra mutatnak. Ha saját metódust akarunk meghívni, akkor a self pszeudováltozót használhatjuk. Ha a közvetlen ős egy metódusára van szükségünk, akkor a super változót használjuk, melynek jelentése a következő:

Ennek például akkor van jelentősége, amikor meghívjuk az ősosztály new metódusát, amely ebben a helyzetben a leszármazottból hoz létre egy új példányt. Ezzel a megoldással lehet konstruktort létrehozni a Smalltalkban:
new ^super new initialize

A new a most definiálandó metódus neve. Ez egy osztálymetódus, tehát az osztályra hívhatjuk meg, és a visszatérési értéke egy új, inicializált példány lesz. A ^ a visszatérési értéket határozza meg (tehát a C-ben szereplő return utasítás megfelelője). A super new utasítás eredménye egy objektum, ami az aktuális osztály (és nem az ős) példánya. Így erre meghívhatjuk az ebben az osztályban definiált initialize metódust, ami az objektumot inicializálja. Ezt adja vissza a ^ operátor.

A super használata megtévesztő lehet, ha egy ősosztály metódusában fordul elő. Ugyanis nem az aktuális objektum szülőosztályára hivatkozik, hanem azon osztály szülőosztályára, amelynek a metódusa éppen fut. Lássuk egy példán keresztül:

Object subclass: #Test1 (...)! Test1 methods! test ^1! proba1 ^self test! Test1 subclass: #Test2 (...)! Test2 methods! test ^2! proba2 ^super test! Test2 subclass: #Test3 (...)! Test3 methods! test ^3! t2:=Test2 new. t3:=Test3 new. t2 proba1 = 2. t3 proba2 = 1.

Az első próba a várakozásnak megfelelő eredményt adta, hiszen: t2 Test2-beli, Test2-nek nincsen proba1 metódusa, nézzük a szülőosztályát. Test1-ben a proba1 metódus a self-re hivatkozik, ami t2, tehát Test2-beli.

A második viszont érdekes: t3 osztálya Test3, neki nincs proba2 metódusa, nézzük a szülőosztályát. Test2-ben a proba2 metódus super-re hivatkozik, amely (hiába t3 Test3-beli, szülőosztálya Test2) az aktuálisan futó metódus osztálya alapján számol: Test2 metódusa fut, akkor Test1 szintjén keresi a test metódust.

Absztrakt műveletek

A Smalltalk nyelv nem támogatja absztrakt osztályok, vagy absztrakt műveletek definiálását, noha a fejlesztői is jól tudták, hogy nem élhetünk absztrakt műveletek nélkül. Ezért úgy igyekeztek orvosolni a problémát, hogy az Object osztályban (subclassResponsibility néven), definiáltak egy metódust amelyik egy speciális hibaüzenettel adja a felhasználó tudtára, hogy olyan metódus hívódott meg, amelynek absztraktnak kéne lennie. Ehhez persze az kell, hogy az az ősosztály azon metódusaiból, amelyeknek absztraktnak kéne lenniük, meghívódjon ez a bizonyos subclassResponsibility metódus. Például:

Object subclass: #Verem ... methods push: Elem "Minden reprezentációban más és más az implementáció!" ^ self subclassResponsibility. ... Verem subclass: #LancoltVerem instanceVariableNames: 'elsoElem, ...' ... methods push: Elem "Befűzzük az új elemet a lista elejére." elsoElem next: ( ElemTarolo new initialize: Elem next: elsoElem ). ^ self. ... Verem subclass: #KorlatozottVerem instanceVariableNames: 'tomb, elemSzam, korlat, ...' ... methods push: Elem "Betesszük az új elemet a tömbbe a következő pozícióra." (elemSzam < korlat) ifTrue: [elemSzam := elemSzam + 1. tomb at: elemSzam put: Elem.] ifFalse: [self error: 'Betelt!'] ^ self. ...

Mindenképpen érdemes tartani magunkat ehhez a konvencióhoz, hogy legalább futás időben kiszűrhessük ha egy a fentihez hasonló "absztrakt" metódus meghívódik, ugyanakkor ügyeljünk arra, hogy az ilyen absztrakt osztályokból közvetlenül ne példányosítsunk, és a leszármazottakban definiáljuk felül az absztrakt metódusokat.

Az Object osztály definiál még egy, az előbbihez hasonlóan hibajelzésre használt metódust. A shouldNotImplement eljárást konvenció szerint akkor használjuk, amikor egy leszármazott osztályban nincsen szükségünk az ősosztály valamely metódusára, és el is akarjuk kerülni, hogy akárcsak az örökölt implementációval is lefusson. Ekkor a kérdéses metódust úgy definiáljuk felül, hogy az csak a shouldNotImplement eljárást hívja meg.

Objektumok létrehozása

Egy objektumot általában a megfelelő osztály new metódusával hozhatunk létre, de az osztály tetszőleges metódusa adhat vissza objektumot, vagy más objektumok, osztályok is létrehozhatják. Például egy grafikai osztály a Pen, amellyel vonalakból építhetünk képeket. Ennek létrehozására általában a használni kívánt ablakosztály pen metódusát használjuk:

|aWindow aPen| "Ez a változók deklarációja (típusmegadás nincs)" aWindow:=GraphPane openWindow:'Ablak Címsora' "A GraphPane osztály openWindow metódusa" aPen:=aWindow pen "! Ez most az igazán érdekes" aPen down; "A pontosvessző azt jelenti, hogy a következő metódus" place 50@50; "ugyanannak az osztálynak, objektumnak szól" goto 100@100. "A pont az utasítást zárja le"

További példák:

"5 hosszú vektor létrehozása" p := Array new: 5 "Üres zsák létrehozása" q := Bag new

Absztrakt (nem példányosítható) osztályokat úgy hozhatunk létre, hogy a new metódust felüldefiniáljuk, és például hibaüzenetet adunk objektum helyett.

A Smalltalk automatikus szemétgyűjtést használ, így explicit felszabadításra nincs lehetőség.

Láthatóság, kategóriák

A Smalltalkban egy belső változót csak az adott objektum műveletei láthatják közvetlenül. Tehát a példányváltozókat a leszármazott osztályokba tartozó objektumok műveletei is láthatják, de nem láthatók más objektumok műveleteiből. Ezzel a Smalltalk eléri, hogy egy osztály reprezentációja rejtve marad a kliensek előtt. Egy kliens csak üzenetküldés útján manipulálhat egy objektumot, így attól, hogy megváltozik a reprezentáció, a klienseknek nem kell feltétlenül megváltoznia.

Minden művelet mindig látható, ezért csak ajánlás, hogy a konvencionálisan "private" szócskával megjelölt műveleteket ne hívjuk meg közvetlenül.

Az adattagok teljesen rejtettek, és ezt nem is lehet megváltoztatni. Ha egy adattagot el akarunk érni, akkor íráshoz és olvasáshoz egy-egy metódust kell definiálnunk. (Viszont ezek neve lehet ugyanaz, mint az adattagnak.)

Például:

Object subclass #vmi instanceVariableNames: 'anInt' classVariableNames:'' poolDicitonaries:'' ... anInt ^anInt. ... anInt:a anInt:=a.

Metódusainkhoz hozzárendelhetünk kategóriákat. Ez csupán az osztályt használó programozónak szóló információ. Példák kategóriákra: accessing, displaying, printing, public, private, initializing, converting, events... A kategóriák eltárolása az osztály definíciójában implementációfüggő, legkönnyebb az objektumtallózó ablakban menüből kiválasztani a kívánt kategóriákat.

Metaosztályok

Mivel minden osztály objektum, és minden objektum pontosan egy osztály példánya, ezért az osztályoknak is, mint objektumoknak, van osztályuk, ezek a metaosztályok. Minden osztályhoz egy külön metaosztály tartozik, hiszen az osztályszintű változók halmaza minden osztályban más és más. Egy metaosztálynak pontosan egy előfordulása van, ez az az osztály, aminek ő a metaosztálya. Elméletben a metaosztályok is osztályok, de nekik nem lehet külön belső változókat és műveleteket definiálni. Például az Integer osztály metaosztályának neve: Integer class.

A metaosztályok öröklődési hierarchiája pontosan megegyezik előfordulásaik öröklődési hierarchiájával. A metaosztályok mint objektumok a MetaClass előfordulásai, és mint osztályok a Class osztály alosztályai. Mivel a Class "normális" osztály, ezért neki is van metaosztálya: a Class class metaosztály. Mivel a Class-nak minden metaosztály alosztálya, ezért a Class class is alosztálya. Az Object osztálynak (minden osztály közös ősének) is megvan ez a tulajdonsága (a metaosztálya a saját alosztálya is), lévén a Class az Object alosztálya, az Object class metaosztály pedig a Class alosztálya.

A MetaClass metaosztálya, a MetaClass class, előfordulása a MetaClass-nak, azaz a MetaClass metaosztálya az ő előfordulása. Ugyanez igaz az Object osztályra is.

Az osztályszintű adattagok és metódusok pontosan úgy öröklődnek, ahogy azt a példányváltozók, ill. példányműveletek teszik. Tehát, ha egy osztálynak van egy i osztályváltozója, akkor az osztályváltozója lesz az osztály összes leszármazottjának is.