MaCocoa 072

Capitolo 072 - Key-Value coding

Comincio una serie di pezzi in cui non c'è codice strutturato, ma solo auto-istruzione. Serviranno a riprendere mano alla programmazione in Objective C e Cocooa, dal momento che è passato parecchio tempo dall'ultima volta che ho scritto una riga di codice funzionante.

Sorgenti: Riprendo, traduco e riassumo: Key-Value Coding Programming Guide di Apple.

Prima stesura: 29 aprile 2006

Intro a KVC

Il Key Value Coding KVC è un meccanismo che permette di accedere alle proprietà di un oggetto indirettamente, senza utilizzare direttamente i metodi accessor o la lettura/scrittura della corrispondente variabile d'istanza.

A quanto pare, è un meccanismo indispensabile per rendere una applicazione scriptabile (ovvero, pilotabile tramite applescript) con facilità. Ma il meccanismo KVC è utile anche per semplificare la scrittura delle applicazioni. Mi riferirò a tutto ciò come al paradigma KVC, inteso come la teoria che inquadra tutta l'operatività relativa a questi meccanismi per la manipolazione delle proprietà di un oggetto

Tutto quello che si deve fare è rispettare un protocollo (che è informale, ovvero non deve essere dichiarato esplicitamente) chiamato NSKeyValueCoding. Il protocollo, in essenza, è costituito da due metodi, uno per leggere la proprietà in base ad un nome, ed il secondo per impostarne il valore:

- (id)valueForKey:(NSString *)key
- (void)setValue:(id)value forKey:(NSString *)key

L'esempio classico è la produzione di dati per una tabella. Il trucco spesso utilizzato per semplificare la stesura dei metodi della classe dataSource è di dare alle colonne della tabella un nome uguale a quello della variabile. Il nome alle colonne è dato (ad esempio) tramite Interface Builder in sede di costruzione dell'interfaccia, impostato nel campo identifier. Recuperando tale nome all'interno del metodo che provvede alla fornitura dei dati, si può risalire in maniera molto semplice al valore dell'elemento, proprio utilizzando il nome e il metodo valueForKey:

- (id)
outlineView:                (NSOutlineView *) outlineView
    objectValueForTableColumn:    (NSTableColumn *) tableColumn
    byItem:                (id)item
{
    NSString * colId ;
    // recupero l'identificatore della colonna
    colId = [ tableColumn identifier] ;
    // in tutti gli altri casi, uso il metodo valueForKey
    return ( [ item valueForKey: colId ] ) ;
}

Mi pare di avere utilizzato estesamente questo trucco quasi ogni volta che dovevo fornire dati ad una tabella, anche se in effetti non ne ero completamente consapevole. Approfitto per approfondire la questione, soprattutto per arrivare alla scriptabilità dell'applicazione.

Termini

Attraverso KVC si può accedere al valore di tre diversi tipi di proprietà di un oggetto: gli attributi, relazione singola, e relazione a molti. Poiché i nomi italiani per gli ultimi due mi fanno ribrezzo e sono una mia approssimativa traduzione, utilizzo gli originali nomi inglesi: to-one relationship e to-many relationship.

Un attributo è una proprietà che si esprime direttamente attraverso un valore, di varia natura, ma che è legato strettamente all'oggetto di partenza. Sono quindi attributi tutte le variabili d'istanza esprimibili come variabili standard del linguaggio (ovvero numeri interi, floating point o altro), ma anche oggetti "semplici", quali NSString, NSColor, NSNumber, eccetera.

La proprietà to-one relationship collega l'oggetto di partenza con un altro oggetto, suscettibile anch'esso di essere dotato di proprietà.

La proprietà to-many relationship collega l'oggetto di partenza con una collezione di altri oggetti, ciascuno dei quali suscettibili di proprietà; è il caso classico di una variabile d'istanza di tipo NSArray (o simili).

Per fare un esempio, che poi userò estesamente anche più avanti, dichiaro le seguenti due classi:

@interface primaClasse : NSObject {
    unsigned long    varLong ;
    NSString        * altroTesto ;
}

@end

@interface secondaClasse : NSObject {
    unsigned long    varLong2 ;
    NSString        * testo ;
    NSMutableArray    * vettore ;
    primaClasse        * objinterno ;
}

@end

Nella classe secondaClasse si riconoscono gli attributi varLong2 (facilmente perché numero scalare) e testo (un oggetto NSString); la to-one relationship objinterno e la to-many relationship vettore.

Per chiudere con le definizioni, abbiamo poi la chiave (key) ed il percorso chiave (key path). La chiave è il nome della proprietà, e quindi una stringa (un oggetto NSString). Il percorso chiave è invece una stringa che specifica un modo per recuperare una proprietà attraverso un percorso. Il percorso è una successione di chiavi separate da un punto: ad esempio chiave1.chiave2 significa individuare in prima battuta la proprietà chiave1. Questa dovrebbe individuare un oggetto, tra le proprietà del quale c'è chiave2. Il percorso chiave individua quindi il valore della proprietà chiave2 dell'oggetto individuato dalla proprietà chiave1.

Leggere e scrivere

Per sfruttare le potenzialità del paradigma KVC occorre realizzare i metodi del protocollo NSKeyValueCoding. Il protocollo è suscettibile di una realizzazione di default che richiede pochi accorgimenti nella stesura delle classi. Per il momento, esamino solamente l'interfaccia.

Per leggere una proprietà, ho già citato il metodo principe per accedere ad una proprietà attraverso il suo nome:

- (id)valueForKey:(NSString *)key

Se quindi ho un elemento item del quale mi interessa conoscere il valore della proprietà nomeProp mi basta scrivere:

val = [ item valueForKey: @"nomeProp" ];

Il metodo è così gentile da fornire sempre un oggetto, anche se il valore della proprietà è un tipo standard, come intero o floating point. Il metodo quindi costruisce un oggetto NSNumber o NSValue appropriato e lo inizializza al valore della proprietà.

Vale la pena notare che il paradigma KVC è utile quando non è noto in sede di compilazione il nome della proprietà desiderata; infatti, in tal caso, è molto più pratico ed efficiente accedere direttamente alla variabile, o meglio ancora, utilizzare il metodo accessor corrispondente. Quindi, in altre parole, in modo forse meno chiaro ma che per me rende meglio la potenza, il paradigma KVC è utile quando la proprietà desiderata non è nota al momento della compilazione, ovvero, tipicamente quando il nome della proprietà è stabilito dal contenuto di una variabile. Senza i meccanismi propri del KVC, si dovrebbe scrivere un metodo piuttosto pedissequo: esaminare il contenuto della variabile, e, a seconda del suo valore, attraverso una catena di if (visto che sarà raramente possibile utilizzare vettori o costrutti switch), distinguere tutti i vari casi possibili. Ad esempio, nel caso dei metodi per fornire dati ad una tabella, nel caso di utilizzo del trucco KVC, basta scrivere quelle poche righe viste in un esempio poco sopra; senza KVC, si deve scrivere qualcosa del tipo:

- (id)
outlineView:                (NSOutlineView *) outlineView
    objectValueForTableColumn:    (NSTableColumn *) tableColumn
    byItem:                (id)item
{
    NSString * colId ;
    // recupero l'identificatore della colonna
    colId = [ tableColumn identifier] ;
    if ( [ colId isEqual: @"colonna1" ] )
    {
        return ( [ item valorePerColonna1 ] );
    }
    if ( [ colId isEqual: @"colonna2" ] )
    {
        return ( [ item valorePerColonna2 ] );
    }
    // eccetera
}

Se la specifica della proprietà è nota attraverso un percorso chiave, il metodo da utilizzare è:

- (id)valueForKeyPath:(NSString *)keyPath

dove keyPath è una stringa con nomi separati da punti. Immagino che la versione standard di questo metodo (già realizzato da Apple per tutti noi) non faccia altro che esaminare i singoli costituenti del percorso, valutarli col metodo valueForKey, e passare all'elemento successivo.

Se infine si volessero recuperare un po' di proprietà in un colpo solo, esiste il metodo

-(NSDictionary *)dictionaryWithValuesForKeys:(NSArray *)keys

dove si passa un vettore di chiavi, e viene restituito un dizionario in cui ad ogni chiave è associato il valore della proprietà corrispondente.

Viene da chiedersi cosa accade quando la chiave o il percorso chiave specifica una proprietà inesistente (non si sa mai). Il protocollo afferma che in tal caso è chiamato il metodo

- (id)valueForUndefinedKey:(NSString *)key

il quale, normalmente, genera una eccezione (qualunque cosa sia: mi figuro un messaggio di errore un po' più complicato del solito). La cosa interessante è che, in una mia classe, posso sovrascrivere il metodo, e quindi restituire un valore di default, o qualcos'altro di piacevole.

Per assegnare valori alle proprietà, esistono i metodi corrispondenti a quelli appena visti:

- (void)setValue:(id)value forKey:(NSString *)key
- (void)setValue:(id)value forKeyPath:(NSString *)keyPath
- (void)setValuesForKeysWithDictionary:(NSDictionary *)keyedValues
- (void)setValue:(id)value forUndefinedKey:(NSString *)key

rispettivamente per impostare il valore di un proprietà attraverso il nome; attraverso un percorso chiave; per impostare contemporaneamente più proprietà; per evidenziare cosa succede in caso di non esistenza della proprietà indicata (ancora una volta, un'eccezione).

La realizzazione

La realizzazione standard del meccanismo KVC utilizza i metodi accessor. Se questi sono fatti appropriatamente, in pratica non occorre scrivere una linea di codice. Infatti, il metodo valueForKey utilizza la stringa passata come argomento come nome del metodo accessor. Ovvero, se la proprietà si chiama nomeProp, il metodo valueForKey chiama il metodo nomeProp oppure isNomeProp per la lettura, ed il metodo setNomeProp per la scrittura. Se quindi scrivo i metodi accessor della classe rispettando questo schema, il protocollo NSKeyValueCoding è automaticamente soddisfatto, senza scrivere una linea aggiuntiva di codice.

Parentesi: che differenza c'è tra il metodo accessor nomeProp e isNomeProp? Le linee guida Apple dicono di usare nomeProp nel caso in cui la proprietà sia espressa attraverso un nome o una azione (verbo), e di utilizzare isNomeProp se la proprietà è espressa da un aggettivo. Tutto molto bello, ma il problema adesso è: quando utilizzare un nome e quando un aggettivo? Chiusa parentesi.

Per la stesura dei metodi accessor, la cosa più semplice è lasciare fare a XCode. La mia versione (la 2.2, ma la cosa è presente da un certo numero di versioni in qua) fornisce due script di supporto che svolgono tutto il lavoro. Basta selezionare le variabili d'istanza di una classe. Poi, accedendo al menu Applescript si seleziona la voce Place Accessor Decls/Defs on Clipboard del menu Code a seconda se si vogliono le dichiarazioni o le definizioni dei metodi. Ci si piazza poi nel posto giusto, e si Incolla il testo nella Clipboard. Nulla di più facile e completo. In effetti, in questa versione di XCode sono prodotti metodi accessor sofisticati anche per i vettori (ovvero, le to-many relationship), cosa che non ricordo accadesse con le versioni precedenti.

Ripredendo quindi le due classi d'esempio citate in precedenza, costruisco un nuova coppia di file .m/.h all'interno dei quali, pur non rispettando una delle regole base della programmazione object-oriented, dichiaro e definisco entrambe le classi. A parte scrivere le variabili d'istanza, lascio a XCode il compito di scrivere il resto del codice. Mi limito ad aggiungere un metodo (per ciascuna classe) che mi aiuta nelle operazioni di debugging e comprensione dei meccanismi:

- (NSString *) locDescr
{
    return ([ NSString stringWithFormat: @"%@ -- varLong: %10ld, altroTesto: %@",
        [self description ], varLong, altroTesto ]);
}

Ricordo che il metodo description è fornito di serie con NSObject (e quindi a tutti i discendenti) come produttore di una stringa che descrive il contenuto dell'oggetto: il suo valore, se l'oggetto è semplice (un numero, una stringa), o qualcosa di più criptico negli altri casi.

Alla prova

Come primo esempio di applicazione, faccio una cosa piuttosto pedestre:

        primaClasse        * obj1 ;

        NSLog( @"\n\t\t\t\t--- test %2d---", testDaEseguire );
        // costruisco l'oggetto
        obj1 = [[ primaClasse alloc] init ];
        // gli assegno le variabili d'istanza per nome
        [ obj1 setValue: [ NSNumber numberWithLong: 12] forKey: @"varLong" ];
        [ obj1 setValue: @"testo1" forKey: @"altroTesto" ];
        // vediamo come e' venuto fuori
        NSLog( [ obj1 locDescr ] );
        
        // accedo alle variabili per nome
        NSLog( @"varLong: %@", [ obj1 valueForKey: @"varLong" ]);
        NSLog( @"testo : %@", [ obj1 valueForKey: @"altroTesto" ]);
        NSLog( @"dict : %@", [obj1 dictionaryWithValuesForKeys:
            [ NSArray arrayWithObjects: @"varLong", @"altroTesto"]]);        

Ho costruito un oggetto, ed ho assegnato le variabili d'istanza non tanto con metodi accessor, ma indirettamente con il paradigma KVC. L'unica cosa men che banale è l'ultima istruzione, in cui raccolgo le variabili indicate in un dizionario.

2006-04-29 17:02:15.252 test[943]
--- test 1---
2006-04-29 17:02:15.258 test[943] <primaclasse: 0x5033d0> -- varLong: 12, altroTesto: testo1
2006-04-29 17:02:15.259 test[943] varLong: 12
2006-04-29 17:02:15.260 test[943] testo : testo1
2006-04-29 17:02:15.263 test[943] dict : {altroTesto = testo1; varLong = 12; }

Nel secondo esempio, ho provato a vedere cosa succede utilizzando nomi inesistenti. La prima realizzazione muore in maniera spettacolare non appena se ne provi l'esecuzione:

        primaClasse        * obj1 ;

        NSLog( @"\n\t\t\t\t--- test %2d---", testDaEseguire );
        obj1 = [[ primaClasse alloc] init ];
        [ obj1 setValue: @"testo1" forKey: @"chiavenonesistente" ];
        NSLog( @"varLong: %@", [ obj1 valueForKey: @"chiavenonesistente" ]);

Ciò che accade è la generazione di una eccezione. Poiché nel mio codice di esempio non esiste alcun agente in grado di trattarla, se eseguo in XCode mi si apre il debugger, da terminale si producono scritte inquietanti:

host189-191:~/Documents/Macocoa/devTest/mc072/build/Debug djzero $ ./test
2006-04-29 16:26:52.290 test[695]
--- test 2---
2006-04-29 16:26:52.341 test[695] *** Uncaught exception: <nsunknownkeyexception> [<primaclasse 0x5033b0> setValue:forUndefinedKey:]: this class is not key value coding-compliant for the key chiavenonesistente.
Trace/BPT trap

In effetti, l'ambiente run-time riconosce il maldestro tentativo ed impedisce operazioni ulteriori.

La seconda versione di questo esempio prova allora a gestire l'eccezione. Riporto il codice così come mi è venuto, riservandomi di approfondire l'interessante argomento della gestione eccezioni:

        primaClasse        * obj1 ;

        NS_DURING        // sezione standard

        NSLog( @"\n\t\t\t\t--- test %2d---", testDaEseguire );
        // costruisco l'oggetto
        obj1 = [[ primaClasse alloc] init ];
        // provo ad impostare un valore
        [ obj1 setValue: @"testo1" forKey: @"chiavenonesistente" ];
        // qui tanto non ci arrivo, vista l'eccezione che si e' scatenata sopra
        NSLog( @"varLong: %@", [ obj1 valueForKey: @"chiavenonesistente" ]);
        
        NS_HANDLER        // handler delle eccezioni
        // localException sembra essere una variabile definita all'interno
        // del meccanismo delle eccezioni che contiene il motivo dei problemi
        NSLog( @"ECCEZIONE: %@", localException );

        NS_ENDHANDLER    // chiusa gestioni

In questo modo, l'esecuzione dell'applicazione non causa traumi; l'impossibilità di lavorare con la variabile indicata da un nome inesistente è riconosciuta facilmente e produce sostanzialmente lo stesso testo visto sopra, ma in maniera più controllata:

2006-04-29 17:02:39.714 test[944]
--- test 2---
2006-04-29 17:02:39.720 test[944] ECCEZIONE: [<primaclasse 0x503500> setValue:forUndefinedKey:]: this class is not key value coding-compliant for the key chiavenonesistente.

Provo quindi a realizzare (per la classe secondaClasse) il metodo del protocollo che gestisce proprio questi comportamenti strani. Scrivo il metodo seguente (niente di più che un metodo civetta per vedere che è invocato, una volta presente):

- ( id) valueForUndefinedKey: (NSString *) key
{
    return ( @"xxx" ) ;
}

Così facendo, il pezzo di codice seguente non produce disastri:

        secondaClasse        * obj1 ;

        NS_DURING        // sezione standard
        
        NSLog( @"\n\t\t\t\t--- test %2d---", testDaEseguire );
        // costruisco l'oggetto
        obj1 = [[ secondaClasse alloc] init ];
        // provo a leggere una variabile inesistente
        NSLog( @"varLong: %@", [ obj1 valueForKey: @"chiavenonesistente" ]);
        // ci riesco, ma il valore non ha senso...
        
        NS_HANDLER        // handler delle eccezioni

        NSLog( @"ECCEZIONE: %@", localException );

        NS_ENDHANDLER    // chiusa gestioni

ma solo una risposta di default non impegnativa:

2006-04-29 17:03:42.004 test[945]
--- test 3---
2006-04-29 17:03:42.010 test[945] varLong: xxx

Operatori su vettori ed insiemi

Una cosa interessante che si può fare con i meccanismi KVC è di applicare un dato operatore a tutti gli oggetti che fanno parte di un vettore. Il costrutto sotto esame appare complicato a prima vista:

keyPathToArray.@operator.keyPathToProperty

La stringa keyPathToArray è un percorso chiave che specifica un elemento di tipo NSArray (o assimilabile). Se l'oggetto cui si applica è già un vettore, si omette la prima parte. La stringa keyPathToProperty specifica un percorso chiave che individua una determinata proprietà (ma ci deve essere da qualche parte una specifica che attraversa un vettore). Infine, la stringa operator determina quale operazione deve essere effettuata sui valori della proprietà individuata. Tra le varie possibilità sono presenti quelle tipiche: il valore massimo, il valore minimo, la media, il numero di elementi.

Con qualche esempio si capisce meglio. Partiamo dall'uso semplice di valueForKeyPath, tornando alle classi di esempio viste in precedenza.

        primaClasse            * obj1 ;
        secondaClasse        * obj2 ;

        NSLog( @"\n\t\t\t\t--- test %2d---", testDaEseguire );
        // costruisco l'oggetto
        obj1 = [[ primaClasse alloc] init ];
        // gli assegno le variabili d'istanza per nome
        [ obj1 setValue: [ NSNumber numberWithLong: 111] forKey: @"varLong" ];
        [ obj1 setValue: @"testo1" forKey: @"altroTesto" ];

        // costruisco un altro oggetto
        obj2 = [[ secondaClasse alloc] init ];
        [ obj2 setValue: [ NSNumber numberWithLong: 222] forKey: @"varLong2" ];
        [ obj2 setValue: @"testo2" forKey: @"testo" ];
        // to-one relationship
        [ obj2 setValue: obj1 forKey: @"objinterno" ];
        // vediamo cosa e' saltato fuori
        NSLog( [ obj2 locDescr ] );
        // accesso alle variabili di obj1 attraverso obj2 e la specifica del path
        NSLog( @"varLong: %@", [ obj2 valueForKeyPath: @"objinterno.varLong" ]);
        [ obj2 setValue: @"testoxxx" forKeyPath: @"objinterno.altroTesto" ];
        NSLog( @"testo1: %@", [ obj2 valueForKeyPath: @"objinterno.altroTesto" ]);        

costruisco il primo oggetto, e lo inizializzo tramite KVC. Passo al secondo oggetto, e lo inizializzo sempre con KVC. Tra i valori che assegno, c'è una una to-one relationship, che coinvolge i due oggetti. A questo punto, posso leggere ed impostare i valori delle variabili interne sempre per nome, sfruttando il percorso completo.

2006-04-29 17:03:55.575 test[946]
--- test 4---
2006-04-29 17:03:55.581 test[946] <secondaclasse: 0x504ee0> -- varLong2: 222, testo: testo2 arrayDim: 0
2006-04-29 17:03:55.584 test[946] varLong: 111
2006-04-29 17:03:55.585 test[946] testo1: testoxxx

Fin qui, è andato tutto bene. Mi complico la vita con la to-many relationship. Per prima cosa un segmento di codice che costruisce il tutto:

        primaClasse            * obj1 ;
        secondaClasse        * obj2 ;
        NSMutableArray        * locArr ;
        unsigned short        ii ;
        id        val ;
        
        NSLog( @"\n\t\t\t\t\t\t\t\t--- test %2d---", testDaEseguire );
        // costruisco un oggetto e gli assegno proprieta'
        obj2 = [[ secondaClasse alloc] init ];
        [ obj2 setValue: [ NSNumber numberWithLong: 222] forKey: @"varLong2" ];
        [ obj2 setValue: @"testo2" forKey: @"testo" ];
        // costruisco un vettore di oggetti di tipo primaClasse
        locArr = [ NSMutableArray arrayWithCapacity: 5 ];
        for ( ii = 0 ; ii < 5 ; ii ++ )
        {
            // costruisco l'oggetto
            obj1 = [[ primaClasse alloc] init ];
            // gli assegno le variabili d'istanza per nome
            [ obj1 setValue: [ NSNumber numberWithLong: (100 + ii)] forKey: @"varLong" ];
            [ locArr addObject: obj1 ];
        }        
        // to-many relationship
        [ obj2 setValue: locArr forKey: @"vettore" ];
        NSLog( [ obj2 locDescr ] );

Adesso, ho un vettore di oggetti primaClasse, all'interno di ciascuno dei quali la variabile varLong ha un valore differente (i cinque valori 100, 101, 102, 103 e 104). Allora:

val = [ locArr valueForKeyPath:@"@sum.varLong" ];

Con questa istruzione si calcola la somma di tutti i valori di varLong che sono presenti negli oggetti di tipo primaClasse raccolti nella variabile locArray. Il risultato dovrebbe essere 510.

Invece con questa istruzione si calcola il numero di elementi del vettore vettore.:

val = [ obj2 valueForKeyPath:@"vettore.@count" ];

Oppure si può calcolare la media dei cinque valori presenti:

val = [ obj2 valueForKeyPath:@"vettore.@avg.varLong" ];

Per finire, l'istruzione

val = [ obj2 valueForKeyPath:@"vettore.@unionOfObjects.varLong" ];

produce un array di elementi; questo array si ottiene unendo tutti gli elementi varLong degli oggetti presenti nel vettore vettore dell'oggetto obj2. In effetti ciò che viene prototto sul terminale mi da ragione:

2006-04-29 17:04:12.791 test[947]
--- test 5---
2006-04-29 17:04:12.799 test[947] <secondaclasse: 0x503fc0> -- varLong2: 222, testo: testo2 arrayDim: 5
2006-04-29 17:04:12.804 test[947] somma: 510
2006-04-29 17:04:12.805 test[947] quanti: 5
2006-04-29 17:04:12.807 test[947] media: 102
2006-04-29 17:04:12.808 test[947] unione: (100, 101, 102, 103, 104)

Licenza Creative Commons
Eccetto dove diversamente specificato, i contenuti di questo sito sono rilasciati sotto Licenza Creative Commons.
Pagina a cura di Livio Sandel (macocoa2012@gmail.com).