Zpět Obsah Další

Foundation Kit: konkrétní třídy 2


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

Hash tabulky, jež Foundation Kit nabízí prostřednictvím tříd NSDictionary a NSMutableDictionary (připomeňme paradigma proměnných a neměnných objektů) 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, ve kterém nám poslouží třída NSMutableDictionary pro vytvoření rejstříku. Podobně jako minule, zároveň si 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 všech složkách vnořených: pro každé slovo vytvoří seznam všech dokumentů, ve kterých je toto slovo využito. Program bude o něco luxusnější než minulý příklad; obsahuje např. dekódování argumentů příkazové řádky. 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 <min.word size>] -input <input folder> -output <output file>\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ás 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:

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áledují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 to které 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ěna 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í. Srovnejte 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:, jíž v API Cocoa lze srovnat libovolné dva objekty, nad nimiž je definována relace menší/větší).

Nakonec se opět podíváme na efektivitu: pro kompletní oindexování cca tří megabytů HTML textu stačily necelé dvě minuty:

13 /tmp/IndexHTML\> ./IndexHTML -input ~/Data/Homepage -output /tmp/test
Aug 25 00:46:00 IndexHTML[1865] Scanning "index.html" (2974 bytes)...
Aug 25 00:46:00 IndexHTML[1865] ...80 words (64 new items)
Aug 25 00:46:00 IndexHTML[1865] Scanning "oc.html" (4017 bytes)...
Aug 25 00:46:00 IndexHTML[1865] ...162 words (116 new items)
Aug 25 00:46:00 IndexHTML[1865] Scanning "Literature.html" (2140 bytes)...
Aug 25 00:46:00 IndexHTML[1865] ...47 words (28 new items)

...

Aug 25 00:47:47 IndexHTML[1865] Scanning "NeXTStep.html" (8489 bytes)...
Aug 25 00:47:47 IndexHTML[1865] ...599 words (58 new items)
Aug 25 00:47:47 IndexHTML[1865] Scanning "OCSoftware.html" (7240 bytes)...
Aug 25 00:47:47 IndexHTML[1865] ...452 words (50 new items)
Aug 25 00:47:47 IndexHTML[1865] Scanning "Psion.html" (3924 bytes)...
Aug 25 00:47:47 IndexHTML[1865] ...156 words (0 new items)
Aug 25 00:47:47 IndexHTML[1865] Scanned 163 files (179385 words, 3209577 bytes)
Aug 25 00:47:55 IndexHTML[1865] Index successfully written to "/tmp/test"
14 /tmp/IndexHTML\>

Výsledek (samozřejmě jen z malé části) pak vidíme na následujícím obrázku:

FK2Index

Jak ukazuje samostatné oindexování slov "koní" a "koních" nebo "končit", "končí" a "končím", pro korektní indexování češtiny bychom potřebovali slovníku ještě předřadit stemmer. Taková komplexní ukázka by však již přesáhla rámec tohoto textu.

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í vygenerovat ř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; rozsah prohledávaného řetězce také můžeme omezit. 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ř. 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 ano-li, 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, jež 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 buďtež 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 třídami NSUserDefaults nebo NSFileManager, a ukázali jsme si další 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.


Zpět Obsah Další

Copyright © Chip, O. Čada 2000-2003