110 votes

Obtenir une liste des propriétés de l'objet en Objective-C

Comment puis-je obtenir une liste (sous la forme d'une NSArray ou d'un NSDictionary) des propriétés d'un objet donné en Objective-C?

Imaginez le scénario suivant : j'ai défini une classe parent qui étend simplement NSObject, qui contient une NSString, un BOOL et un objet NSData en tant que propriétés. Ensuite, j'ai plusieurs classes qui étendent cette classe parent, ajoutant beaucoup de propriétés différentes chacune.

Existe-t-il un moyen d'implémenter une méthode d'instance sur la classe parent qui parcourt tout l'objet et renvoie, par exemple, un NSArray de chacune des propriétés de la classe (enfant) en tant que NSStrings qui ne sont pas dans la classe parent, afin que je puisse ensuite utiliser ces NSString pour le KVC?

118voto

boliva Points 2588

J'ai réussi à trouver la réponse moi-même. En utilisant la bibliothèque Obj-C Runtime, j'ai pu accéder aux propriétés de la manière souhaitée :

- (void)myMethod {
    unsigned int outCount, i;
    objc_property_t *properties = class_copyPropertyList([self class], &outCount);
    for(i = 0; i < outCount; i++) {
        objc_property_t property = properties[i];
        const char *propName = property_getName(property);
        if(propName) {
            const char *propType = getPropertyType(property);
            NSString *propertyName = [NSString stringWithCString:propName
                                                                encoding:[NSString defaultCStringEncoding]];
            NSString *propertyType = [NSString stringWithCString:propType
                                                                encoding:[NSString defaultCStringEncoding]];
            ...
        }
    }
    free(properties);
}

Cela m'a obligé à créer une fonction C 'getPropertyType', principalement inspirée d'un exemple de code Apple (je ne me souviens pas exactement de la source) :

static const char *getPropertyType(objc_property_t property) {
    const char *attributes = property_getAttributes(property);
    char buffer[1 + strlen(attributes)];
    strcpy(buffer, attributes);
    char *state = buffer, *attribute;
    while ((attribute = strsep(&state, ",")) != NULL) {
        if (attribute[0] == 'T') {
            if (strlen(attribute) <= 4) {
                break;
            }
            return (const char *)[[NSData dataWithBytes:(attribute + 3) length:strlen(attribute) - 4] bytes];
        }
    }
    return "@";
}

6 votes

+1 à l'exception que cela générera une erreur sur les types primitifs, tels que int. Veuillez consulter ma réponse ci-dessous pour une version légèrement améliorée de la même chose.

1 votes

En tant que question de correction, [NSString stringWithCString:] est obsolète au profit de [NSString stringWithCString:encoding:].

4 votes

Devrait importer l'en-tête du runtime objc #import Cela fonctionne sur ARC.

76voto

orange80 Points 5822

La réponse de @boliva est bonne, mais a besoin d'un petit supplément pour gérer les types primitifs, tels que int, long, float, double, etc.

J'ai construit sur la sienne pour ajouter cette fonctionnalité.

// PropertyUtil.h
#import 

@interface PropertyUtil : NSObject

+ (NSDictionary *)classPropsFor:(Class)klass;

@end

// PropertyUtil.m
#import "PropertyUtil.h"
#import "objc/runtime.h"

@implementation PropertyUtil

static const char * getPropertyType(objc_property_t property) {
    const char *attributes = property_getAttributes(property);
    printf("attributes=%s\n", attributes);
    char buffer[1 + strlen(attributes)];
    strcpy(buffer, attributes);
    char *state = buffer, *attribute;
    while ((attribute = strsep(&state, ",")) != NULL) {
        if (attribute[0] == 'T' && attribute[1] != '@') {
            // it's a C primitive type:
            /* 
                if you want a list of what will be returned for these primitives, search online for
                "objective-c" "Property Attribute Description Examples"
                apple docs list plenty of examples of what you get for int "i", long "l", unsigned "I", struct, etc.            
            */
            return (const char *)[[NSData dataWithBytes:(attribute + 1) length:strlen(attribute) - 1] bytes];
        }        
        else if (attribute[0] == 'T' && attribute[1] == '@' && strlen(attribute) == 2) {
            // it's an ObjC id type:
            return "id";
        }
        else if (attribute[0] == 'T' && attribute[1] == '@') {
            // it's another ObjC object type:
            return (const char *)[[NSData dataWithBytes:(attribute + 3) length:strlen(attribute) - 4] bytes];
        }
    }
    return "";
}

+ (NSDictionary *)classPropsFor:(Class)klass
{    
    if (klass == NULL) {
        return nil;
    }

    NSMutableDictionary *results = [[[NSMutableDictionary alloc] init] autorelease];

    unsigned int outCount, i;
    objc_property_t *properties = class_copyPropertyList(klass, &outCount);
    for (i = 0; i < outCount; i++) {
        objc_property_t property = properties[i];
        const char *propName = property_getName(property);
        if(propName) {
            const char *propType = getPropertyType(property);
            NSString *propertyName = [NSString stringWithUTF8String:propName];
            NSString *propertyType = [NSString stringWithUTF8String:propType];
            [results setObject:propertyType forKey:propertyName];
        }
    }
    free(properties);

    // returning a copy here to make sure the dictionary is immutable
    return [NSDictionary dictionaryWithDictionary:results];
}

@end

1 votes

Avez-vous l'intention d'avoir #import en haut du fichier .h ?

2 votes

[NSString stringWithUTF8String:propType] n'a pas pu analyser "propType const char *"NSNumber\x94\xfdk;" et renvoie une chaîne nulle... Je ne sais pas pourquoi c'est un si étrange NSNumber. Peut-être parce que ActiveRecord?

0 votes

Magnifique! Merci beaucoup.

28voto

Farthen Points 545

La réponse de @orange80 a un problème : en fait, elle ne termine pas toujours la chaîne avec des 0. Cela peut entraîner des résultats inattendus comme un crash lors de la tentative de conversion en UTF8 (j'ai en fait eu un bug de crash assez ennuyeux à cause de ça. C'était amusant de le déboguer ^^). Je l'ai corrigé en obtenant en fait une NSString de l'attribut puis en appelant cStringUsingEncoding:. Cela fonctionne comme un charme maintenant. (Fonctionne également avec ARC, du moins pour moi)

Voici donc ma version du code maintenant:

// PropertyUtil.h
#import 

@interface PropertyUtil : NSObject

+ (NSDictionary *)classPropsFor:(Class)klass;

@end

// PropertyUtil.m
#import "PropertyUtil.h"
#import 

@implementation PropertyUtil

static const char *getPropertyType(objc_property_t property) {
    const char *attributes = property_getAttributes(property);
    //printf("attributes=%s\n", attributes);
    char buffer[1 + strlen(attributes)];
    strcpy(buffer, attributes);
    char *state = buffer, *attribute;
    while ((attribute = strsep(&state, ",")) != NULL) {
        if (attribute[0] == 'T' && attribute[1] != '@') {
            // it's a C primitive type:
            /*
             if you want a list of what will be returned for these primitives, search online for
             "objective-c" "Property Attribute Description Examples"
             apple docs list plenty of examples of what you get for int "i", long "l", unsigned "I", struct, etc.
             */
            NSString *name = [[NSString alloc] initWithBytes:attribute + 1 length:strlen(attribute) - 1 encoding:NSASCIIStringEncoding];
            return (const char *)[name cStringUsingEncoding:NSASCIIStringEncoding];
        }
        else if (attribute[0] == 'T' && attribute[1] == '@' && strlen(attribute) == 2) {
            // it's an ObjC id type:
            return "id";
        }
        else if (attribute[0] == 'T' && attribute[1] == '@') {
            // it's another ObjC object type:
            NSString *name = [[NSString alloc] initWithBytes:attribute + 3 length:strlen(attribute) - 4 encoding:NSASCIIStringEncoding];
            return (const char *)[name cStringUsingEncoding:NSASCIIStringEncoding];
        }
    }
    return "";
}

+ (NSDictionary *)classPropsFor:(Class)klass
{
    if (klass == NULL) {
        return nil;
    }

    NSMutableDictionary *results = [[NSMutableDictionary alloc] init];

    unsigned int outCount, i;
    objc_property_t *properties = class_copyPropertyList(klass, &outCount);
    for (i = 0; i < outCount; i++) {
        objc_property_t property = properties[i];
        const char *propName = property_getName(property);
        if(propName) {
            const char *propType = getPropertyType(property);
            NSString *propertyName = [NSString stringWithUTF8String:propName];
            NSString *propertyType = [NSString stringWithUTF8String:propType];
            [results setObject:propertyType forKey:propertyName];
        }
    }
    free(properties);

    // returning a copy here to make sure the dictionary is immutable
    return [NSDictionary dictionaryWithDictionary:results];
}

@end

0 votes

@farthen pouvez-vous fournir un exemple qui démontre le problème avec le code que j'ai fourni? Je suis juste curieux de le voir.

0 votes

@orange80 Eh bien, autant que je m'en souvienne, les données ne sont jamais terminées par zéro. Si c'est le cas, cela n'arrive que par accident. Je me trompe peut-être cependant. En d'autres nouvelles : j'ai toujours ce code en cours d'exécution et il fonctionne parfaitement :p

0 votes

@orange80 J'ai rencontré ce problème en essayant d'invoquer votre version de IMAAdRequest de la bibliothèque publicitaire IMA de Google. La solution de farthen l'a résolu.

9voto

Chatchavan Points 81

Lorsque j'ai essayé avec iOS 3.2, la fonction getPropertyType ne fonctionne pas bien avec la description de la propriété. J'ai trouvé un exemple dans la documentation d'iOS : "Guide de programmation de l'exécution Objective-C : Propriétés déclarées".

Voici un code révisé pour la liste des propriétés dans iOS 3.2 :

#import 
#import 
...
unsigned int outCount, i;
objc_property_t *properties = class_copyPropertyList([UITouch class], &outCount);
for(i = 0; i < outCount; i++) {
    objc_property_t property = properties[i];
    fprintf(stdout, "%s %s\n", property_getName(property), property_getAttributes(property));
}
free(properties);

7voto

J'ai trouvé que la solution de boliva fonctionne bien dans le simulateur, mais sur l'appareil, la sous-chaîne de longueur fixe cause des problèmes. J'ai écrit une solution plus conviviale pour Objective-C à ce problème qui fonctionne sur l'appareil. Dans ma version, je convertis le C-String des attributs en NSString et effectue des opérations de chaîne sur celui-ci pour obtenir une sous-chaîne juste de la description du type.

/*
 * @returns Une chaîne décrivant le type de la propriété
*/

+ (NSString *)propertyTypeStringOfProperty:(objc_property_t) property {
    const char *attr = property_getAttributes(property);
    NSString *const attributes = [NSString stringWithCString:attr encoding:NSUTF8StringEncoding];

    NSRange const typeRangeStart = [attributes rangeOfString:@"T@\""];  // début de la chaîne de type
    if (typeRangeStart.location != NSNotFound) {
        NSString *const typeStringWithQuote = [attributes substringFromIndex:typeRangeStart.location + typeRangeStart.length];
        NSRange const typeRangeEnd = [typeStringWithQuote rangeOfString:@"\""]; // fin de la chaîne de type
        if (typeRangeEnd.location != NSNotFound) {
            NSString *const typeString = [typeStringWithQuote substringToIndex:typeRangeEnd.location];
            return typeString;
        }
    }
    return nil;
}

/**
* @returns (NSString) Dictionnaire du nom de propriété --> type
*/

+ (NSDictionary *)propertyTypeDictionaryOfClass:(Class)klass {
    NSMutableDictionary *propertyMap = [NSMutableDictionary dictionary];
    unsigned int outCount, i;
    objc_property_t *properties = class_copyPropertyList(klass, &outCount);
    for(i = 0; i < outCount; i++) {
        objc_property_t property = properties[i];
        const char *propName = property_getName(property);
        if(propName) {

            NSString *propertyName = [NSString stringWithCString:propName encoding:NSUTF8StringEncoding];
            NSString *propertyType = [self propertyTypeStringOfProperty:property];
            [propertyMap setValue:propertyType forKey:propertyName];
        }
    }
    free(properties);
    return propertyMap;
}

0 votes

Cela jette une exception EXC_BAD_ACCESS sur NSRange const typeRangeStart = [attributes rangeOfString:@"T@\""]; // début de la chaîne de type

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X