如何将一个 C 结构体放入 NSArray 中?最佳方法是什么?

90

如何在NSArray中存储C语言结构体?有什么优缺点和内存处理方面的问题?

特别是,valueWithBytesvalueWithPointer之间有什么区别?这个问题由justin和catfish提出。

这里有一个链接,可以了解苹果关于valueWithBytes:objCType:的讨论,供未来读者参考...

对于一些横向思考和更多性能方面的考虑,Evgen提出了在C++中使用STL::vector的问题。

(这引发了一个有趣的问题:是否有一个快速的C库,类似于STL::vector但轻量得多,允许最小的"整洁的数组处理" ...?)

所以原始问题是...

例如:

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

那么:存储自己的结构体在NSArray中的正常、最佳和惯用方式是什么,如何处理该习惯中的内存?请注意,我特别寻找通常用于存储结构体的习惯用法。当然,可以通过创建一个新的小类来避免这个问题。但是我想知道实际将结构体放入数组中的通常惯用法,谢谢。

顺便说一句,这里有可能不是最佳的NSData方法...

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

顺便提一下,感谢Jarret Hardie的参考意见,以下是如何将CGPoints及其类似类型存储在NSArray中的方法:

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

(请参见如何以简便的方式将CGPoint对象添加到NSArray中?


你的代码用于将其转换为NSData应该没问题,并且没有内存泄漏。然而,也可以使用一个标准的C++结构体数组Megapoint p[3]。 - Swapnil Luktuke
问题在两天之内,您不能添加赏金。 - Matthew Frederick
1
valueWithCGPoint虽然不适用于OSX,但它是UIKit的一部分。 - lppier
@Ippier 的 valueWithPoint 在 OS X 上可用。 - Chris Schreiner
12个回答

164

NSValue不仅支持CoreGraphics结构体,您也可以用它来支持自己的结构体。我建议这样做,因为对于简单的数据结构,该类可能比NSData轻量级。

只需使用以下表达式:

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

获取值的方法如下:

Megapoint p;
[value getValue:&p];

4
它实际上是复制结构p,而不是指向它的指针。 @encode指令提供了有关结构大小的所有必要信息。当释放NSValue(或数组)时,它的结构副本将被销毁。如果您在此期间使用了getValue:,那么没问题。请参阅“Number and Value Programming Topics”中的“Using Values”部分:http://developer.apple.com/library/ios/documentation/Cocoa/Conceptual/NumbersandValues/Articles/Values.html#//apple_ref/doc/uid/20000174-BAJJHDEG - Justin Spahr-Summers
1
@Joe Blow 大部分正确,除了它在运行时无法更改。您正在指定C类型,该类型始终必须完全知道。如果它可以通过引用更多数据而变得“更大”,那么您可能会使用指针来实现它,并且@encode将使用该指针描述结构,但完全描述指向的数据,这确实可能会发生变化。 - Justin Spahr-Summers
1
NSValue在被释放时会自动释放结构体的内存吗?文档对此有些不清楚。 - devios1
1
所以为了完全清楚,NSValue 拥有它复制到自己的数据,我不必担心释放它(在 ARC 下)? - devios1
1
@devios 正确。NSValue 并不会真正进行任何“内存管理”,你可以认为它只是在内部拥有结构体值的一个副本。例如,如果该结构体包含嵌套的指针,NSValue 就不知道如何释放、复制或者对其进行任何操作,它会保持不变地复制地址。 - Justin Spahr-Summers
显示剩余8条评论

7
我建议您坚持使用NSValue方法,但如果您真的希望在NSArray(以及Cocoa中的其他集合对象)中存储普通的struct数据类型,您可以间接地使用Core Foundation和免费桥接实现。 CFArrayRef(以及其可变对应项CFMutableArrayRef)为开发人员创建数组对象提供了更多的灵活性。请参阅指定初始化程序的第四个参数:
CFArrayRef CFArrayCreate (
    CFAllocatorRef allocator,
    const void **values,
    CFIndex numValues,
    const CFArrayCallBacks *callBacks
);

这使您能够请求CFArrayRef对象使用Core Foundation的内存管理例程,不使用任何内存管理例程,甚至是您自己的内存管理例程。

必要示例:

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

上面的示例完全忽略了与内存管理相关的问题。结构体myStruct是在堆栈上创建的,因此当函数结束时被销毁--数组将包含指向一个不存在的对象的指针。您可以通过使用自己的内存管理例程来解决这个问题--这就是为什么提供该选项的原因--但那样您就必须做引用计数、分配内存、释放内存等繁重的工作。我不建议使用这种解决方案,但如果有人感兴趣,我会把它保留在这里。 :-)

这里演示了在堆上分配内存(而不是栈)时使用您的结构:

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

可变数组(至少在此情况下)是以空状态创建的,因此不需要指向存储在其中的值的指针。这与不可变数组不同,在其中内容在创建时被“冻结”,因此必须将值传递给初始化程序。 - Aidan Steele
@Joe Blow:你提到的内存管理是一个非常好的观点。你是正确的,会感到困惑:我上面发布的代码示例会导致神秘的崩溃,具体取决于何时函数的堆栈被覆盖。我开始详细说明我的解决方案如何使用,但意识到我正在重新实现Objective-C自己的引用计数。对于紧凑的代码表示歉意——这不是能力问题,而是懒惰。写别人看不懂的代码没有意义。 :) - Aidan Steele
如果你愿意“泄露”(缺乏更好的词)这个struct,那么你肯定可以只分配一次并且不在将来释放它。我在我的编辑答案中包含了一个例子。此外,myStruct不是打字错误,因为它是在堆栈上分配的结构,而不是指向在堆上分配的结构的指针。 - Aidan Steele

4

存储指针并按如下方式取消引用指针;

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

3

如果您要在多个ABI /架构之间共享此数据,则最好使用贫民版的Objc序列化器:

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

如果结构可能随时间(或针对特定平台)发生变化,那么这种方法不如其他选项快速,但在某些情况下(您未指定重要性或不重要性),它不太可能出现问题。

如果序列化表示未退出进程,则任意结构的大小/顺序/对齐方式不应更改,并且有更简单和更快速的选项。

无论哪种情况,您已经添加了一个引用计数的对象(与NSData、NSValue相比),因此...创建一个包含Megapoint的objc类在许多情况下是正确的答案。


@Joe Blow 有些东西可以执行序列化操作。可参考以下网址:http://en.wikipedia.org/wiki/Serialization,http://www.parashift.com/c++-faq-lite/serialization.html,以及苹果公司的《档案和序列化编程指南》。 - justin
假设XML文件正确地表示了某些内容,那么是的——这是一种常见的人类可读序列化形式。 - justin

3
如果你感到自己很专业或者需要创建很多个类:有时候动态构造一个 Objective-C 类是很有用的(参见:class_addIvar)。这样,你可以从任意类型创建任意 Objective-C 类。你可以逐个字段指定,也可以只通过结构体信息进行传递(但这实际上是复制 NSData)。对某些人来说可能会很有用,但对大部分读者而言可能只是有趣的知识点。

我该如何在这里应用它?

你可以调用 class_addIvar 并将 Megapoint 实例变量添加到新类中,或者你可以在运行时合成一个 Megapoint 类的 Objective-C 变体(例如,为 Megapoint 的每个字段添加一个实例变量)。
前者相当于编译后的 Objective-C 类:
@interface MONMegapoint { Megapoint megapoint; } @end

后者相当于编译的 ObjC 类:
@interface MONMegapoint { float w,x,y,z; } @end

在添加了实例变量之后,您可以添加/合成方法。

要在接收端读取存储的值,请使用合成方法、object_getInstanceVariablevalueForKey:(这通常会将这些标量实例变量转换为NSNumber或NSValue表示)。

顺便说一句:您收到的所有答案都很有用,具体取决于上下文/场景,有些更好/更差/无效。关于内存、速度、易于维护、易于传输或存档等特定需求,将确定对于给定情况哪种方式最佳...但没有一种“完美”的解决方案在每个方面都是理想的。没有“将c-struct放入NSArray的最佳方法”,只有“将c-struct放入NSArray的最佳方法,适用于特定的情况、案例或一组要求”--您必须指定。

此外,NSArray是一个通用的可重用数组接口,适用于指针大小(或更小)的类型,但有其他容器更适合c-structs,因为有许多原因(std::vector是c-structs的典型选择)。


如果你只是为了避免学习新的集合类型而经历所有这些麻烦 - 那么你应该学习新的集合类型 =) std::vector(例如)比NSArray更适合保存C/C++类型、结构体和类。通过使用NSValue、NSData或NSDictionary类型的NSArray,您会失去很多类型安全性,同时增加大量的分配和运行时开销。如果您想坚持使用C,那么他们通常会使用malloc和/或堆栈上的数组...但是std::vector会为您隐藏大部分复杂性。 - justin
实际上,如果您想进行数组操作/迭代,正如您所提到的-STL(C++标准库的一部分)非常适合。您可以选择更多类型(例如,如果插入/删除比读取访问时间更重要),并且有大量现有的方法来操作容器。此外-这不是C++中的裸内存-容器和模板函数是类型感知的,并在编译时进行检查-比从NSData / NSValue表示中提取任意字节字符串更安全。它们还具有边界检查和大多数自动管理内存。 - justin
如果你预计会有很多这样的低层级工作,那么现在就应该学习它,但是需要花费时间来学习。通过将所有内容封装在objc表示中,您将失去很多性能和类型安全性,同时使自己编写更多的样板代码来访问和解释容器及其值(如果“因此,要非常具体的话…”正是您想要做的)。 - justin
它只是你可以使用的另一个工具。集成Objective-C和C++、C++和Objective-C、C和C++或其他几种组合可能会出现一些问题。在任何情况下,添加语言功能和使用多种语言都会带来一些小成本。这是双向的。例如,当编译为Objective-C++时,构建时间会增加。此外,这些源代码不容易在其他项目中重用。当然,你可以重新实现语言功能...但这通常不是最好的解决方案。在Objective-C项目中集成C++很好,与在同一项目中使用Objective-C和C源代码一样“混乱”。 - justin
在我的代码库中,C++ 是最常用的语言,因此我通常只需要在 C++ 翻译中隐藏 Objective-C 接口。如果一个类需要在 C 或 OBC(没有 ++)中使用,则我会将所需内容包装在 C 或 Objective-C 接口中。这样可以减少大部分噪音。 - justin
显示剩余3条评论

0

不要试图将 C 结构体放入 NSArray 中,而是可以将它们作为 C 结构体数组放入 NSData 或 NSMutableData 中。要访问它们,您可以执行以下操作:

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

或将其设定

struct MyStruct    * theStruct = (struct MyStruct*)[myData mutableBytes];
theStruct[2].integerNumber = 10;

0
我建议你在C/C++类型中使用std::vector或std::list,因为首先它比NSArray更快,其次如果速度不够快,你总是可以为STL容器创建自己的分配器,使它们变得更快。所有现代移动游戏、物理和音频引擎都使用STL容器来存储内部数据。只是因为它们真的很快。
如果这对你来说不合适,有一些关于NSValue的好答案 - 我认为这是最可接受的。

STL是C++标准库的一部分,它是一个库。请参考以下链接:http://en.wikipedia.org/wiki/Standard_Template_Library 和 http://www.cplusplus.com/reference/stl/vector/。 - Evgen Bodunov
这是一个有趣的说法。你有关于STL容器与Cocoa容器类速度优势的文章链接吗? - Aidan Steele
这里有一篇关于NSCFArray和std::vector的有趣阅读:http://ridiculousfish.com/blog/archives/2005/12/23/array/。在您发布的示例中,最大的损失通常是为每个元素创建一个objc对象表示(例如,NSValue、NSData或包含Megapoint的Objc类型需要进行分配和插入到引用计数系统中)。通过使用Sedate Alien的方法将Megapoint存储在特殊的CFArray中,可以避免这种情况,该数组使用一个单独的后备存储器来按顺序分配Megapoints(尽管两个示例都没有说明这种方法)。 - justin
但是,使用NSCFArray与向量(或其他STL类型)相比,将产生额外的开销,如动态调度、未内联的额外函数调用、大量类型安全性以及许多优化器机会……该文章只关注插入、读取、遍历和删除。很有可能,你不会比一个16字节对齐的C数组Megapoint pt[8];更快——这是C++中的一种选择,也适用于专门的C++容器(例如std::array)。此外,请注意,该示例没有添加特殊的对齐方式(16字节被选中,因为它是Megapoint的大小)。 - justin
std::vector 会增加一点点开销,并且需要进行一次内存分配(如果你知道需要的大小)...但这比99.9%以上的情况更接近底层。通常,你只需要使用 vector,除非大小是固定的或有一个合理的最大值。 - justin
我同意。在测试您的数据使用量和大小后,您将看到哪个容器更适合。 - Evgen Bodunov

0

0

除了使用NSArray,还有另一个容器NSPointerArray适用于指针类型:

Megapoint point = ...;
NSPointerArray *arr = [NSPointerArray weakObjectsPointerArray];

[arr addPointer:&point];

for (NSUInteger i = 0; i < arr.count; i++) {
    Megapoint *ppoint = [arr pointerAtIndex:i];
    NSLog(@"%p", ppoint);
}

保留对象的内存是您自己的风险。


这将获得“十年老问题”徽章! - Fattie

0
对于您的结构,您可以添加一个属性objc_boxable并使用@()语法将您的结构放入NSValue实例中,而无需调用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)];

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接