¿Cuál es la mejor manera de poner una c-struct en un NSArray?

89

¿Cuál es la forma habitual de almacenar estructuras en C en un NSArray? ¿Ventajas, desventajas, manejo de la memoria?

En particular, ¿cuál es la diferencia entre valueWithBytesy valueWithPointer - planteado por justin y bagre a continuación?

Aquí hay un enlace a la discusión de Apple valueWithBytes:objCType:para futuros lectores ...

Para un poco de pensamiento lateral y mirando más al rendimiento, Evgen ha planteado el problema del uso STL::vectoren C ++ .

(Eso plantea un problema interesante: ¿hay una biblioteca c rápida, no muy diferente STL::vectorpero mucho más ligera, que permita el mínimo "manejo ordenado de matrices" ...?)

Entonces la pregunta original ...

Por ejemplo:

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

Entonces: ¿cuál es la mejor manera normal, idiomática de almacenar la propia estructura así en un NSArray, y cómo manejas la memoria en ese idioma?

Tenga en cuenta que estoy buscando específicamente el idioma habitual para almacenar estructuras. Por supuesto, uno podría evitar el problema haciendo una nueva clase pequeña. Sin embargo, quiero saber cómo se usa el idioma habitual para poner estructuras en una matriz, gracias.

Por cierto, aquí está el enfoque NSData, que tal vez sea? no es el mejor ...

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];

Por cierto, como punto de referencia y gracias a Jarret Hardie, aquí se explica cómo almacenar CGPointsy similares en un NSArray:

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

(consulte ¿Cómo puedo agregar objetos CGPoint a un NSArray de manera fácil? )

Fattie
fuente
su código para convertirlo a NSData debería estar bien ... y sin pérdidas de memoria ... sin embargo, también se podría usar un arreglo estándar de estructuras C ++ Megapoint p [3];
Swapnil Luktuke
No puede agregar una recompensa hasta que la pregunta tenga dos días.
Matthew Frederick
1
valueWithCGPoint no está disponible para OSX. Es parte de UIKit
lppier
@Ippier valueWithPoint está disponible en OS X
Schpaencoder

Respuestas:

160

NSValue no solo es compatible con las estructuras de CoreGraphics, también puede usarlas para las suyas propias. Recomendaría hacerlo, ya que la clase probablemente sea más liviana que NSDatapara estructuras de datos simples.

Simplemente use una expresión como la siguiente:

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

Y para recuperar el valor:

Megapoint p;
[value getValue:&p];
Justin Spahr-Summers
fuente
4
@Joe Blow @Catfish_Man Realmente copia la estructura p, no un puntero a ella. La @encodedirectiva proporciona toda la información necesaria sobre el tamaño de la estructura. Cuando suelta NSValue(o cuando lo hace la matriz), su copia de la estructura se destruye. Si ha usado getValue:mientras tanto, está bien. Consulte la sección "Uso de valores" de "Temas de programación de valores y números": developer.apple.com/library/ios/documentation/Cocoa/Conceptual/…
Justin Spahr-Summers
1
@Joe Blow Mayormente correcto, excepto que no podría cambiar en tiempo de ejecución. Está especificando un tipo C, que siempre debe conocerse por completo. Si pudiera hacerse "más grande" haciendo referencia a más datos, entonces probablemente lo implementaría con un puntero, y @encodedescribiría la estructura con ese puntero, pero no describiría completamente los datos apuntados, que de hecho podrían cambiar.
Justin Spahr-Summers
1
¿Liberará NSValueautomáticamente la memoria de la estructura cuando se desasigne? La documentación es un poco confusa al respecto.
devios1
1
Entonces, para ser completamente claro, ¿ NSValueposee los datos que copia en sí mismo y no tengo que preocuparme por liberarlos (bajo ARC)?
devios1
1
@devios Correcto. NSValueen realidad, no realiza ninguna "gestión de la memoria" per se; puede pensar en ello como si tuviera una copia del valor de la estructura internamente. Si la estructura contuviera punteros anidados, por ejemplo, NSValueno sabría liberar o copiar o hacer algo con ellos, los dejaría intactos, copiando la dirección como está.
Justin Spahr-Summers
7

Le sugiero que se ciña a la NSValueruta, pero si realmente desea almacenar structtipos de datos sencillos en su NSArray (y otros objetos de colección en Cocoa), puede hacerlo, aunque de forma indirecta, utilizando Core Foundation y puentes gratuitos. .

CFArrayRef(y su contraparte mutable CFMutableArrayRef) brindan al desarrollador más flexibilidad al crear un objeto de matriz. Vea el cuarto argumento del inicializador designado:

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

Esto le permite solicitar que el CFArrayRefobjeto use las rutinas de administración de memoria de Core Foundation, ninguna en absoluto o incluso sus propias rutinas de administración de memoria.

Ejemplo obligatorio:

// 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];

El ejemplo anterior ignora por completo los problemas con respecto a la gestión de la memoria. La estructura myStructse crea en la pila y, por lo tanto, se destruye cuando finaliza la función; la matriz contendrá un puntero a un objeto que ya no está allí. Puede solucionar esto utilizando sus propias rutinas de administración de memoria, de ahí la razón por la que se le brinda la opción, pero luego debe hacer el trabajo duro de contar referencias, asignar memoria, desasignarla, etc.

No recomendaría esta solución, pero la mantendré aquí en caso de que sea de interés para alguien más. :-)


Aquí se demuestra el uso de su estructura asignada en el montón (en lugar de la pila):

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];
Sedate Alien
fuente
Las matrices mutables (al menos en este caso) se crean en un estado vacío y, por lo tanto, no necesitan un puntero a los valores que se almacenarán dentro. Esto es distinto de la matriz inmutable en la que el contenido se "congela" en la creación y, por lo tanto, los valores deben pasarse a la rutina de inicialización.
Sedate Alien
@Joe Blow: Ese es un punto excelente que hace con respecto a la gestión de la memoria. Tiene razón en estar confundido: el ejemplo de código que publiqué anteriormente causaría bloqueos misteriosos, dependiendo de cuándo se sobrescriba la pila de la función. Comencé a detallar cómo podría usarse mi solución, pero me di cuenta de que estaba volviendo a implementar el propio recuento de referencias de Objective-C. Mis disculpas por el código compacto, no es una cuestión de aptitud sino de pereza. No tiene sentido escribir código que otros no puedan leer. :)
Sedate Alien
Si estuviera feliz de "filtrar" (a falta de una palabra mejor) struct, ciertamente podría asignarlo una vez y no liberarlo en el futuro. Incluí un ejemplo de esto en mi respuesta editada. Además, no fue un error tipográfico myStruct, ya que era una estructura asignada en la pila, a diferencia de un puntero a una estructura asignada en el montón.
Sedate Alien
4

Un método similar para agregar c struct es almacenar el puntero y quitar la referencia del puntero como tal;

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
fuente
3

si te sientes nerd o si realmente tienes muchas clases para crear: ocasionalmente es útil construir dinámicamente una clase objc (ref:) class_addIvar. de esta manera, puede crear clases objc arbitrarias a partir de tipos arbitrarios. puede especificar campo por campo, o simplemente pasar la información de la estructura (pero eso es prácticamente replicar NSData). a veces útil, pero probablemente más un "hecho divertido" para la mayoría de los lectores.

¿Cómo aplicaría esto aquí?

puede llamar a class_addIvar y agregar una variable de instancia Megapoint a una nueva clase, o puede sintetizar una variante objc de la clase Megapoint en tiempo de ejecución (por ejemplo, una variable de instancia para cada campo de Megapoint).

el primero es equivalente a la clase objc compilada:

@interface MONMegapoint { Megapoint megapoint; } @end

el último es equivalente a la clase objc compilada:

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

después de haber agregado los ivars, puede agregar / sintetizar métodos.

para leer los valores almacenados en el extremo receptor, utilice sus métodos sintetizados object_getInstanceVariable, o valueForKey:(que a menudo convertirá estas variables de instancia escalares en representaciones NSNumber o NSValue).

Por cierto: todas las respuestas que ha recibido son útiles, algunas son mejores / peores / inválidas según el contexto / escenario. Las necesidades específicas en cuanto a memoria, velocidad, facilidad de mantenimiento, facilidad de transferencia o archivo, etc. determinarán cuál es la mejor para un caso dado ... pero no existe una solución "perfecta" que sea ideal en todos los aspectos. no existe una "mejor manera de poner una c-struct en un NSArray", solo una "mejor forma de poner una c-struct en un NSArray para un escenario, caso o conjunto de requisitos específicos ", que tendría para especificar.

Además, NSArray es una interfaz de matriz generalmente reutilizable para tipos de tamaño de puntero (o más pequeños), pero hay otros contenedores que son más adecuados para c-structs por muchas razones (std :: vector es una opción típica para c-structs).

justin
fuente
Los antecedentes de las personas también entran en juego ... cómo necesitas usar esa estructura a menudo eliminará algunas posibilidades. 4 floats es bastante infalible, pero los diseños de estructura varían según la arquitectura / compilador demasiado para usar una representación de memoria contigua (por ejemplo, NSData) y esperar que funcione. El serializador objc del pobre tiene probablemente el tiempo de ejecución más lento, pero es el más compatible si necesita guardar / abrir / transmitir el Megapoint en cualquier dispositivo OS X o iOS. La forma más común, en mi experiencia, es simplemente poner la estructura en una clase objc. si está pasando por todo esto solo para (continuación)
justin
(cont) Si está pasando por todas estas molestias solo para evitar aprender un nuevo tipo de colección, entonces debería aprender que el nuevo tipo de colección =) std::vector(por ejemplo) es más adecuado para contener tipos, estructuras y clases de C / C ++ que NSArray. Al usar un NSArray de tipos NSValue, NSData o NSDictionary, está perdiendo mucha seguridad de tipos mientras agrega una tonelada de asignaciones y sobrecarga de tiempo de ejecución. Si desea seguir con C, generalmente usarán malloc y / o matrices en la pila ... pero le std::vectorocultan la mayoría de las complicaciones.
justin
de hecho, si desea manipulación / iteración de matrices como mencionó, stl (parte de las bibliotecas estándar de c ++) es excelente para eso. tiene más tipos para elegir (por ejemplo, si insertar / eliminar es más importante que los tiempos de acceso de lectura) y toneladas de formas existentes de manipular los contenedores. Además, no es solo memoria en c ++, los contenedores y las funciones de plantilla son conscientes de los tipos y se verifican en la compilación, mucho más seguro que extraer una cadena de bytes arbitraria de las representaciones NSData / NSValue. también tienen verificación de límites y, en su mayoría, administración automática de memoria. (cont)
justin
(cont.) si espera tener mucho trabajo de bajo nivel como este, entonces debería aprenderlo ahora, pero le llevará tiempo aprenderlo. al envolver todo esto en representaciones de objc, está perdiendo mucho rendimiento y seguridad de tipos, mientras se obliga a escribir mucho más código repetitivo para acceder e interpretar los contenedores y sus valores (si 'Por lo tanto, para ser muy específico ...' es exactamente lo que quieres hacer).
justin
es solo otra herramienta a tu disposición. puede haber complicaciones al integrar objc con c ++, c ++ con objc, c con c ++ o cualquiera de varias otras combinaciones. En cualquier caso, agregar funciones de idioma y usar varios idiomas tiene un pequeño costo. va por todos lados. por ejemplo, los tiempos de construcción aumentan cuando se compila como objc ++. Además, estas fuentes no se reutilizan en otros proyectos con tanta facilidad. Claro, puede volver a implementar las funciones del idioma ... pero esa no suele ser la mejor solución. integrar c ++ en un proyecto objc está bien, es tan 'desordenado' como usar fuentes objc y c en el mismo proyecto. (cont
justin
3

sería mejor usar el serializador objc del pobre si está compartiendo estos datos entre múltiples abis / arquitecturas:

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;
/* ... */

... particularmente si la estructura puede cambiar con el tiempo (o por plataforma específica). no es tan rápido como otras opciones, pero es menos probable que se rompa en algunas condiciones (que no ha especificado como importantes o no).

si la representación serializada no sale del proceso, entonces el tamaño / orden / alineación de estructuras arbitrarias no debería cambiar, y hay opciones que son más simples y rápidas.

en cualquier caso, ya está agregando un objeto ref-counted (en comparación con NSData, NSValue) así que ... crear una clase objc que contenga Megapoint es la respuesta correcta en muchos casos.

justin
fuente
@Joe Blow algo que realiza la serialización. para referencia: en.wikipedia.org/wiki/Serialization , parashift.com/c++-faq-lite/serialization.html , así como la "Guía de programación de archivos y serializaciones" de Apple.
justin
asumiendo que el archivo xml representa correctamente algo, entonces sí, es una forma común de serialización legible por humanos.
justin
0

Le sugiero que use std :: vector o std :: list para los tipos C / C ++, porque al principio es más rápido que NSArray, y en segundo lugar, si no habrá suficiente velocidad para usted, siempre puede crear la suya propia asignadores para contenedores STL y hacerlos aún más rápidos. Todos los motores de juegos, física y audio móviles modernos utilizan contenedores STL para almacenar datos internos. Solo porque realmente rápido.

Si no es para ti, hay buenas respuestas de los chicos sobre NSValue, creo que es más aceptable.

Evgen Bodunov
fuente
STL es una biblioteca incluida parcialmente en la biblioteca estándar de C ++. en.wikipedia.org/wiki/Standard_Template_Library cplusplus.com/reference/stl/vector
Evgen Bodunov
Esa es una afirmación interesante. ¿Tiene un enlace a un artículo sobre la ventaja de velocidad de los contenedores STL frente a las clases de contenedores Cocoa?
Sedate Alien
aquí hay una lectura interesante sobre NSCFArray vs std :: vector: ridiculousfish.com/blog/archives/2005/12/23/array en el ejemplo de su publicación, la mayor pérdida es (típicamente) crear una representación de objeto objc por elemento (por ejemplo, , NSValue, NSData u Objc que contienen Megapoint requiere una asignación e inserción en el sistema de recuento de referencias). en realidad, podría evitarlo utilizando el enfoque de Sedate Alien para almacenar un Megapoint en un CFArray especial que usa un almacén de respaldo separado de Megapoints asignados de manera contigua (aunque ninguno de los ejemplos ilustra ese enfoque). (cont)
justin
pero luego usar NSCFArray vs el vector (u otro tipo stl) incurrirá en una sobrecarga adicional para el envío dinámico, llamadas de funciones adicionales que no están en línea, una tonelada de seguridad de tipos y muchas posibilidades de que el optimizador se active ... eso el artículo solo se centra en insertar, leer, caminar, eliminar. lo más probable es que no obtenga más rápido que una matriz c alineada de 16 bytes Megapoint pt[8];; esta es una opción en c ++ y contenedores especializados de c ++ (por ejemplo, std::array); también tenga en cuenta que el ejemplo no agrega la alineación especial (se eligieron 16 bytes porque es del tamaño de Megapoint). (cont)
justin
std::vectoragregará una pequeña cantidad de gastos generales a esto, y una asignación (si sabe el tamaño que necesitará) ... pero esto está más cerca del metal de lo que necesitan más del 99,9% de los casos. normalmente, solo usaría un vector a menos que el tamaño sea fijo o tenga un máximo razonable.
justin
0

En lugar de intentar poner c struct en un NSArray, puede ponerlos en un NSData o NSMutableData como una matriz de estructuras ac. Para acceder a ellos deberías hacer

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

o para configurar entonces

struct MyStruct    * theStruct = (struct MyStruct*)[myData mutableBytes];
theStruct[2].integerNumber = 10;
Día de nathan
fuente
0

Para su estructura, puede agregar un atributo objc_boxable y usar la @()sintaxis para poner su estructura en la instancia de NSValue sin llamar 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
fuente
-2

Un objeto Obj C es solo una estructura C con algunos elementos agregados. Así que simplemente cree una clase personalizada y tendrá el tipo de estructura C que requiere un NSArray. Cualquier estructura C que no tenga el cruft extra que un NSObject incluye dentro de su estructura C será indigerible para un NSArray.

El uso de NSData como contenedor solo podría almacenar una copia de las estructuras y no las estructuras originales, si eso hace una diferencia para usted.

hotpaw2
fuente
-3

Puede utilizar clases NSObject distintas de C-Structures para almacenar información. Y puede almacenar fácilmente ese NSObject en NSArray.

Satya
fuente