Programování v prostředí Cocoa (11) Třídy Foundation Kitu II. Minule jsme se začali na konkrétních příkladech seznamovat s použitím nejběžnějších tříd Foundation Kitu. Dnes si ukážeme několik dalších příkladů. NS(Mutable)Dictionary Hašovací tabulky, jež Foundation Kit nabízí prostřednictvím tříd NSDictionary a NSMutableDictionary, patří mezi nejužívanější služby vůbec: jsou totiž nesmírně pohodlné a flexibilní. Připomeňme si minulý příklad, ve kterém jsme využili NSCountedSet pro frekvenční analýzu daného textu. Dnes si ukážeme analogický příklad, v němž nám poslouží třída NSMutableDictionary pro vytvoření rejstříku. Podobně jako minule si zároveň ukážeme funkci a služby několika dalších tříd – příkladem bude kompletní program. Náš prográmek vytvoří kompletní index všech souborů HTML v zadané složce a ve všech složkách vnořených – pro každé slovo udělá seznam všech dokumentů, ve kterých je toto slovo využito. Program bude o něco luxusnější než minulý příklad; obsahuje třeba dekódování argumentů příkazového řádku. Nejprve se podíváme na zdrojový text (kompletní program zabere méně než 60 řádků), a pak si některé příkazy vysvětlíme podrobněji: int main (int argc, const char *argv[]) { NSAutoreleasePool *pool=[[NSAutoreleasePool alloc] init]; NSCharacterSet *wordDelims=[NSCharacterSet characterSetWithCharactersInString:@" ,.?!;:\"\'/-()0123456789*#@\\\r\n"]; NSFileManager *fm=[NSFileManager defaultManager]; NSUserDefaults *df=[NSUserDefaults standardUserDefaults]; NSString *ifolder=nil,*ofile=nil; NSMutableDictionary *index=[NSMutableDictionary dictionary]; NSMutableString *output=[NSMutableString string]; int minword=3,totalf=0,totalw=0,totalb=0; id en,o; ifolder=[df objectForKey:@"input"]; // *1* ofile=[df objectForKey:@"output"]; if (o=[df objectForKey:@"minword"]) minword=[o intValue]; if (!ifolder || !ofile) { printf("IndexHTML [-minword ] -input -output \n"); exit(0); } for (en=[fm enumeratorAtPath:ifolder];o=[en nextObject];) if ([[o pathExtension] isEqual:@"html"]) { // *2* NSAutoreleasePool *pool=[[NSAutoreleasePool alloc] init]; NS_DURING // *3* NSString *fname=[o lastPathComponent]; NSAttributedString *htmlContents=[[[NSAttributedString alloc] initWithPath:[ifolder stringByAppendingPathComponent:o] documentAttributes:NULL] autorelease]; // *4* NSString *contents=[htmlContents string]; NSScanner *sc=[NSScanner scannerWithString:contents]; int len=[[en fileAttributes] fileSize],current=totalw,currentItems=[index count]; NSLog(@"Scanning \"%@\" (%d bytes)...",fname,len); totalf++; totalb+=len; while (![sc isAtEnd]) { NSString *word; [sc scanCharactersFromSet:wordDelims intoString:NULL]; // skip any delimiters if ([sc scanUpToCharactersFromSet:wordDelims intoString:&word] && [word length]>minword) { NSMutableSet *s=[index objectForKey:word]; // *5* if (!s) [index setObject:s=[NSMutableSet set] forKey:word]; [s addObject:fname]; totalw++; } } NSLog(@"...%d words (%d new items)",totalw-current,[index count]-currentItems); NS_HANDLER NSLog(@"*** aborted since %@",[localException reason]); NS_ENDHANDLER [pool release]; } NSLog(@"Scanned %d files (%d words, %d bytes)",totalf,totalw,totalb); for (en=[[[index allKeys] sortedArrayUsingSelector:@selector(compare:)] objectEnumerator];o=[en nextObject];) // *6* [output appendFormat:@"%@: %@\n",o,[[index objectForKey:o] allObjects]]; if (![output writeToFile:ofile atomically:NO]) NSLog(@"!!! Can't write %@",ofile); NSLog(@"Index successfully written to \"%@\"",ofile); [pool release]; exit(0); return 0; } Na řádku s komentářem *1* začíná zpracování vstupních argumentů. Bylo by zbytečné na tomto místě podrobně popisovat služby třídy NSUserDefaults. Za stručnou zmínku však stojí to, že kromě dekódování příkazového řádku zajišťuje přístup k velmi obecné databázi uživatelských předvoleb. Bez dalšího programování máme tedy k dispozici nejen příkazový řádek, ale můžeme i fixovat standardní hodnoty argumentů v této databázi a náš program je automaticky využije. Hlavní důvod, proč se zde třídou NSUserDefaults vůbec zabýváme, však je ilustrace jedné z velmi příjemných vlastností API Cocoa, kterou je důsledné využívání polymorfismu. Všimněte si, že pro získání hodnoty požadovaného argumentu z objektu NSUserDefaults slouží přesně stejná zpráva (objectForKey:), jako pro získání hodnoty požadovaného klíče z objektu NSDictionary (na řádku s komentářem *5* nebo na řádku za komentářem *6*). To přináší dvě obrovské výhody: * doba potřebná k naučení se API Cocoa je mnohem kratší než doba potřebná pro naučení se jiných – i daleko chudších – API; * často se hodí i přímé využití polymorfismu pro větší flexibilitu kódu. Obsah řádku *2* je vše, co potřebujeme pro vyhledání všech souborů, jež budeme indexovat. Obsah příkazu for zajistí procházení všech souborů uvnitř složky ifolder a složek vnořených, a následující příkaz if z nich vybere jen soubory HTML. Makra NS_DURING (*3*), NS_HANDLER a NS_ENDHANDLER jsou v Cocoa standardní obsluhou výjimek. Funkčně tedy odpovídají kombinaci try/catch z C++, jsou však mnohem efektivnější. V našem jednoduchém prográmku slouží k tomu, aby – pokud při zpracování některého souboru dojde k výjimce – nebyl program ukončen, ale aby indexování pokračovalo dalším souborem. Na řádku *4* používáme třídu NSAttributedString, která dokáže korektně načíst HTML soubor a vrátit jeho textový obsah jako standardní string (toho využijeme hned na následujícím řádku). Pro vyhledání jednotlivých slov v jeho obsahu použijeme NSScanner přesně stejně jako v minulém příkladu. Tři řádky od komentáře *5* obsahují vlastní indexování. Jeho logika je jednoduchá: nejprve z objektu NSMutableDictionary (který je uložen v proměnné index) získáme objekt, jehož klíčem je dané slovo. Pokud takový objekt dosud neexistuje (protože jde o první výskyt daného slova), vytvoříme jej (jako prázdný NSMutableSet) a ihned jej – s daným slovem jako klíčem – vložíme do indexu (to je obsahem druhého řádku). Třetí řádek je triviální, prostě do objektu NSMutableSet vloží jméno souboru, ve kterém jsme dané slovo nalezli. Je snad zřejmé, že tímto způsobem nakonec v proměnné index vybudujeme skutečný index, v němž klíči budou jednotlivá slova a odpovídajícími hodnotami množiny obsahující jména všech dokumentů, ve kterých se dané slovo vyskytuje. Mimochodem, malý kvíz pro pozorné čtenáře: proč jsme pro seznamy souborů použili třídu NSMutableSet a ne třídu NSMutableArray? Mohli bychom sice index vypsat přímo (příkazem [index writeToFile:ofile...]), jenže pak by slova nebyla setříděná podle abecedy. Proto vytvoříme NSMutableString, do kterého na pouhých dvou řádcích (*6* a následující) vygenerujeme výstupní seznam slov a odpovídajících souborů v abecedním pořadí. Srovnejme příkaz pro třídění (sortedArrayUsingSelector:) s obdobným příkazem z minulého příkladu –tentokrát využíváme dynamického systému Objective C a prostě uvedeme zprávu, jejíž pomocí se mají při třídění slova porovnávat (je jí standardní zpráva compare:, kterou v lze API Cocoa srovnat libovolné dva objekty, nad nimiž je definována relace menší/větší). Jak je vidět na výpisu v HTML podobě článku na Chip CD, pro kompletní indexování cca tří megabajtů HTML textu stačily necelé dvě minuty. Výsledek (samozřejmě jen z malé části) pak vidíme na obrázku. NS(Mutable)String Řadu příkladů práce s objekty těchto tříd jsme již viděli. V minulém příkladu jsme pomocí třídy NSMutableString generovali výstupní data, NSString byl použit pro načítání vstupních souborů i pro údaje z argumentů příkazového řádku... V tomto odstavci si ukážeme pár dalších služeb podrobněji. // možností vytvořit řetězec je řada: id a=@"Toto je statický objekt třídy NSString"; char *xx="Můžeme samozřejmě využít i proměnné \"char *\""; id b=[NSMutableString stringWithCString:xx]; char *yy="I\0když\0obsahují\0nulové\0znaky\0!" id c=[NSString stringWithCString:yy length:30]; // řetězec lze načíst přímo ze souboru: id d=[NSString stringWithContentsOfFile:@"/tmp/something.text"]; // nebo vytvořit pomocí "printf"-formátu: id e=[NSString stringWithFormat:@"total:%d, %s, %@, %5.3f\n",1,"ahoj",a,3.14159]; // můžeme si také vyžádat automatický "překlad" prostřednictvím // překladové tabulky v aktivním adresáři lproj (podrobnosti viz NSBundle): id f=[NSString localizedStringWithFormat:@"%d files",ff]; // výše uvedený příklad vytvoří např. při aktivní češtině a // odpovídající položce v tabulce stringů řetězec // @"15 souborů". Řetězce často generují i jiné třídy (např. libovolný objekt OpenStepu vrátí NSString, obsahující jeho popis, na základě zprávy description). Jiným hezkým příkladem je NSArray – objekty této třídy umějí vytvořit řetězec daný kombinací všech obsažených prvků a libovolného oddělovače: NSArray *a=[NSArray arrayWithObjects:@"A",@"B",@"C",nil]; NSLog(@"%@",[a componentsJoinedByString:@" nebo "]); // vypíše "A nebo B nebo C" NSLog(@"DOS path: %@",[a componentsJoinedByString:@"\\"]); // vypíše "DOS path: A\B\C" Základní služby pro práci s řetězci samozřejmě zahrnují nejrůznější kombinace a rozklady. Ukažme si několik příkladů, využívajících řetězce a – f vytvořené v prvním příkladu: NSLog(@"%@",[f stringByAppendingString:f]); // vypíše "15 souborů15 souborů" NSLog(@"%@",[f stringByAppendingFormat:@" je prostě %@",f]); // vypíše "15 souborů je prostě 15 souborů" NSArray a=[e componentsSeparatedByString:@", "]; // vytvoří pole @"total:1",@"ahoj",@"Toto ... NSString",@"3.142" NSLog(@"%@",[c substringFromIndex:29]); // vypíše "!" [b deleteCharactersInRange:(NSRange){8,8}]; // b obsahuje "Můžeme proměnné "char *"" [b appendString:@" použít"]; // b obsahuje "Můžeme proměnné "char *" použít" [b insertString:@"snadno " atIndex:7]; // b obsahuje "Můžeme snadno proměnné "char *" použít" Pro označování částí řetězců a pro vyhledávání slouží typ NSRange – obyčejná struktura, obsahující dvě čísla, pozici a délku: NSLog(@"%@",[a substringFromRange:(NSRange){8,8}]); // vypíše "statický" NSRange r=[a rangeOfString:@"je"]; NSLog(@"\"je\" je na pozici %d, před ním je \"%@\"",r.location,[a substringToIndex:r.location]); // vypíše ""je" je na pozici 5, před ním je "Toto "" Prostřednictvím přepínačů si můžeme vyžádat i hledání odzadu, hledání bez ohledu na velikost písmen nebo hledání pouze od zadané pozice. Omezit lze také rozsah prohledávaného řetězce. Pro základní a nejčastěji potřebná porovnávání jsou samozřejmě k dispozici hotové metody: if ([a hasPrefix:@"Toto"]) // platí if ([c hasSuffix:@"!"]) // platí if ([d isEqual:e]) // neplatí Zajímavá je i možnost vyžádat si nejdelší společný prefix dvou řetězců. I zde máme možnost volit, zda se má nebo nemá brát v úvahu velikost písmen: NSLog(@"%@",[a commonPrefixWithString:e options:NSCaseInsensitiveSearch]); // vypíše "Tot" Prozatím jsme se vůbec nezabývali vnitřním kódováním řetězce. To je v objektovém prostředí samozřejmé – do vnitřního kódování nám přece nic není a zajímají nás pouze zprávy, které je objekt schopen zpracovat. Kódování však může být zajímavé ze dvou důvodů – předně z použitého kódování vyplývá rozsah znaků, které řetězec může obsahovat; druhým důvodem může být programová volba kódování pro konkrétní účel – například pro ukládání do souboru bude asi nejvýhodnější kódování, které zabere nejméně místa. Základním kódováním pro třídu NSString je Unicode v tom smyslu, že řetězce reprezentované objekty třídy NSString mohou obsahovat libovolné znaky Unicode, a že metody pro přímý přístup do řetězce (např. metoda characterAtIndex:) operují právě s šestnáctibitovými kódy znaků podle standardu Unicode. Pro další kódování máme k dispozici předdefinovaný typ NSStringEncoding, který reprezentuje kódování, a následující metody: // vypíšeme všechna kódování, která jsou k dispozici NSStringEncoding *en=[NSString availableStringEncodings]; while (en) NSLog(@"%@",[NSString localizedNameOfStringEncoding:en++]); // zjistíme, které kódování odpovídá běžným Céčkovým řetězcům en=[NSString defaultCStringEncoding]; // ověříme, lze-li do něj převést string d bez ztráty informace if ([d canBeConvertedToEncoding:en]) // a pokud ano, převedeme jej: newd=[d dataUsingEncoding:en]; else { // ne-li, vyhledáme nejúspornější bezztrátové kódování en=[d smallestEncoding]; // a použijeme jej: newd=[d dataUsingEncoding:en]; } // pro použití ve standardním C jsou k dispozici // pomocné převáděcí metody void std_func(int i,float f,char *c) { printf("%d, %e, %s",i,f,c); } NSString *s=@"3.141592654"; std_func([s intValue],[s floatValue],[s cString]); // vypíše "3, 3.141593e+00, 3.141592654" Tím samozřejmě možnosti NSStringů zdaleka nekončí; na úrovni tohoto článku by však nemělo smysl podrobně popisovat všechny metody. Proto se již seznámíme jen se samostatnou skupinou služeb, které zajišťují korektní práci s názvy souborů a adresářů – samozřejmě v konkrétním hostitelském operačním systému, takže programátor se nemusí starat o to, oddělují-li se jednotlivé položky lomítkem, obráceným lomítkem nebo třeba dvojtečkou: // příklady jsou z Unixu: NSString *p=@"/Users/oc/Apps/Test.app"; NSLog(@"%@",[p lastPathComponent]); // vypíše "Test.app" NSLog(@"%@",[p pathExtension]); // vypíše "app" NSLog(@"%@",[p stringByAppendingPathComponent:@"Czech.lproj"]); // vypíše "/Users/oc/Apps/Test.app/Czech.lproj" NSLog(@"%@",[p stringByDeletingLastPathComponent]); // vypíše "/Users/oc/Apps" NSLog(@"%@",[p stringByAbbreviatingWithTildeInPath]); // pro uživatele "oc" vypíše "~/Apps/Test.app" NSString *p=@"~/Apps/Test.app"; NSLog(@"%@",[p stringByExpandingTildeInPath]); // pro uživatele Steve vypíše "/Users/Steve/Apps/Test.app" NSString *p=@"/oc/RootTemp"; NSLog(@"%@",[p stringByResolvingSymlinksInPath]); // u mě vypíše "/private/tmp", protože // /oc/RootTemp je link ("zástupce") složky /private/tmp NSString *p=@"~/../oc/./RootTemp/./TmpFile"; NSLog(@"%@",[p stringByStandardizingPath]); // vypíše "/Users/oc/RootTemp/TmpFile" Shrnutí Viděli jsme příklady použití dvou nesmírně často využívaných tříd – NS(Mutable)Dictionary a NS(Mutable)String. Bez podrobnějšího výkladu jsme se seznámili s řadou dalších tříd API Cocoa, mj. s NSUserDefaults nebo NSFileManager, a ukázali jsme si jiný příklad použití třídy NSMutableSet. V příštím dílu se podíváme na další zajímavé třídy Foundation Kitu. Ondřej Čada Chyba! Neznámý argument přepínače./7