Objective-C-ben úgy definiálunk objektumokat, hogy definiáljuk az osztályukat. Az osztály egy prototípus egy adott objektumra: a keletkező objektumok az osztály adattagjait kapják meg, és az osztályban definiált metódusokat az osztályhoz tartozó objektumok használni tudják. A fordító minden osztályról egyetlen elérhető objektumot készít, egy osztály objektumot, amely tudja, hogy kell az osztály példányait létrehozni. Emiatt ezt az objektumot rendszerint gyártónak hívjuk: factory objectnek. Az osztály objektum az osztály lefordított változata, az objektumok pedig, amiket gyárt, az osztály példányai. A programunkban a "munkát" elvégző objektumok olyan példányok, amiket ez a factory object gyárt le futási időben. Az osztály objektum olyan objektum, ami nem az osztály példánya, nincsenek meg benne az adattagok, és a példánymetódusokat nem hívhatjuk meg rá. Definiálhatunk azonban osztálymetódusokat is, kifejezetten az osztály objektum számára, például írhatunk osztálymetódust, amely megadja az osztály aktuális verzióját. Készíthetünk absztrakt osztályokat, amik nincsenek teljesen definiálva, ám előre megírt hasznos kódokat tartalmazhatnak, így örökléssel sok felesleges munkát megspórolhatunk. A nyelvben nincs olyan megkötés, hogy absztrakt osztályt nem lehet példányosítani, igaz, nem is tudunk osztályt expliciten absztrakttá tenni, mint például C++-ban vagy a C#-ban.
A nyelvben kötelező szétválasztani az interfészt és az implementációt. Az interfész deklarálja az osztály adattagjait, metódusait, és megnevezi az ősosztályát, az implementációban pedig definiáljuk a metódusokat, ezzel tulajdonképpen az osztályt. A két részt tipikusan két fáljba szedjük, előfordulhat azonban, hogy több fáljba esnek szét a részek a kategóriának nevezett nyelvi eszköz miatt. Az interfész deklarálásának szintaktikája (A "ClassName.h" fájlban) :
Megjegyzés: GNUStep fordító alatt kipróbálva az osztályszintű változók ilyen módú deklarációja nem működik, ehelyett a .m állományban javasolt létrehozni a változót ugyanilyen módon (példa a Gyár tervminta World.m fájljában). Ekkor a változó fájl szinten lesz statikus és érdemes az +(void)initialize metódusban inicializálni.
Az osztálynév és az ősosztály megadása után kapcsos zárójelben kell felsorolni az adattagokat. Ezek után kell a kapcsos bezáró zárójel és az @end direktíva között felsorolni az osztály illetve példánymetódusokat. Az osztálymetódusok + jellel, a példánymetódusok - jellel vannak bevezetve, majd kerek zárójelben opcionálisan a visszatérési érték szerepel, majd a metódus neve, aztán opcionálisan kettősponttal elválasztva a paraméterek felsorolása (típus)név formában. Lehetőség van változó számú paraméter átadására a "..." írásával.
Természetesen az ősosztályt ismerni kell, ezért be kell importálni, ha használni akarjuk. Ha az interfész olyan osztályokra hivatkozik, amiket nem tudunk beimportálni, akkor lehetőség van azok elő-deklarációjára :
Az implementáció megírásakor, ha az külön fájlban van, értelemszerűen be kell importálni az interfész headerfájlját. Az implemetációs részt következőképp definiálhatjuk (ClassName.m fájlban) :
Az osztály adattagjainak láthatóságának szabályozására a @public, @private, @protected és a @package direktívák adnak lehetőséget. A publikus tagok mindenki számára elérhetőek, a privát tagok csak az osztályon belül láthatóak, a védettek osztályon és leszármazottakon belül láthatóak, 64 biten pedig a package láthatóságú adattagok az osztályt implementáló image-en belül publikusként, kívül pedig privátként viselkednek. A default láthatóság a protected.
A nyelvben az objektumok allokálása kötelezően dinamikus. Ez azt jelenti, hogy globálisan vagy veremben (lokális változóként) nem lehet objektumot deklarálni. Természetesen primitív típusokra(pl.: int, float, char) ez a megkötés nem érvényes.
Objektumok deklarálására két lehetőség van.
Minden metódusnak két rejtett paramétere van, a self és a _cmd, vagyis ez
Az adott objektum osztályának megfelelő mutatót deklarálunk majd az így lefoglalt memóriaterületre inicializáljuk az objektumot. Ezzel már fordítási időben ismertté válik az objektum (statikus) típusa és sok hiba fordítási időben is kiderül, valamint lehetséges kódoptimalizálást végezni.
Az általános id objektumreferenciatípussal deklaráljuk az objektumra való hivatkozást, ezzel nagyon szabad kezet kapunk a polimorfizmussal, dinamikus kötéssel, öröklődéssel. Elsőre úgy tűnhet hogy az id megfelel más nyelvekben is szereplő ősosztálynak, mint például C#-ban az object. Azonban mivel a nyelv alapvetően dinamikusan típusos, ez a hasonlóság korántsem valós. Hiszen míg C#-ban az object-nek csakis a saját eljárásait hívhatjuk meg addig Objectiv-C-ben az id bármilyen üzenetet képes fogadni. C#-ban egy object típusú változó nagyon korlátos felülettel rendelkezik így általában kockázatos upcast műveletét kell végeznünk a változón hogy a dinamikus típusát jobban kihasználó felületet biztosítsunk számára. Ezzel szemben egy id típusú változó felülete nem korlátos, azaz bármilyen üzenetre potenciálisan képes reagálni.
Az id változó viselkedeését a közkedvelt angolszász modással szokták elmagyarazni, miszerint "ha ugy totyog mint egy kacsa, és úgy hápog mint egy kacsa akkor az egy kacsa!". Azaz az objektum viselkedése dönti el a típusát. A fenti modás mentén szokás ezt a típusosságot duck-typing-nak is nevezni.
Az első példában, ha a Square a Rectangle-ből származik, teljes Square objektumot fogunk kapni, nem csak Rectangle-t. Ha viszont olyan példányt akarunk példányosítani, ami nem leszármazott (vagy saját maga), vagy olyan metódusát akarjuk meghívni, ami nincs neki, akkor ez fordítási hibát fog eredményezni. A második példában az id típusú változónk bármilyen objektumra mutathat, kihasználhatjuk a dinamikus kötés minden előnyét, de futási időben keletkeznek csak hibák, amelyeket le kell kezelni.
Minden osztály objektumnak van legalább egy metódusa (mint az alloc), amely helyet foglal a példányoknak, és minden objektumpéldánynak van legalább egy metódusa (mint az init), amely inicializálja és előkészíti a használatra. Minden osztály objektumnak elküld a runtime system egy üzenetet minden más üzenet érkezése előtt, ez az initialize. Ha ezt implementáljuk az osztályunkban, akkor lehetőségünk van az osztály objektumot is előkészíteni:
Objective- C- ben nincs konstruktor metódus. Helyette az init inicializáló metódust használjuk, amely mindig egy id típusú objektummal tér vissza. Az inicializálás mindig a memóriafoglalás után történik, így a példányosítás mindig két lépésből áll: memóriafoglalás és inicializálás. Az init metódus azonban csak abban az esetben megfelelő, ha az inicializáláshoz nincs szükségünk semmilyen bejövő adatra, vagyis paraméterre. Mivel Objective- C- ben nem lehet a függvényeket túlterhelni, ezért a paraméterrel ellátott inicializáló műveleteket nem nevezhetjük init- nek, azonban kötelezően init-el kell kezdődniük. Számtalan osztály rendelkezik beépített inicializáló metódussal, például:
Egy objektum megszűnésekor három metódus hívódik meg automatikusan. Az egyik a –.cxx_destruct metódus, ez nem felülírható a programozó által. A másik két metódus a –finalize és a –dealloc. Amennyiben használjuk a nyelv nyújtotta szemétgyűjtést, foglalkoznunk kell a szemétgyűjtő által nem kezelt erőforrások felszabadításával. Ilyen például a fájl bezárása vagy a malloc() segítségével foglalt memória felszabadítása. Amennyiben egy osztály esetében szükség van hasonló tevékenységre, a –finalize metódust kell deklarálni. Ha nem használjuk a szemétgyűjtőt, akkor a –dealloc metódusban kell elvégeznünk a memória felszabadítását. Mindkét esetben szükséges az üzenet továbbítása az ősosztály felé is, vagyis [ super dealloc] vagy [ super finalize] metódusok hívása. Egy tipikus dealloc metódus a következőképpen néz ki:
Egy osztály definíció különféle információkat tartalmaz, többségük az osztály példányaira vonatkozik:
Ezt az információt a fordító lefordítja, adat struktúrákban rögzíti és elérhetővé tesz a runtime system részére. A fordító egyetlen objektumot készít, az osztályobjektumot, ami az osztályt reprezentálja. Az osztályobjektum hozzáfér az osztály összes információjához, ami főleg arról szól, hogy milyenek az osztály példányai. Képes új példányokat létrehozni az osztálydefinícióban rögzített terv szerint.
Bár az osztályobjektum tartalmaz egy példány prototípust, ő maga nem egy példány. Nincsenek saját példányváltozói, és nem hajthat végre olyan metódusokat amelyek az osztály példányai számára készültek (példány metódusok). Ugyanakkor egy osztály tartalmazhat olyan metódusokat, amelyek kifejezetten az osztály részére készültek (osztály metódusok). Az osztály örökli a hierarchiában fölötte álló osztályok osztálymetódusait,csakúgy ahogy a példányok öröklik a példánymetódusokat.
Amikor definiálunk egy új osztályt, megadjuk a példányváltozóit. Az osztály minden példánya karbantarthatja a saját másolatát az így deklarált változóknak, azaz minden objektum karbantartja a saját adatait. Ugyanakkor a példányváltozó fogalmának nincsen semmilyen „osztályváltozó” párja. Csak az osztálydefinícióból inicializált, belső adatstruktúrák állnak az osztályok rendelkezésére. Sőt, az osztály objektumnak nincs hozzáférése egyetlen példány példányváltozókhoz sem, nem tudja inicializálni, olvasni vagy módosítani őket.
Hogy egy osztály összes példánya megoszthasson valamilyen adatot, definiálnunk kell valamiféle külső változót. A legegyszerűbb módja ennek, hogy deklarálunk egy változót az osztály implementációs fájlban:
Egy szofisztikáltabb implementációt is készíthetünk, ha a változót „static”-nak deklaráljuk, és a menedzselésére külön létrehozunk osztálymetódusokat. Ha static-nak deklarálunk egy változót, az limitálja a scope-ját az osztályra, és annak is arra a részére ami a fájlban van definiálva. (Így a példányváltozókkal ellentétben ezeket a változókat az alosztályok se nem öröklik, se módosítani nem tudják.) Ezt a mintát gyakran alkalmazzák egy osztály megosztott példányának létrehozására.
A statikus változók segítséget nyújtanak abban, hogy a példányok legyártásától eltérő funkcionalitást adjunk az osztályobjektumokhoz, ami így közeledhet ahhoz hogy egy teljes és önálló objektum legyen. Egy osztály objektum felhasználható a példányok koordinálására, hogy töröljön példányokat a legyártott objektumok listájából, vagy egyéb az alkalmazás szempontjából fontos folyamatokat is ütemezhet. Abban az esetben, ha egy bizonyos objektum típusból csak egyetlen példányra van szükség (singleton), annak állapotát reprezentálhatjuk statikus változókkal, amelyeket kizárólag osztálymetódusokból használunk. Ezzel megspóroljuk az objektum allokálásának és inicializálásának költségét.
Bármely osztály, vagy egy osztály bármely példánya felfedheti előttünk az osztály objektumát, ha elküldjük neki a "class" üzenetet. Az így nyert osztály objektumot később felhasználhatjuk típusként (alloc üzenet fogadására), vagy átadhatjuk üzenetben másik objektumnak, amivel a generikus programozáshoz típusparaméterek átadását lehet megvalósítani.
A következő példányosítási módok szintén legálisak:
Örökléskor a származtatott osztály örökli a bázis osztály összes adattagját és tagfüggvényét. Általában minden osztálynak közvetve vagy közvetlenül őse az NSObject (Amennyiben NEXTStep illetve Cocoa frameworkot használunk). Az osztály objektumok, mivel nincsenek adattagjaik, csak metódusokat örökölnek. Lehetséges örökölt metódusok felüldefiniálása, ekkor ez a metódus hívódik meg, és a későbbi származtatott osztályok, akiknek ez az őse, az új metódust öröklik. A többszörös öröklődést viszont a nyelv nem támogatja, ennek a megkerülésére vezették be a protokollt. A protokoll tulajdonképpen egy adattagok nélküli absztrakt bázisosztály, amely csak metódusokat tartalmaz. Többszörös öröklést tudunk úgy helyettesíteni, hogy egyszeresen öröklünk egy osztályból, és közben az új osztály megfelel egy vagy több protokollnak (interface deklaráláskor kacsacsőrbe téve a protokollokat ):
Ekkor a Shape olyan, mintha az NSObject, Archiving és ReferenceCounting osztályokból öröklődött volna egyszerre, mivel a protokollokban deklarált metódusokat kötelező implementálni.
Az üzenetküldés jelentősége az Objective-C-ben abban áll, hogy az üzenetek és az objektumok nincsenek szorosan összekötve, hanem egy üzenet küldésekor csak futási időben dől el, hogy mi történik. Például egy négyzet és egy kör másképp viselkedik, ha egy üzenettel megkérdezzük a területüket, de az adott objektumról, ami a kezünkben van, nem kell tudni a pontos típusát, hiszen különböző objektumoknak különböző megvalósításai lehetnek ugyanarra a függvényre (polimorfizmus). A metódus neve az üzenetben kiválasztja az implementációt, ezért szokás a metódusneveket az üzenetekben szelektoroknak hívni.
A kategória segítségével már meglévő osztályokhoz adhatunk hozzá metódusokat - akkor is, ha nincs meg az osztály forrása. Ez egy rendkívül erős eszköz, amellyel öröklés nélkül terjeszthetjük ki az osztályok funkcionalitását. Kategóriák használatával osztályaink implementációját több forrásfájlra is szét tudjuk darabolni. Egy osztályhoz adott metódus az osztály típusnak is része lesz a kategórián belül, a fordító számít arra, hogy ez a metódus létezik az osztály példányaiban. Adattagokat nem vehetünk fel kategóriák segítségével. Ugyanúgy kell metódust hozzáadni osztályhoz, mint egy osztály interfészében/implementációjában, azzal a különbséggel, hogy nem az ősosztályt nevezzük meg, hanem a kategóriát kerek zárójelben. Konvenció, hogy a kategória forrásfájljait "Osztálynév+Kategórianév.*" formában nevezzük el. A kategória deklarálásának és definiálásának formája:
Természetesen a hozzáadott metódus látja az összes adattagját az osztálynak, még a privátokat is. A kategóriában hozzáadható függvények száma korlátlan, de mindnek más nevet kell adni. A kategóriában adott függvények kiterjeszthetik az osztály f uncionalitását, vagy felülírhatnak örökölt függvényeket. Megvan a lehetőség arra, hogy más kategóriákban definiált függvényeket is felülírjunk, ám ez nem megbízható és nem is ajánlott. Lehetséges az osztályhierarchia gyökerének a kibővítése is: ha az NSObjectet kategorizáljuk, az összes leszármazottban megjelenik az adott függvény, amely néha hasznos tulajdonság, néha veszélyes.
Alapvetően a SmallTalk-ból vette át az Objective-C az üzenetküldés ötletét. Ha egy objektummal csináltatni akarunk valamit, akkor üzenetet küldünk neki, hogy végezze el az adott metódust. Az üzenetküldés szintaktikája:
A fogadó egy objektum, az üzenet pedig a metódus neve, amit végre kell hajtani. Üzenetküldéskor a runtime system kiválasztja a megfelelő metódust és meg is hívja. Paramétereket is átadhatunk: egy paramétert egyszerűen egy kettőspont után:
Több paraméter átadása már nem ilyen egyszerű, mert a paramétereket a nevükkel együtt kell megadni:
p>Visszatérési értéket is kapunk, ha a metódus ad ilyet:
Az üzeneteket egymásba is ágyazhatjuk:
Érvényes a nil-nek üzenetet küldeni, ekkor semmi nem történik, de ha a metódusnak, amit hívunk, van visszatérési értéke, akkor előre megszabott esetekben előre megszabott értékeket kapunk, egyébként nem definiált értéket.
Az üzenetküldést kiválthatjuk a "Dot Syntax" alkalmazásával, ami csak szintaktikus cukorka, de mégis hasznos lehet. A nyelv biztosít . operátort, amivel egyszerűbben, üzenetküldés nélkül érjük el a függvényeket:
ekvivalens ezzel:
Objktive-C-ben a szelektor kifejezésnek két értelme van. Gyakran használjuk egyszerűen egy metódus nevére és szignatúrájára hivatkozva amikor azt a forráskódban üzenetként használjuk. Ugyanakkor azt az egyedi azonosítót is jelentheti, ami a metódus nevét helyettesíti a forráskód lefordítása után. A lefordított szelektorok típusa SEL. Minden azonos nevű metódushoz ugyanaz a szelektor tartozik. Az ilyen szelektort használhatjuk egy objektumon valamilyen akció kiváltására. Ez adja a Cocoa target-action tervezési mintájának alapját.
Hatékonysági szempontok miatt a lefordított kódban nem használunk teljes ASCII neveket szelektorként. Ehelyett a fordító egy táblázatba írja a metódus neveket, majd párosítja a metódus egyedi azonosítójával, ami a metódust futás időben reprezentálja. A runtime system biztosítja, hogy minden azonosító egyedi legyen: Nincs két azonos szelektor, és minden azonos nevű metódushoz ugyanaz a szelektor tartozik.
A lefordított szelektorokhoz a speciális SEL típust rendelik, hogy megkülönböztessék őket a többi adattól. Érvényes szelektor értéke sosem lehet 0. Engedd, hogy a rendszer rendelje a metódusokat a SEL azonosítókhoz, az önkényes hozzárendelés hasztalan.
A @selector() direktíva segítségével hivatkozhatunk a lefordított szelektorra a teljes metódusnév helyett. A következő példában a setWidth:height: szelektorát a setWidthHeight változóhoz rendeljük hozzá:
A szelektorok SEL változókhoz rendelésének a leghatékonyabb módja a fordítás idejű @selector() direktíva használata. Ugyanakkor, néhány esetben futásidőben kell karakterláncokat szelektorokká konvertálnunk. Ezt megtehetjük az NSSelectorFromString függvénnyel:
Az ellentétes irányú konverzió szintén lehetséges. Az NSStringFromSelector függvény megadja a szelektor metódusának nevét:
A lefordított szelektorok metódus neveket azonosítanak, és nem metódus implementációkat. Egy osztály display metódusához például ugyanaz a szelektor tartozik, mint a más osztályokban definiált display metódusoknak. Az a polimorfizmus és a dinamikus kötés szempontjából létkérdés, mivel így elküldhetjük ugyanazt az üzenetet a különböző osztályhoz tartozó fogadóknak. Ha egy szelektor – egy metódus mintájú implementációnk lenne, akkor az üzenet nem különbözne egy függvényhívástól.
Az ugyanolyan nevű osztály és példánymetódust ugyanahhoz a szelektorhoz rendeljük. Ugyanakkor, mivel különböző doménhez tartoznak, nem keverhetőek össze. Egy osztály definiálhat egy display metódust, a display példány metódusa mellé.
Az üzenetkezelő rutin a metódus implementációknak csak a szelektorokon keresztül éri el, így az egy szelektorhoz tartozó metódusokat ugyanúgy kezeli. A metódus visszatérési értékét és a paraméterei típusait a szelektorból deríti ki. Emiatt, a statikusan tipizált fogadóknak küldött üzenetek kivételével, a dinamikus kötés megköveteli, hogy az összes azonos nevű metódus implementációnak azonos visszatérési és paraméter típusai legyenek. (Statikusan tipizált fogadók kivételek e szabály alól, hiszen a fordító ismerheti a metódus implementációját az osztály típusból.) Bár az azonos nevű osztály és példány metódusokat azonos szelektor reprezentálja, lehetnek különböző paraméter és visszatérési típusaik.
Objective-C-ben a protokollok az interfészek szerepét töltik be. Egy protokollon belül metódusokat deklarálhatunk, melyeket más osztályoknak implementálnia kell, amennyiben az adott protokollal rendelkezni óhajt. Ezáltal ha csak annyit tudnunk egy osztályról, hogy mely protokollokat implementálja és ismerjük a protokoll metódusait, akkor azokon keresztűl mindenképpen tudjuk használni az osztályt.
A protokoll szintaxisa a következő:
Egy protokollban deklarált metódust az őt felhasználó osztálynak alapértelmezetten kötelező implementálnia. Azonban van lehetőségünk opcionálisan megvalsóítható metódusokat is megnevezni. Erre szolgál az @optional direktíva. Amennyiben az opcionális metódusok után kívánunk megadni kötelezően implelementálásra szánt metódusokat, úgy ekkor a @required direktívát használjuk. Protokollt egy osztály a következő szintaxissal adoptálhat:
Lehetőségünk van leellenőrizni futásidőben, hogy egy objektum megfelel-e egy protokollnak. Ezt a conformsToProtocol üzenettel tehetjük meg az alábbi módon:
Ezen kívül használhatunk olyan id típust, ami korlátozott egy vagy több protokollra. Ez egyedülálló az Objective C nyelvben, a legtöbb programnyelv ugyanis nem kínál eszközt több protokollnak megfelelő változó létrehozására.
A Posing hasonlít a kategóriákra egy kis csavarral. Arra ad lehetőséget, hogy származtassunk egy osztályt, majd globálisan helyettesítsük vele az ősét. A helyettesítés után valahányszor az ős osztályt szeretnék használni, a gyermeket használjuk. A korábban létrehozott példányok nem változnak meg, csak az újak. Az alábbi követleményeknek kell teljesülnie a pózoló osztályra:
A Posing használatát elavultként jelölték meg Mac OS X v10.5 verziónál és nem elérhető a 64 bites rendszereken.
Objektumok létrehozásakor, ha nem automatikus objektum felszabadítást választunk, akkor manuálisan kell felszabadítani a memóriát. Erre a feladatra az Objectiv-C egy nagyon kézenfekvő megoldást ad, amelyet az ősosztály az NSObject bíztosít. Gyakorlatilag, ha létrehozunk egy objektumot egy osztályból az alloc szelektorral az alábbi módon:
akkor azt az alábbi módon fel kell szabadítanunk:
Azonban mi történik akkor, ha esetleg ezt az objektumot átadtuk paraméterként egy másik objektumnak, amely a referenciát letárolta és erre a referenciaként továbbra is hivatkozik, sőt ezt az objektumot fel is akarja majd szabadítani? Nyilván ebben az esetben, ha csak szimplán töröljük az eredeti objektumunkat, akkor a máshol használt referencia használata illegális lesz, majd ennek másodszori tőrlése végzetes lehet. Ennek a paradox helyzetnek a feloldására vezették be a retain szelektort, amelyet akkor kell meghívnunk, ha az objektumot több helyen használjuk, illetve menetközben akár több helyen is felszabadítjuk a release-sel. Mielőtt átadnánk paraméternek, hívjuk meg a retain-t:
Gyakorlatilag minden objektum tartalmaz egy referencia számlálót, amelyet a retain növel eggyel, illetve a release csökkent eggyel. Amennyiben ez a referencia számláló eléri a nullát, akkor valóban megtörténik az objektum felszabadítása a release-ben. Általában a referenciák felszabadítását egy adott osztályban a dealloc szelektorban végezzük el. Ez az a szelektor, amely minden objektum felszabadításakor meghívódik automatikusan. Ha ezt felüldefiniáljuk, akkor az ősosztály dealloc-ját vissza kell hívnunk.
A példaprogramok között (shapes.m) látható példa a fenti manuális memória kezelésre