Quelle est la meilleure façon de mettre un c-struct dans un NSArray?

88

Quelle est la manière habituelle de stocker des c-structures dans un NSArray? Avantages, inconvénients, gestion de la mémoire?

Notamment, quelle est la différence entre valueWithByteset valueWithPointer - élevé par Justin et le poisson-chat ci-dessous.

Voici un lien vers la discussion d'Apple sur les valueWithBytes:objCType:futurs lecteurs ...

Pour une réflexion latérale et un examen plus approfondi des performances, Evgen a soulevé le problème de l'utilisation STL::vectoren C ++ .

(Cela soulève un problème intéressant: existe-t-il une bibliothèque c rapide, pas sans rappeler STL::vectormais beaucoup plus légère, qui permet une "gestion ordonnée des tableaux" minimale ...?)

Donc la question initiale ...

Par exemple:

typedef struct _Megapoint {
    float   w,x,y,z;
} Megapoint;

Alors: quelle est la manière normale, la meilleure et idiomatique de stocker sa propre structure comme ça dans un NSArray, et comment gérez-vous la mémoire dans cet idiome?

Veuillez noter que je recherche spécifiquement l'idiome habituel pour stocker des structures. Bien sûr, on pourrait éviter le problème en créant une nouvelle petite classe. Cependant, je veux savoir comment est l'idiome habituel pour mettre réellement des structures dans un tableau, merci.

BTW voici l'approche NSData qui est peut-être? pas mieux ...

Megapoint p;
NSArray *a = [NSArray arrayWithObjects:
    [NSData dataWithBytes:&p length:sizeof(Megapoint)],
    [NSData dataWithBytes:&p length:sizeof(Megapoint)],
    [NSData dataWithBytes:&p length:sizeof(Megapoint)],
        nil];

BTW comme point de référence et grâce à Jarret Hardie, voici comment stocker CGPointset similaires dans un NSArray:

NSArray *points = [NSArray arrayWithObjects:
        [NSValue valueWithCGPoint:CGPointMake(6.9, 6.9)],
        [NSValue valueWithCGPoint:CGPointMake(6.9, 6.9)],
        nil];

(voir Comment puis-je ajouter facilement des objets CGPoint à un NSArray? )

Fattie
la source
votre code pour le convertir en NSData devrait être bien .. et sans fuite de mémoire .... cependant, on pourrait aussi bien utiliser un tableau C ++ standard de structures Megapoint p [3];
Swapnil Luktuke
Vous ne pouvez pas ajouter de prime avant que la question n'ait deux jours.
Matthew Frederick
1
valueWithCGPoint n'est cependant pas disponible pour OSX. Ça fait partie de UIKit
lppier
@Ippier valueWithPoint est disponible sur OS X
Schpaencoder

Réponses:

158

NSValue ne prend pas uniquement en charge les structures CoreGraphics - vous pouvez également l'utiliser pour vous-même. Je recommanderais de le faire, car la classe est probablement plus légère que NSDatapour les structures de données simples.

Utilisez simplement une expression comme celle-ci:

[NSValue valueWithBytes:&p objCType:@encode(Megapoint)];

Et pour récupérer la valeur:

Megapoint p;
[value getValue:&p];
Justin Spahr-Summers
la source
4
@Joe Blow @Catfish_Man Il copie en fait la structure p, pas un pointeur vers elle. La @encodedirective fournit toutes les informations nécessaires sur la taille de la structure. Lorsque vous relâchez le NSValue(ou lorsque le tableau le fait), sa copie de la structure est détruite. Si vous avez utilisé getValue:entre-temps, vous allez bien. Consultez la section «Utilisation des valeurs» des «Rubriques de programmation des nombres et des valeurs»: developer.apple.com/library/ios/documentation/Cocoa/Conceptual/…
Justin Spahr-Summers
1
@Joe Blow Plutôt correct, sauf que cela ne pourrait pas changer à l'exécution. Vous spécifiez un type C, qui doit toujours être parfaitement connu. S'il pouvait devenir "plus grand" en référençant plus de données, alors vous implémenteriez probablement cela avec un pointeur, et le @encodedécrirait la structure avec ce pointeur, mais ne décrirait pas complètement les données pointées, ce qui pourrait en effet changer.
Justin Spahr-Summers
1
Libère-t-il NSValueautomatiquement la mémoire de la structure lorsqu'elle est désallouée? La documentation est un peu floue à ce sujet.
devios1
1
Donc, pour être tout à fait clair, le NSValuepossède les données qu'il copie sur lui-même et je n'ai pas à me soucier de les libérer (sous ARC)?
devios1
1
@devios Correct. NSValuene fait pas vraiment de «gestion de la mémoire» en soi - vous pouvez le considérer comme une simple copie de la valeur de la structure en interne. Si la structure contenait des pointeurs imbriqués, par exemple, NSValuene saurait libérer, copier ou faire quoi que ce soit avec ceux-ci, cela les laisserait intacts, en copiant l'adresse telle quelle.
Justin Spahr-Summers
7

Je vous suggère de vous en tenir à la NSValueroute, mais si vous souhaitez vraiment stocker des structtypes de données simples dans votre NSArray (et d'autres objets de collection dans Cocoa), vous pouvez le faire - bien qu'indirectement, en utilisant Core Foundation et un pont sans frais .

CFArrayRef(et son équivalent mutable, CFMutableArrayRef) offrent au développeur plus de flexibilité lors de la création d'un objet tableau. Voir le quatrième argument de l'initialiseur désigné:

CFArrayRef CFArrayCreate (
    CFAllocatorRef allocator,
    const void **values,
    CFIndex numValues,
    const CFArrayCallBacks *callBacks
);

Cela vous permet de demander que l' CFArrayRefobjet utilise les routines de gestion de la mémoire de Core Foundation, aucune ou même vos propres routines de gestion de la mémoire.

Exemple obligatoire:

// One would pass &kCFTypeArrayCallBacks (in lieu of NULL) if using CF types.
CFMutableArrayRef arrayRef = CFArrayCreateMutable(kCFAllocatorDefault, 0, NULL);
NSMutableArray *array = (NSMutableArray *)arrayRef;

struct {int member;} myStruct = {.member = 42};
// Casting to "id" to avoid compiler warning
[array addObject:(id)&myStruct];

// Hurray!
struct {int member;} *mySameStruct = [array objectAtIndex:0];

L'exemple ci-dessus ignore complètement les problèmes liés à la gestion de la mémoire. La structure myStructest créée sur la pile et est donc détruite lorsque la fonction se termine - le tableau contiendra un pointeur vers un objet qui n'est plus là. Vous pouvez contourner ce problème en utilisant vos propres routines de gestion de la mémoire - d'où la raison pour laquelle l'option vous est fournie - mais vous devez ensuite effectuer le dur travail de comptage de références, d'allocation de mémoire, de désallocation, etc.

Je ne recommanderais pas cette solution, mais je la garderai ici au cas où elle intéresserait quelqu'un d'autre. :-)


L'utilisation de votre structure allouée sur le tas (au lieu de la pile) est illustrée ici:

typedef struct {
    float w, x, y, z;
} Megapoint;

// One would pass &kCFTypeArrayCallBacks (in lieu of NULL) if using CF types.
CFMutableArrayRef arrayRef = CFArrayCreateMutable(kCFAllocatorDefault, 0, NULL);
NSMutableArray *array = (NSMutableArray *)arrayRef;

Megapoint *myPoint = malloc(sizeof(Megapoint);
myPoint->w = 42.0f;
// set ivars as desired..

// Casting to "id" to avoid compiler warning
[array addObject:(id)myPoint];

// Hurray!
Megapoint *mySamePoint = [array objectAtIndex:0];
Alien calme
la source
Les tableaux mutables (au moins dans ce cas) sont créés dans un état vide et n'ont donc pas besoin d'un pointeur vers les valeurs à stocker. Ceci est distinct du tableau immuable dans lequel le contenu est "figé" à la création et par conséquent les valeurs doivent être passées à la routine d'initialisation.
Sedate Alien
@Joe Blow: C'est un excellent point que vous faites sur la gestion de la mémoire. Vous avez raison d'être confus: l'exemple de code que j'ai posté ci-dessus provoquerait des plantages mystérieux, selon le moment où la pile de la fonction est écrasée. J'ai commencé à élaborer sur la façon dont ma solution pourrait être utilisée, mais je me suis rendu compte que je réimplémentais le comptage de références d'Objective-C. Mes excuses pour le code compact - ce n'est pas une question d'aptitude mais de paresse. Inutile d'écrire du code que les autres ne peuvent pas lire. :)
Sedate Alien
Si vous étiez heureux de "fuir" (faute d'un meilleur mot) le struct, vous pourriez certainement l'attribuer une fois et ne pas le libérer à l'avenir. J'ai inclus un exemple de cela dans ma réponse modifiée. De plus, ce n'était pas une faute de frappe myStruct, car il s'agissait d'une structure allouée sur la pile, distincte d'un pointeur vers une structure allouée sur le tas.
Sedate Alien
4

Une méthode similaire pour ajouter une structure c est de stocker le pointeur et de dé-référencer le pointeur comme tel;

typedef struct BSTNode
{
    int data;
    struct BSTNode *leftNode;
    struct BSTNode *rightNode;
}BSTNode;

BSTNode *rootNode;

//declaring a NSMutableArray
@property(nonatomic)NSMutableArray *queues;

//storing the pointer in the array
[self.queues addObject:[NSValue value:&rootNode withObjCType:@encode(BSTNode*)]];

//getting the value
BSTNode *frontNode =[[self.queues objectAtIndex:0] pointerValue];
Keynes
la source
3

si vous vous sentez ringard, ou avez vraiment beaucoup de classes à créer: il est parfois utile de construire dynamiquement une classe objc (ref:) class_addIvar. de cette façon, vous pouvez créer des classes objc arbitraires à partir de types arbitraires. vous pouvez spécifier champ par champ, ou simplement transmettre les informations de la structure (mais cela réplique pratiquement NSData). parfois utile, mais probablement plus un «fait amusant» pour la plupart des lecteurs.

Comment appliquerais-je cela ici?

vous pouvez appeler class_addIvar et ajouter une variable d'instance Megapoint à une nouvelle classe, ou vous pouvez synthétiser une variante objc de la classe Megapoint au moment de l'exécution (par exemple, une variable d'instance pour chaque champ de Megapoint).

le premier est équivalent à la classe objc compilée:

@interface MONMegapoint { Megapoint megapoint; } @end

ce dernier est équivalent à la classe objc compilée:

@interface MONMegapoint { float w,x,y,z; } @end

après avoir ajouté les ivars, vous pouvez ajouter / synthétiser des méthodes.

pour lire les valeurs stockées à l'extrémité de réception, utilisez vos méthodes synthétisées object_getInstanceVariable, ou valueForKey:(qui convertiront souvent ces variables d'instance scalaires en représentations NSNumber ou NSValue).

btw: toutes les réponses que vous avez reçues sont utiles, certaines sont meilleures / pires / invalides selon le contexte / scénario. des besoins spécifiques en matière de mémoire, de vitesse, de facilité de maintenance, de facilité de transfert ou d'archivage, etc. détermineront ce qui est le mieux pour un cas donné ... mais il n'y a pas de solution «parfaite» qui soit idéale à tous égards. il n'y a pas de `` meilleur moyen de mettre un c-struct dans un NSArray '', juste un `` meilleur moyen de mettre un c-struct dans un NSArray pour un scénario, un cas ou un ensemble d'exigences spécifiques '' - ce que vous auriez spécifier.

De plus, NSArray est une interface de tableau généralement réutilisable pour les types de taille pointeur (ou plus petits), mais il existe d'autres conteneurs qui conviennent mieux aux c-structs pour de nombreuses raisons (std :: vector étant un choix typique pour les c-structs).

Justin
la source
Les antécédents des gens entrent également en jeu ... la façon dont vous devez utiliser cette structure éliminera souvent certaines possibilités. 4 floats est assez infaillible, mais les dispositions de structure varient trop selon l'architecture / le compilateur pour utiliser une représentation de mémoire contiguë (par exemple, NSData) et s'attendre à ce qu'elle fonctionne. le sérialiseur d'objc du pauvre homme a probablement le temps d'exécution le plus lent, mais c'est le plus compatible si vous devez enregistrer / ouvrir / transmettre le mégapoint sur n'importe quel appareil OS X ou iOS. La manière la plus courante, d'après mon expérience, est de simplement mettre la structure dans une classe objc. si vous passez par tout cela juste pour (suite)
justin
(suite) Si vous passez par tous ces tracas uniquement pour éviter d'apprendre un nouveau type de collection - alors vous devriez apprendre que le nouveau type de collection =) std::vector(par exemple) est plus adapté pour contenir des types, des structures et des classes C / C ++ que NSArray. En utilisant un NSArray de types NSValue, NSData ou NSDictionary, vous perdez beaucoup de sécurité de type tout en ajoutant une tonne d'allocations et de surcharge d'exécution. Si vous voulez vous en tenir à C, ils utiliseraient généralement malloc et / ou des tableaux sur la pile ... mais vous std::vectorcache la plupart des complications.
justin
en fait, si vous voulez une manipulation / itération de tableau comme vous l'avez mentionné - stl (qui fait partie des bibliothèques standard c ++) est parfait pour cela. vous avez plus de types à choisir (par exemple, si insérer / supprimer est plus important que les temps d'accès en lecture), et des tonnes de façons existantes de manipuler les conteneurs. aussi - ce n'est pas de la mémoire nue en C ++ - les conteneurs et les fonctions de modèle sont sensibles au type et vérifiés à la compilation - beaucoup plus sûr que d'extraire une chaîne d'octets arbitraire des représentations NSData / NSValue. ils ont également une vérification des limites et la plupart du temps une gestion automatique de la mémoire. (suite)
justin
(suite) Si vous pensez avoir beaucoup de travail de bas niveau comme celui-ci, alors vous devriez simplement l'apprendre maintenant - mais cela prendra du temps à apprendre. en enveloppant tout cela dans des représentations objc, vous perdez beaucoup de performances et de sécurité de type, tout en vous obligeant à écrire beaucoup plus de code standard pour accéder et interpréter les conteneurs et leurs valeurs (Si 'Ainsi, pour être très précis ...' c'est exactement ce que vous voulez faire).
justin
c'est juste un autre outil à votre disposition. il peut y avoir des complications en intégrant objc avec c ++, c ++ avec objc, c avec c ++ ou n'importe laquelle de plusieurs autres combinaisons. l'ajout de fonctionnalités linguistiques et l'utilisation de plusieurs langues sont dans tous les cas à un faible coût. ça va dans tous les sens. par exemple, les temps de compilation augmentent lors de la compilation en tant que objc ++. de plus, ces sources ne sont pas réutilisées aussi facilement dans d’autres projets. bien sûr, vous pouvez réimplémenter les fonctionnalités linguistiques ... mais ce n'est pas souvent la meilleure solution. l'intégration de c ++ dans un projet objc est bien, c'est à peu près aussi `` désordonné '' que d'utiliser des sources objc et c dans le même projet. (suite
justin
3

il serait préférable d'utiliser le sérialiseur objc du pauvre si vous partagez ces données entre plusieurs abis / architectures:

Megapoint mpt = /* ... */;
NSMutableDictionary * d = [NSMutableDictionary new];
assert(d);

/* optional, for your runtime/deserialization sanity-checks */
[d setValue:@"Megapoint" forKey:@"Type-Identifier"];

[d setValue:[NSNumber numberWithFloat:mpt.w] forKey:@"w"];
[d setValue:[NSNumber numberWithFloat:mpt.x] forKey:@"x"];
[d setValue:[NSNumber numberWithFloat:mpt.y] forKey:@"y"];
[d setValue:[NSNumber numberWithFloat:mpt.z] forKey:@"z"];

NSArray *a = [NSArray arrayWithObject:d];
[d release], d = 0;
/* ... */

... en particulier si la structure peut évoluer dans le temps (ou par plateforme ciblée). ce n'est pas aussi rapide que d'autres options, mais il est moins susceptible de se briser dans certaines conditions (que vous n'avez pas spécifiées comme importantes ou non).

si la représentation sérialisée ne quitte pas le processus, alors la taille / l'ordre / l'alignement des structures arbitraires ne devraient pas changer, et il existe des options qui sont plus simples et plus rapides.

dans les deux cas, vous ajoutez déjà un objet ref-counted (par rapport à NSData, NSValue) donc ... créer une classe objc qui contient Megapoint est la bonne réponse dans de nombreux cas.

Justin
la source
@Joe Blow quelque chose qui effectue la sérialisation. pour référence: en.wikipedia.org/wiki/Serialization , parashift.com/c++-faq-lite/serialization.html , ainsi que le «Guide de programmation des archives et des sérialisations» d'Apple.
justin
en supposant que le fichier xml représente correctement quelque chose, alors oui - c'est une forme courante de sérialisation lisible par l'homme.
justin
0

Je vous suggère d'utiliser std :: vector ou std :: list pour les types C / C ++, car au début c'est juste plus rapide que NSArray, et dans un second temps s'il n'y aura pas assez de vitesse pour vous - vous pouvez toujours créer le vôtre allocateurs pour les conteneurs STL et les rendre encore plus rapides. Tous les moteurs mobiles modernes de jeu, de physique et audio utilisent des conteneurs STL pour stocker les données internes. Juste parce qu'ils sont vraiment rapides.

Si ce n'est pas pour vous - il y a de bonnes réponses de gars à propos de NSValue - je pense que c'est plus acceptable.

Evgen Bodunov
la source
STL est une bibliothèque partiellement incluse dans la bibliothèque standard C ++. en.wikipedia.org/wiki/Standard_Template_Library cplusplus.com/reference/stl/vector
Evgen Bodunov
C'est une affirmation intéressante. Avez-vous un lien vers un article sur l'avantage de vitesse des conteneurs STL par rapport aux classes de conteneurs Cocoa?
Sedate Alien
voici une lecture intéressante sur NSCFArray vs std :: vector: ridiculousfish.com/blog/archives/2005/12/23/array dans l'exemple de votre message, la plus grande perte est (généralement) la création d'une représentation d'objc par élément (par exemple , NSValue, NSData ou Objc contenant Megapoint nécessite une allocation et une insertion dans le système ref-counted). vous pouvez en fait éviter cela en utilisant l'approche de Sedate Alien pour stocker un mégapoint dans un CFArray spécial qui utilise un magasin de sauvegarde séparé de mégapoints alloués de manière contiguë (bien qu'aucun exemple n'illustre cette approche). (suite)
justin
mais l'utilisation du NSCFArray par rapport au vecteur (ou d'un autre type stl) entraînera une surcharge supplémentaire pour la répartition dynamique, des appels de fonction supplémentaires qui ne sont pas en ligne, une tonne de sécurité de type et de nombreuses chances pour l'optimiseur de démarrer ... que l'article se concentre simplement sur l'insertion, la lecture, la marche, la suppression. il y a de fortes chances que vous n'obtiendrez pas plus rapidement qu'un tableau c aligné sur 16 octets Megapoint pt[8];- c'est une option en c ++ et des conteneurs C ++ spécialisés (par exemple std::array) - notez également que l'exemple n'ajoute pas l'alignement spécial (16 octets ont été choisis car c'est la taille de Megapoint). (suite)
justin
std::vectorajoutera une petite quantité de frais généraux à cela, et une allocation (si vous connaissez la taille dont vous aurez besoin) ... mais c'est plus proche du métal que plus de 99,9% des cas ont besoin. généralement, vous utiliseriez simplement un vecteur à moins que la taille ne soit fixe ou n'ait un maximum raisonnable.
justin
0

Au lieu d'essayer de mettre une structure c dans un NSArray, vous pouvez les placer dans un NSData ou NSMutableData en tant que tableau ac de structures. Pour y accéder, vous feriez

const struct MyStruct    * theStruct = (const struct MyStruct*)[myData bytes];
int                      value = theStruct[2].integerNumber;

ou pour régler alors

struct MyStruct    * theStruct = (struct MyStruct*)[myData mutableBytes];
theStruct[2].integerNumber = 10;
Jour de Nathan
la source
0

Pour votre structure, vous pouvez ajouter un attribut objc_boxable et utiliser la @()syntaxe pour mettre votre structure dans l'instance NSValue sans appeler valueWithBytes:objCType::

typedef struct __attribute__((objc_boxable)) _Megapoint {
    float   w,x,y,z;
} Megapoint;

NSMutableArray<NSValue*>* points = [[NSMutableArray alloc] initWithCapacity:10];
for (int i = 0; i < 10; i+= 1) {
    Megapoint mp1 = {i + 1.0, i + 2.0, i + 3.0, i + 4.0};
    [points addObject:@(mp1)];//@(mp1) creates NSValue*
}

Megapoint unarchivedPoint;
[[points lastObject] getValue:&unarchivedPoint];
//or
// [[points lastObject] getValue:&unarchivedPoint size:sizeof(Megapoint)];
Andrew Romanov
la source
-2

Un objet Obj C est juste une structure C avec quelques éléments ajoutés. Alors créez simplement une classe personnalisée et vous aurez le type de structure C requis par un NSArray. Toute structure C qui n'a pas la cruauté supplémentaire qu'un NSObject inclut dans sa structure C sera indigeste à un NSArray.

L'utilisation de NSData comme wrapper peut ne stocker qu'une copie des structures et non les structures d'origine, si cela fait une différence pour vous.

hotpaw2
la source
-3

Vous pouvez utiliser des classes NSObject autres que les C-Structures pour stocker des informations. Et vous pouvez facilement stocker ce NSObject dans NSArray.

Satya
la source