Nastal čas na kakao - Inicializace: tipy a triky - MujMAC.cz - Apple, Mac OS X, Apple iPod

Odběr fotomagazínu

Fotografický magazín "iZIN IDIF" každý týden ve Vašem e-mailu.
Co nového ve světě fotografie!

 

Zadejte Vaši e-mailovou adresu:

Kamarád fotí rád?

Přihlas ho k odběru fotomagazínu!

 

Zadejte e-mailovou adresu kamaráda:

Seriály

Více seriálů



Informace

Nastal čas na kakao - Inicializace: tipy a triky

20. srpna 2004, 00.00 | Než pokročíme k dalšímu tématu (jímž bude správné psaní tzv. accesorů – zběžně je ostatně "naťukneme" již dnes), vyplatí se ještě si ukázat několik triků a správných vzorců při implementaci a užívání inicializátorů. Nakonec si také ukážeme jeden potenciální "podraz", na který si při implementaci inicializátorů musíme – aspoň v těch složitějších případech – trošku dávat pozor.

Než pokročíme k dalšímu tématu (jímž bude správné psaní tzv. accesorů – zběžně je ostatně "naťukneme" již dnes), vyplatí se ještě si ukázat několik triků a správných vzorců při implementaci a užívání inicializátorů. Nakonec si také ukážeme jeden potenciální "podraz", na který si při implementaci inicializátorů musíme – aspoň v těch složitějších případech – trošku dávat pozor.

Alokace na vyžádání

Až dosud jsme si ukazovali "klasický" vzorec, při němž jsou vložené objekty a další zdroje alokovány v metodě init (přesněji, v designovaném inicializátoru, jímž může být init nebo některá z metod initWith...) a uvolněny v metodě dealloc. To je samozřejmě dobře a správně; existují však situace, kdy je šikovnější zdroje alokovat ne hned při vytvoření objektu, ale teprve ve chvíli, kdy jsou skutečně zapotřebí.

Princip je jednoduchý – využijeme prostě toho, že instanční proměnné objektu jsou automaticky nulovány ve chvíli, kdy je objekt standardní zprávou alloc vytvořen, a v designovaném inicializátoru je necháme tak. Na začátku metod, jež s instančními proměnnými pracují, pak prostě ověříme zda patřičná instanční proměnná obsahuje potřebný objekt, a pokud tomu tak není, teprve ji naplníme.

Ukažme si typický příklad objektu, který obsahuje nějaké přiřazení jmen a hodnot, lhostejno jaké. Náš objekt bude pro tento účel obsahovat vložený objekt třídy NSMutableDictionary, k němuž se bude přistupovat pomocí interních metod valueForKey: a setValue:forKey:. Pro lepší ilustraci si nejprve ukážeme tradiční implementaci:

@interface Test:NSObject {
  NSMutableDictionary *dict;
  ...
}
...
@end
@implementation Test
-init {
  if (!(self=[super init])) return nil;
  dict=[[NSMutableDictionary alloc] init];
  ...
  return self;
}
-(void)dealloc {
  ...
  [dict release];
  [super dealloc];
}
...
-valueForKey:key {
  return [dict valueForKey:key];
}
-(void)setValue:value forKey:key {
  [dict setValue:value forKey:key];
}
...
@end

Většinou se však v takovýchto případech vyplatí nevytvářet vložené objekty typu našeho pomocného slovníku dict hned při inicializaci, ale ponechat to až na chvíli, kdy je to skutečně zapotřebí. Je to tím praktičtější, čím je větší pravděpodobnost, že za určitých okolností se bude s naším objektem pracovat aniž by se vůbec vložený objekt použil, ale také tím praktičtější, čím je vložený objekt náročnější na paměť a/nebo čím je jeho vlastní inicializace komplikovanější.

V praxi tedy velmi často podobný kód píšeme trochu jinak – ukažme si pouze metody, jejichž obsah se změní:

...
-init {
  if (!(self=[super init])) return nil;
  ...
  return self;
}
...
-(void)setValue:value forKey:key {
  if (!dict) dict=[[NSMutableDictionary alloc] init];
  [dict setValue:value forKey:key];
}
...

V extrémním případě, kdy na vyžádání alokujeme všechny zdroje, jež náš objekt používá, se dokonce může stát, že vůbec nebudeme potřebovat vlastní reimplementaci metody init: byla by totiž prázdná, všechna inicializace se provede až ve chvíli, kdy je to skutečně zapotřebí. To je jedna z mála situací, kdy implementujeme pouze jednu z metod init/dealloc a ne obě společně.

Mimochodem, je vhodné si uvědomit, že má-li takovýto kód být korektní, musíme si dávat velký pozor na použití proměnné dict: víceméně platí, že bychom ji neměli nikde používat přímo, jen prostřednictvím pomocných metod valueForKey: (jejíž případné volání dříve, než byla proměnná inicializována, bude korektní díky tomu, že v Objective C můžeme hodnotě nil zaslat libovolnou zprávu a výsledek je opět nil) a setValue:forKey:. Kromě toho je v tomto případě ovšem korektní i použití v metodě dealloc; bylo ale třeba si to explicitně uvědomit.

My se tomuto "odstínění" či "zapouzdření" instančních proměnných pomocí speciálních přístupových metod budeme podrobněji věnovat příště, až si budeme povídat o tzv. accesorech.

Singleton a sdružená třída

Minule jsme se zmínili, že to, že metoda init může vracet jiný objekt, než který odpovídající zprávu přijal, se nejčastěji využívá při implementaci tzv. singletonů (tříd, jež mají jen jedinou sdílenou instanci) a sdružených tříd (jež namísto vlastních instancí při vytváření nových objektů vracejí instance skrytých podtříd).

Aniž bychom se zdržovali příliš podrobným výkladem, vyplatí se ukázat si alespoň rámcový příklad kódu metody init pro oba tyto případy:

@implementation Singleton
-init {
  static Singleton *myself=nil;
  if (myself) {
    [self release]; //*
    return myself;
  }
  if (!(self=[super init])) return nil;
  ...
  return myself=self;  
}

Za zmínku stojí řádek, označený hvězdičkou: samozřejmě zde použijeme release (a nikoli autorelease), protože v tomto specifickém případě je skutečně zcela zřejmé, že chceme objekt uvolnit okamžitě.

Je také třeba si uvědomit, že uvolňujeme objekt, který dosud nebyl korektně inicializován (neboť jsme dosud neprovedli "[super init]"). Naprostá většina singletonů je dědicem třídy NSObject, kdy to v praxi příliš nevadí (a naopak je žádoucí "ničím se nezdržovat" a uvolnit objekt co nejdříve); proto uvádíme tuto variantu. Ve zcela obecném případě to však je mírně nekorektní, takže řada programátorů raději celý blok "if (myself)..." přemísťuje až na začátek vlastní inicializace, označené třemi tečkami.

Implementace metody init... ve sdružené třídě je ještě jednodušší – prostě vždy odstraní objekt, který zprávu přijal, a namísto něj vrátí objekt vhodné podtřídy. Metoda init bez argumentů by tedy byla zcela triviální; ukážeme si namísto toho princip implementace metody initWithSize:, jež zvolí vhodnou podtřídu podle požadované velikosti:

@interface Cluster:... @end // sdružená třída, veřejné API
@interface Under1000:Cluster ... @end // skrytá podtřída pro malé velikosti
@interface Over1000:Cluster ... @end // skrytá podtřída pro velké velikosti
...
@implementation Cluster
-initWithSize:(int)size {
  [self release];
  if (size<1000) return [[Under1000 alloc] initWithSize:size];
  return [[Over1000 alloc] initWithSize:size];
}

Pozor na částečně inicializované objekty!

Mechanismus inicializace, který prostřednictvím metod init... nabízí Objective C a Cocoa, je mnohem flexibilnější, než konstruktory jazyků typu C++ či Java. Nic ovšem není zadarmo: flexibilita inicializátorů, jmenovitě nesmírně praktická možnost jejich volného dědění, může přinést při nevhodném použití problémy. Konkrétně, může se nám stát, že je volána některá z metod objektu dříve, než je objekt plně inicializován.

Vzpomeňme si na strukturu designovaných inicializátorů a jejich dědění z minulého dílu; dejme tomu, že máme třídy Test a jejího dědice Pokus, jež obě mají jediný (designovaný) inicializátor init, a ten že ve třídě Test používá nějakou pomocnou metodu, dejme tomu foobar. Pokud pak metodu foobar třída Pokus reimplementuje, můžeme narazit – podívejme se na konkrétní kód:

@implementation Test
-(void)foobar { ... }
-init {
  if (!(self=[super init])) return nil;
  [self foobar];
  return self;
}
@end
@implementation Pokus:Test {
  NSMutableArray *array;
}
-(void)foobar {
	[array addObject:@"FUBAR!"]; //*
}
-init {

  if (!(self=[super init])) return nil; //**
  array=[[NSMutableArray alloc] init];
  return self;
}
@end

Tento kód nejspíš nebude fungovat tak, jak chceme – protože metoda foobar se zavolá dříve, než proběhne kompletní inicializace třídy Pokus, takže v době jejího volání bude ještě proměnná array obsahovat hodnotu nil, a tudíž příkaz "addObject:" neudělá zhola nic.

Je zřejmé, jak k tomu dojde? Sledujte se mnou:

  • nově vytvořený, prázdný objekt třídy Pokus dostane zprávu init;
  • odpovídající metoda nejprve volá "[super init]", tedy metodu init třídy Test;
  • ta ovšem (po inicializaci nadtřídy) hned pošle sama sobě zprávu foobar;
  • jsme tedy v metodě foobar třídy Pokus na řádku označeném hvězdičkou; v inicializaci jsme se však dosud nedostali dále, než na řádek označený dvěma hvězdičkami – speciálně, proměnná array dosud nebyla naplněna.

Samozřejmě, to je jen další důvod používat alokaci na vyžádání, s níž jsme se seznámili na začátku dnešního článku: kdyby bývala metoda foobar začínala příkazem "if (!array) array=[[NSMutableArray alloc] init]", vše by bylo v nejlepším pořádku.

Obsah seriálu (více o seriálu):

Tématické zařazení:

 » Rubriky  » Informace  

 » Rubriky  » Agregator  

 » Rubriky  » Začínáme s  

Diskuse k článku

 

Vložit nový příspěvek   Sbalit příspěvky

 

Singletony

Autor: ANo Muž

Založeno: 25.08.2004, 16:35
Odpovědí: 0

Nevim, jestli jsem to s temi singletony dobre pochopil:

Uvedena modelova implementace umoznuje vicenasobne volani [[Singleton alloc] init], ale neumoznuje stejny pocet zaslani release vracenym intancim, coz je IMO spatne.

Podle me

- by vicenasobne vytvoreni instance melo vyvolat vyjimku a misto toho by mela existovat metoda +sharedSingleton, obdobne jako je tomu u NSApplication a jinych trid se sdilenymi instancemi v Cocoa;

nebo

- by pripadne implementace -init mela obsahovat "return [myself retain];" misto pouheho "return myself;" (nic moc, ale prijde mi to porad konsistentnejsi, nez priklad v clanku).

At uz je ma pripominka spravna, nebo ne, jeste by me zajimalo, jak by mely byt implementovany metody +sharedXXX, +alloc, -init (tzn. ktera zavisi na ktere, kde se kontroluje, zda jiz sdilena instance neexistuje, kde se inicializuje, jak je to s dedenim) u singletonu a la Cocoa (jako NSApplication).

Diky za pripadne osvetleni problematiky a vubec za skvely serial!

Odpovědět na příspěvek

RE: Singletony

Autor: OC Muž

Založeno: 26.08.2004, 19:39

No, ono je to malinko složitější; napsat singletona znamená se postarat *o více věcí*, z nichž jsem (v rámci právě probírané tématiky) ukázal jen jednu. A já se *moc omlouvám*, že jsem to zapomněl zdůraznit.

Druhá by byla nabídnout rozumnou metodu třídy (to je to Vaše +sharedSingleton). Třetí je postarat se o to, aby se nám singleton neztratil; Vámi navržené řešení "return [self retain]" není zlé, ale obecně lepší (protože "neprůstřelnější") je reimplementovat release, nějak zhruba takto:

-(void)release {
if (self!=myself) [super release];
}

A tak dále. Ohledně implementace dalších metod: +alloc se (až na naprosté výjimky) nereimplementuje; mělo by se tedy ponechat beze změny. Inicializuje se vždy v init (initWith...). Rozhodování, zda sdílená instance existuje nebo ne, může být buď v init(With), jak jsem ukázal v článku, nebo v +shared..., takto:

+(Singleton*)sh
aredSingleton {
static Singleton *singleton=nil;
if (!singleton) singleton=[[self alloc] init];
return singleton;
}

Zálež
spíš na tom, co je v konkrétním případě vhodnější, zda může mít výjimečně smysl vytvořit druhou instanci nebo ne, zda se předpokládá, že se instance bude dědit nebo ne, zda je v API určeno, že jde o singleton, nebo zda to má být jen skrytý implementační detail, a podobně.

Odpovědět na příspěvek

 

 

Odpověď na příspěvek:

No, ono je to malinko složitější; napsat singletona znamená se postarat *o více věcí*, z nichž jsem (v rámci právě probírané tématiky) ukázal jen jednu. A já se *moc omlouvám*, že jsem to zapomněl zdůraznit.

Druhá by byla nabídnout rozumnou metodu třídy (to je to Vaše +sharedSingleton). Třetí je postarat se o to, aby se nám singleton neztratil; Vámi navržené řešení "return [self retain]" není zlé, ale obecně lepší (protože "neprůstřelnější") je reimplementovat release, nějak zhruba takto:

-(void)release {
if (self!=myself) [super release];
}

A tak dále. Ohledně implementace dalších metod: +alloc se (až na naprosté výjimky) nereimplementuje; mělo by se tedy ponechat beze změny. Inicializuje se vždy v init (initWith...). Rozhodování, zda sdílená instance existuje nebo ne, může být buď v init(With), jak jsem ukázal v článku, nebo v +shared..., takto:

+(Singleton*)sharedSingleton {
static Singleton *singleton=nil;
if (!singleton) singleton=[[self alloc] init];
return singleton;
}

Záleží spíš na tom, co je v konkrétním případě vhodnější, zda může mít výjimečně smysl vytvořit druhou instanci nebo ne, zda se předpokládá, že se instance bude dědit nebo ne, zda je v API určeno, že jde o singleton, nebo zda to má být jen skrytý implementační detail, a podobně.


Jméno:

Pohlaví:

,

E-mail:

Předmět:

Příspěvek:

 

Kontrola:

Do spodního pole opište z obrázku 5 znaků:

Kód pro ověření

 

 

 

 

Nejčtenější články
Nejlépe hodnocené články
Apple kurzy

 

Přihlášení k mému účtu

Uživatelské jméno:

Heslo: