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:

Soutěž

Sponzorem soutěže je:

IDIF

 

Odkud pochází fotografka Anne Erhard?

V dnešní soutěži hrajeme o:

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  

 

 

 

 

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

Uživatelské jméno:

Heslo: