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.
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. |
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:
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:
A specifikációs információkból is látszik, hogy a Smalltalkban csak egyszeres öröklődés van.
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:
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.
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): nil, true, false,self, super 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:
Az unáris metódus fejléc csupán egy unáris üzenet szelektorból áll - ami szintaktikailag egy azonosító.
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.
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.
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ő:
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:
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.
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:
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.
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:
További példák:
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.
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:
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.
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.