Objective-C alkeet

Tässä olisi sitten pikaopas Objective-C kieleen tarjolla. Objective-C on C-kielen laajennos, johon on yhdistetty olio-ominaisuuksia Smalltalk-kielestä. Objective-C:tä käytetään pääasiassa Cocoa-ohjelmien kehittämiseen Mac OS X -käyttöjärjestelmään. GCC tukee kuitenkin obj-c koodin kääntämistä muillakin alustoilla, mutta tätä en ole itse sen tarkemmin kokeillut.

Tämän oppaan olisi tarkoitus selittää obj-c:n perusrakenne. Oletan kuitenkin, että sinulla on hieman kokemusta C-kielestä ja olio-ohjelmoinnista.

Oliot

Objective-C on C++:n ja javan tavoin olio-kieli, joten alkuun olisi hyvä kertoa miten olioita käsitellään.

Olion luominen

Olioilmentymiin viitataan osoittimilla. Ensin oliolle on varattava tilaa vapaasta muistista alloc-luokkametodilla:
NSMutableArray *taulukko = [NSMutableArray alloc];

Nyt meillä on olemassa muistipaikan osoite, johon mahtuu yksi NSMutableArray-tyyppinen olio. Oliota ei kuitenkaan ole vielä olemassa, joten olion konstruktori-metodia pitää kutsua:
[taulukko init];

Tämän kaiken voi tietenkin tehdä yhdellä rivillä:
NSMutableArray *taulukko = [[NSMutableArray alloc] init];

Olion metodien kutsuminen

Olion luomisen yhteydessä näimmekin jo miten luokan metodia ja olion parametritöntä metodia kutsutaan. Metodille voi tietenkin antaa myös parametrejä. Kutsutaan taulukko-olion addObject-metodia ja annetaan parametriksi tavara-olio:
[taulukko addObject:tavara];

Parametrejä voi olla myös useampia. Syntaxi näyttää hieman monimutkaiselta:
[taulukko insertObject:tavara atIndex:4];

Koodista saadaan hieman luettavampaa, jos eri paremetrin laitetaan eri riveille:

[taulukko insertObject:tavara 
               atIndex:4]

Muistinhallinta

Objective-C:n uudemmassa 2.0-versiossa, joka julkaistiin Mac OS X 10.5 mukana, on javan tapaan automaattinen muistinhallinta. Vanhemmassa versiossa on "puoliautomaattinen" muistinhallinta, joka perustuu retain-laskuriin.

Jokainen Objective-C-kielen luokka on peritty NSObject-luokasta, joka sisältää retain-count-attribuutin, sekä retain- ja release-metodit. Kun oliolle varataan tilaa alloc-metodilla, retain-laskuri alustetaan arvoon 1, joka ilmoittaa kyseisen olion olevan vielä käytössä.

Kun oliota tarvitaan jossain, kutsutaan retain-metodia:
[taulukko retain];
, joka kasvattaa laskuria yhdellä.

Kun olio tulee tarpeettomaksi, kutsutaan release-metodia:
[taulukko release];
, joka vähentää laskuria yhdellä. Kun laskuri saavuttaa arvon 0, kukaan ei ole enää kiinnostunut oliosta. Olion dealloc-metodia kutsutaan automaattisesta ja olion varaama tila vapautetaan.

retain- ja release-metodeja kutsutaan automaattisesti esimerkiksi silloin, kun olio lisätä NSMutableArray-taulukkoon tai muuhun säilö-olioon.

Autorelease

retain/release-metodian kanssa tulee kuitenkin joskus vastaan ongelmia. Jos metodi esim. palauttaa uuden merkkijono-olion, mihin arvoon retain-count pitäisi asettaa. Mikäli arvoksi jätetään alloc-metodin asettama 1, jää olion vapauttaminen metodin kutsujan tehtäväksi. Tämä tekee paljon turhaa koodia mikäli kutsuja ei ole kiinnostunut paluuarvosta. Tätä varten NSObject-luokassa on autorelease-metodi:
[taulukko autorealease];

Jokaisessa cocoa-ohjelmassa on olemassa NSAutoreleasePool-tyyppinen olio, joka huolehtii tälläisten olioiden automaattisesta vapauttamisesta. Kun olion autorelease-metodia kutsutaan, olio lisätään autoreleasePooliin. Kun ohjelman käyttäjän aloittama toiminto on suoritettu loppuun, autoreleasePool kutsuu jokaisen itseensä lisätyn olion release-metodia. Tällä tavalla käyttämättömät oliot saadaan kätevästi tuhottua melkein kuin itsestään.

Omien luokkien luominen

Luodaan seuraavaksi yksinkertainen komentorivistä ajettava ohjelma. Koodassa on muutamia asioita, joihin palaan uudelleen oppaan loppuosassa.

Luodaan uusi XCode-projekti

  1. Avaa XCode
  2. File -> New Project
  3. Valitse Command Line Utility / Foundation Tool
  4. Annetaan projektille nimeksi vaikkapa "Puhelinluettelo"

Luodaan oma Henkilo-luokka

Luodaan ensin Henkilo-niminen luokka, jolla on 3 attribuuttia: nimi, osoite ja puhelinnumero. Toteutetaan luokalle konstruktori-metodi ja attributtien get/set-metodit. Luokka on C ja C++ kielien tapaan jaettu kahteen erilliseen tiedostoon: header-tiedosto (*.h) sisältää luokan määritykset ja koodi-tiedoston (*.m) sisältää koodin.

  1. Klikkaa Sources-kansiota oikealla napilla ja valitse Add -> New File
  2. Valitse Objective-C class ja anna sille nimeksi "Henkilo.m"
  3. Avaa Henkilo.h-tiedosto ja muokkaa sen sisältö:
    #import <Cocoa/Cocoa.h>
    
    @interface Henkilo : NSObject {
        NSString *nimi;
        NSString *osoite;
        NSString *puhNum;
    }
    
    -(id)initWithNimi:(NSString*)nimi osoite:(NSString*)osoite puhelinNumero:(NSString*)numero;
    
    -(NSString*)nimi;
    -(NSString*)osoite;
    -(NSString*)puhNum;
    
    -(void)setNimi:(NSString*)uusiNimi;
    -(void)setOsoite:(NSString*)uusiOsoite;
    -(void)setPuhNum:(NSString*)uusiPuhNum;
    
    @end
    
    • #import -esikäsittelijäkomennolla otimme mukaan kaikki Cocoa-komponentit. #import <Foundation/Foundation.h> olisi ehkä tähän hieman fiksumpi valita, koska se sisältäisi kaiken tässä esimerkissä tarvittavan.
    • Tämän jälkeen esiteltiin uusi Henkilo-luokka, joka perittiin NSObject-luokasta
    • Henkilo-luokalla on kolme attribuuttia: nimi, osoite ja puhNum. Ne kaikki ovat osoittimia NSString-tyyppisiin olioihin. Kaikki attribuutit ovat oletuksena protected-tyyppisiä. Näkyvyyteen voisi vaikuttaa @private, @protected ja @public -avainsanoilla.
    • Seuraavaksi määriteltiin luokan konstruktori-metodi, joka ottaa parametreikseen 3 NSString-merkkijono-oliota
    • Lopuksi määriteltiin attribuuttien get- ja set-metodit. Huomaa get/set-metodien muoto! Cocoan Objective-C toteutus olettaa, että:
      • get-metodin nimi on sama kuin attribuutin nimi
      • set-metodin nimi on muotoa set + attribuutin nimi ensimmäinen kirjain pienellä
      • On olemassa myös getAttribuutti-muotoinen esitys. Tämä on kuitenkin varattu hieman erilaisella toiminnolle. Esim:
        CGFloat red, greed, blue, alph;
        [color getRed:&red green:&green blue:&blue alpha:&alph];
        

        , jossa metodille välitetään niiden muuttujien osoitteet, joihin metodin halutaan tallentavan halutut arvot.

  4. Tämän jälkeen voidaankin kirjoittaa itse luokan toteutus. Muokkaa Henkilo.h:
    #import "Henkilo.h"
    
    // Luokan toteutus tulee @implementation osan sisälle 
    @implementation Henkilo
    
    // konstruktori-metodi
    -(id)initWithNimi:(NSString*)nimi osoite:(NSString*)osoite puhelinNumero:(NSString*)numero
    {
        // Kutsutaan isä-luokan (NSObject) konstruktori-metodia
        self = [super init];
    
        // Alustetaan attribuutit
        [self setNimi:nimi];
        [self setOsoite:osoite];
        [self setPuhNum:numero];
        
        // init-metodi palauttaa osoittimen itseensä
        return self;
    }
    
    // Tuhoojametodi
    -(void)dealloc
    {
        // Vapautetaan kaikki attribuutit
        [nimi release];
        [osoite release];
        [puhNum release];
    }
    
    // Attribuuttien saantimetodit
    -(NSString*)nimi
    {
        return nimi;
    }
    
    -(NSString*)osoite
    {
        return osoite;
    }
    
    -(NSString*)puhNum
    {
        return puhNum;
    }
    
    // Attribuuttien set-metodit
    -(void)setNimi:(NSString*)uusiNimi
    {
        // Kasvatetaan uuden arvon retain-laskuria
        [uusiNimi retain];
        
        // Vähennetään vanhan arvon retain-laskuria
        [nimi release];
        
        // Huomaa järjestys! Mikäli vapauttaisit vanhan arvon ensin ja 
        // uusiNimi sattuisi osoittamaan samaan NSString-olioon kuin nimi, 
        // saisit aikaan paljon ongelmia. 
        
        nimi = uusiNimi;
    }
    
    -(void)setOsoite:(NSString*)uusiOsoite
    {
        [uusiOsoite retain];
        [osoite release];
        osoite = uusiOsoite;
    }
    
    -(void)setPuhNum:(NSString*)uusiPuhNum
    {
        [uusiPuhNum retain];
        [puhNum release];
        puhNum = uusiPuhNum;
    }
    
    @end
    

Ohjelman main-funktion toteutus

Foundation Tool -ohjelma on sen verran yksinkertainen, että sen toiminta lähtee tavallisten C-ohjelmien tapaan liikkeelle main-funktiosta. Toteutetaan seuraavaksi main-funktio ja tehdään Henkilo-luokaan vielä pari tarpeellista muutosta.

  1. Avaa Puhelinluettelo.m ja muokkaa se seuraavasta
    #import <Foundation/Foundation.h>
    #include "Henkilo.h"
    
    int main (int argc, const char * argv[]) {
        // Luodaan ensin AutoreleasePool huolehtimaan autorelease-olioista
        NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init];
    
        // Luodaan taulukko henkiloita varten
        NSMutableArray *henkilot = [[NSMutableArray alloc] init];
        
        // Luodaan uusi Henkilo-olio
        Henkilo *lisattava = [[Henkilo alloc] initWithNimi:@"Matti Meikäläinen"
                                                    osoite:@"Testiosoite 13"
                                             puhelinNumero:@"1234567890"];
        // Lisätään olio taulukkoon
        [henkilot addObject:lisattava];
        // Vapautetaan henkilö (taulukko hoitaa retain/release automaattisesti)
        [lisattava release];
        
        // Luodaan vielä 2 muuta henkilöä käyttäen samaa väliaikaismuuttujaa
        lisattava = [[Henkilo alloc] initWithNimi:@"Testi Testikäyttäjä"
                                           osoite:@"Kaakaotie 55"
                                    puhelinNumero:@"6459394837"];
        [henkilot addObject:lisattava];
        [lisattava release];
        
        lisattava = [[Henkilo alloc] initWithNimi:@"Aku Ankka"
                                           osoite:@"Testiosoite 14"
                                    puhelinNumero:@"00100010001001"];
        [henkilot addObject:lisattava];
        [lisattava release];
        
        // Käydään taulukon sisältö läpi ja tulostetaan ne
        int i;
        for(i = 0; i < [henkilot count]; i++)
            // NSLog tulostaa konsoliin (vertaa printf c ja java)
            NSLog(@"%@", [henkilot objectAtIndex:i]);
        
        [henkilot release];
        
        // Vapautetaan autoreleasePool
        [pool drain];
        
        return 0;
    }
    
  2. Voit koitaa ajaa ohjelma tähän kohtaa: Run -> Console -> Build and Go... Henkilöt tulostuvat kuitenkin hieman oudosti. Näkyviin tulee vain olion luokan nimi ja muistipaikan osoite. Jotta saisimme Henkilo-olion tulostamaan itsensä oikealla tavalla, meidän täytyy lisätä Henkilo.m-tiedostoon seuraava metodi:
    // NSObject-luokassa on description-metodi, jonka tehtävänä on muuttaa olio merkkijonoksi
    // Uudelleenmääritellään se tähän kohtaa
    -(NSString*)description
    {
        NSString *tulos;
        // Kutsutaan NSString luokan-metodia. Palauttaa autorelease-merkkijono-olion
        tulos = [NSString stringWithFormat:@"Nimi: %@, Osoite: %@, puhelin: %@", [self nimi], [self osoite], [self puhNum]];
        return tulos;
    }
    

Luokan metodin toteuttaminen

  1. Lisää henkilo.h tiedostoon seuraava määrittely:
    // Luokan metodi, joka palauttaa autorelease Henkilo-olion
    // + -merkki alussa määrittelee metodin luodan metodiksi (vastaa javan static-metodia)
    +(id)henkiloWithNimi:(NSString*)nimi osoite:(NSString*)osoite puhelinNumero:(NSString*)numero;
    
  2. Lisää henkilo.m tiedostoon seuraava toteutus:
    // Luokan metodi, joka palauttaa autorelease Henkilo-olion
    +(id)henkiloWithNimi:(NSString*)nimi osoite:(NSString*)osoite puhelinNumero:(NSString*)numero
    {
        Henkilo *uusi = [[Henkilo alloc] initWithNimi:nimi osoite:osoite puhelinNumero:numero];
        [uusi autorelease];
        return uusi;
    }
    
  3. Nyt voisit muokata Puhelinluettelo.m tiedostoa seuraavasti:
        // Luodaan uusi Henkilo-olio hyödyntäen luokan metodia
        Henkilo *lisattava = [Henkilo henkiloWithNimi:@"Matti Meikäläinen"
                                               osoite:@"Testiosoite 13"
                                        puhelinNumero:@"1234567890"];
    

Muutama hyödyllinen Objective-C luokka

NSObject

Kaikki Objective-C-luokat peritään NSObject-luokasta. Objective-C luokat ovat aika monimutkaisia, koska ne sisältävät esim. muistinhallinnan ja useita Cocoaan liittyviä ominaisuuksia. init, retain-, release- ja description- metodeihin tuossa jo tutustuttiinkin. Näiden lisäksi voisi mainita vielä isEqual:-metodin, joka ottaa paremetrikseen toisen olion ja palauttaa true mikäli oliot ovat samat. isEqual on uudelleenmääritelty useiden luokkien kohdalla, esim NSString palauttaa true mikäli merkkijonojen sisältö on sama.

NSString

Objective-C:ssä merkkijonot voivat olla joko C-kielen char-taulukoita tai NSString-tyyppisiä olioita. NSString tyyppiset merkkijono esitellään @-merkin kanssa:
NSString *esim = @"Testi merkkijono";

Merkkijono voidaan alustaa myös hieman monipuolisemmin initWithFormat:-metodilla:
NSString *esim = [[NSString alloc] initWithFormat:@"Tämä on %d. opas, jonka kirjoitan %@ -sivustolle", 2, @"macintoast.org";
Tästä syntyy merkkijono: "Tämä on 2. opas, jonka kirjoitan macintoast.org -sivustolle"

length-metodi palauttaa merkkijonon pituuden: [merkkijono length]

NSArray

NSArray on luokka, johon voidaan tallentaa taulukkomaisesti toisia olioita. Indeksointi alkaa C-taulukon tapaan nollasta. NSArrayn sisältöä ei voi muokata sen luomisen jälkeen. Uusi taulukko luodaan luettelemalla taulukkoon tulevat alkiot:
NSArray *taulukko = [NSArray arrayWithObjects: eka, toka, kolmas, nil];

NSArrayn alkioihin pääsee käsiksi objectAtIndex:-metodilla. Haetaan 3 alkio:
id alkio = [taulukko objectAtIndex:2];

Alkioiden määrän saa selville count-metodilla: int maara = [taulukko count];

NSMutableArray

NSMutableArray eroaa NSArraystä Mutable-avainsanalla, joka kertoo olion olevan muokattava luomisensa jälkeen. objectAtIndex:- ja count -metodin toimivat samalla tavalla kuin NSArrayssa.

Lisätään uusi alkion NSMutableArray loppuun: [taulukko addObject:alkio];
Lisätään uusi alkion NSMutableArray indeksiin 5: [taulukko insertObject:alkio atIndex:5];
Poistetaan alkio indeksistä 5: [taulukko removeObjectAtIndex:5];
Tyhjätään NSMutableArray: [taulukko removeAllObjects];

NSNumber

NSNumber pitää sisällään numeroarvon. Numeroarvo voi olla tyypiltään signed tai unsigned char, short int, int, long int, long long int, float, double tai jopa BOOL-arvo. Luodaan double-tyyppisen liukuluvun sisältävä NSNumber-olio:
NSNumber luku = [NSNumber numberWithDouble:5.65];
ja kysytään olion sisältämä double-luku:
double a = [luku doubleValue];

Muuta Objective-C asiaa

NSLog-funktio

NSLog-funktion avulla voidaan tulostaa Objective-C -ohjelmasta konsoliin. Funktio muistuttaa toiminnaltaan paljon C:n ja javan printf-funktioita. NSLog ottaa vastaan merkkijonon ja äärettömän määrän parametrejä: NSLog(@"formatoitava merkkijono", ...);

Esim: NSLog(@"Tämä on %d. opas, jonka kirjoitan %@ -sivustolle", 2, @"macintoast.org");
tulostaa: "Tämä on 2. opas, jonka kirjoitan macintoast.org -sivustolle"

  • %@ tulostaa olion description-metodin palauttavan NSString-merkkijonon
  • %d tulostaa long-tyyppisen muuttujan
  • %x tulostaa long-tyyppisen muuttujan hexana
  • %o tulostaa long-tyyppisen muuttujan octal-lukuna
  • %f tulostaa double-tyyppisen muuttujan
  • %c tulostaa char-muuttujan merkkinä
  • %s tulostaa null-loppuisen ASCII-merkkijonon
  • %S tulostaa null-loppuisen UNICODE-merkkijonon
  • Lisää löytyy manuaaleista

Tyypit ja vakiot

Objective-C sisältää muutaman tietotyypin ja vakion, joita ei löydy sellaisenaan C-kielestä:

  • id on osoitin mihin tahansa olioon
  • BOOL on char-tyyppinen, mutta sitä käytetään boolean-arvona
  • YES kääntyy 1
  • NO kääntyy 0
  • Seuraavia käytetään Cocoassa:
    • IBOutlet on esikäsittelijä-makro, joka kääntyy tyhjäksi. Toimii vinkkinä Interface Builderille.
    • IBAction on esikäsittelijä-makro, joka kääntyy void:ksi. Toimii vinkkinä Interface Builderille.
  • nil on sama kuin NULL

Uutta Objective-C 2.0:ssa

Uutta Objective-C 2.0 toi mukanaan muutamia käteviä uudistuksia. Hyödyllisin näistä on varmasti automaattinen muistinhallinta, jonka ansiosta retain/release-sotkun saa jätettyä kokonaan pois. Tällöin täytyy kuitenkin muistaa, että automaattinen muistinhallinta toimii vain Mac OS X 10.5 käyttiksessä. Automaattisen muistinhallinnan saa myös toimimaan C-tyyppien ja taulukoiden kanssa, mutta se vaatii hieman enemmän huomiota. Tästä lisää joskus jossain toisessa oppaassa...

Automaattinen muistinhallinta kytketään päälle ja pois: Project -> Edit Project Settings -> Build / Objective-C Garbage Collection.

@property ja @synthesize

Toinen hyödyllinen koodin määrää vähentävä ominaisuus on @property ja @synthesize. Niiden avulla get/set-metodit voidaan totauttaa melkein automaattisesti. Muutetaan Puhelinluettelo käyttämään näitä:

  1. Poista Henkilo.h tiedostosta get/set metodien esittelyt ja korvaa ne näillä:
    @property (readwrite, retain) NSString *nimi;
    @property (readwrite, retain) NSString *osoite;
    @property (readwrite, retain) NSString *puhNum;
    
  2. Poista Henkilo.m tiedostosta get/set metodien toteutukset ja korvaa ne näillä:
    @synthesize nimi;
    @synthesize osoite;
    @synthesize puhNum;
    
  3. Nyt ohjelman pitäisi toimia aivan niinkuin ennenkin. @property ja @synthesize huolehtivat get/set-metodian toteuttamisesta. Voit tietenkin edelleen uudelleenmääritellä metodit *.m-tiedoston puolella mikäli haluat.

@property paremetrit:

  • readonly Toteuttaa vain get-metodit
  • readwrite Totettuttaa get- ja set-metodit
  • assign Ei toteuta retain/release
  • retain Totetuttaa retain/release
  • copy Palauttaa get-metodilla kopion attribuutista