【问题标题】:What's the best way to put a c-struct in an NSArray?将 c-struct 放入 NSArray 的最佳方法是什么?
【发布时间】:2011-05-29 20:31:02
【问题描述】:

NSArray 中存储 c 结构的常用方法是什么?优点、缺点、内存处理?

值得注意的是,valueWithBytesvalueWithPointer 之间有什么区别——由 justin 和 catfish 下面提出。

Here's a link Apple 为未来的读者讨论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,以下是如何在 NSArray 中存储 CGPoints 和类似内容:

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

(见How can I add CGPoint objects to an NSArray the easy way?

【问题讨论】:

  • 您将其转换为 NSData 的代码应该没问题......并且没有内存泄漏......但是,不妨使用标准的 C++ 结构体 Megapoint p[3] 数组;跨度>
  • 在问题出现两天之前,您无法添加赏金。
  • valueWithCGPoint 不适用于 OSX。它是 UIKit 的一部分
  • @Ippier valueWithPoint 在 OS X 上可用

标签: iphone objective-c cocoa struct nsarray


【解决方案1】:

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

只需使用如下表达式:

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

然后将值取回:

Megapoint p;
[value getValue:&p];

【讨论】:

  • @Joe Blow @Catfish_Man 它实际上复制了结构p,而不是指向它的指针。 @encode 指令提供了有关结构大小的所有必要信息。当您释放NSValue(或当数组释放时),它的结构副本被破坏。如果您在此期间使用过getValue:,那就没问题了。请参阅“数字和值编程主题”的“使用值”部分:developer.apple.com/library/ios/documentation/Cocoa/Conceptual/…
  • @Joe Blow 基本正确,只是它不会能够在运行时更改。您正在指定一个 C 类型,它必须始终是完全已知的。如果它可以通过引用更多数据而变得“更大”,那么您可能会使用指针来实现它,而@encode 将使用该指针描述结构,但 not 完全描述了指向数据,这确实可以改变。
  • NSValue 会在结构体被释放时自动释放它的内存吗?文档对此有点不清楚。
  • 所以为了完全清楚,NSValue 拥有它复制到自己的数据,我不必担心释放它(在 ARC 下)?
  • @devios 正确。 NSValue 本身并没有真正做任何“内存管理”——你可以认为它只是在内部拥有结构值的副本。例如,如果结构包含嵌套指针,NSValue 将不知道释放或复制这些指针或对它们做任何事情——它会保持它们不变,照原样复制地址。
【解决方案2】:

我建议您坚持使用NSValue 路线,但如果您确实希望在您的 NSArray(以及 Cocoa 中的其他集合对象)中存储普通的 'ol struct 数据类型,您可以这样做——尽管是间接的,使用 Core Foundation 和 toll-free bridging

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

【讨论】:

  • 可变数组(至少在这种情况下)是在空状态下创建的,因此不需要指向要存储的值的指针。这与不可变数组不同,其中内容在创建时“冻结”,因此必须将值传递给初始化例程。
  • @Joe Blow:您在内存管理方面提出的观点非常好。你是对的:我上面发布的代码示例会导致神秘的崩溃,这取决于函数的堆栈何时被覆盖。我开始详细说明如何使用我的解决方案,但意识到我正在重新实现 Objective-C 自己的引用计数。我为紧凑的代码道歉——这不是能力问题,而是懒惰问题。编写其他人无法阅读的代码毫无意义。 :)
  • 如果您乐于“泄露”(因为需要更好的词)struct,您当然可以分配它一次,以后不再释放它。我在我编辑的答案中包含了一个例子。此外,这不是 myStruct 的拼写错误,因为它是在堆栈上分配的结构,与指向在堆上分配的结构的指针不同。
【解决方案3】:

一个类似的添加c结构的方法是存储指针,然后取消引用指针;

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

【讨论】:

    【解决方案4】:

    如果你觉得书呆子,或者真的有很多类要创建:动态构造一个 objc 类有时很有用(参考:class_addIvar)。这样,您可以从任意类型创建任意 objc 类。您可以逐个字段指定,或者只传递结构的信息(但这实际上是在复制 NSData)。有时有用,但对大多数读者来说可能更像是一个“有趣的事实”。

    我将如何在这里应用它?

    您可以调用 class_addIvar 并将 Megapoint 实例变量添加到新类,或者您可以在运行时合成 Megapoint 类的 objc 变体(例如,Megapoint 的每个字段的实例变量)。

    前者相当于编译好的objc类:

    @interface MONMegapoint { Megapoint megapoint; } @end
    

    后者相当于编译好的objc类:

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

    添加 ivars 后,您可以添加/合成方法。

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

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

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

    【讨论】:

    • 人们的背景也会发挥作用......你需要如何使用该结构通常会消除一些可能性。 4 个浮点数非常简单,但结构布局因架构/编译器而异,无法使用连续的内存表示(例如 NSData)并期望它能够工作。穷人的 objc 序列化程序可能执行时间最慢,但如果您需要在任何 OS X 或 iOS 设备上保存/打开/传输 Megapoint,它是最兼容的。根据我的经验,最常见的方法是将结构简单地放在 objc 类中。如果您正在经历所有这些只是为了(续)
    • (cont) 如果您经历所有这些麻烦只是为了避免学习新的集合类型——那么您应该学习新的集合类型 =)std::vector(例如)更适合比 NSArray 持有 C/C++ 类型、结构和类。通过使用 NSValue、NSData 或 NSDictionary 类型的 NSArray,在增加大量分配和运行时开销的同时,您会失去很多类型安全性。如果您想坚持使用 C,那么他们通常会在堆栈上使用 malloc 和/或数组...但是 std::vector 隐藏了您的大部分复杂情况。
    • 事实上,如果你想像你提到的那样进行数组操作/迭代 - stl(c++ 标准库的一部分)非常适合。您有更多类型可供选择(例如,如果插入/删除比读取访问时间更重要),以及大量现有的容器操作方法。此外 - 它不是 C++ 中的裸内存 - 容器和模板函数是类型感知的,并在编译时检查 - 比从 NSData/NSValue 表示中提取任意字节字符串要安全得多。他们也有边界检查和内存的自动管理。 (续)
    • (cont) 如果您预计自己会有很多这样的低级工作,那么您现在就应该学习它 - 但学习需要时间。通过将这一切都包装在 objc 表示中,您将失去很多性能和类型安全性,同时让自己编写更多样板代码来访问和解释容器及其值(如果“因此,非常具体......”是正是你想做的)。
    • 这只是您可以使用的另一个工具。将 objc 与 c++、c++ 与 objc、c 与 c++ 或其他几种组合中的任何一种集成可能会出现复杂情况。无论如何,添加语言功能和使用多种语言确实需要付出很小的代价。一切顺利。例如,编译为 objc++ 时构建时间会增加。同样,这些资源也不能轻易地在其他项目中重用。当然,您可以重新实现语言功能......但这通常不是最好的解决方案。将 c++ 集成到 objc 项目中很好,就像在同一个项目中使用 objc 和 c 源代码一样“混乱”。(继续
    【解决方案5】:

    如果您要跨多个 abis/架构共享此数据,最好使用穷人的 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 类是正确的答案。

    【讨论】:

    【解决方案6】:

    我建议您将 std::vector 或 std::list 用于 C/C++ 类型,因为首先它比 NSArray 快,其次如果速度不够,您总是可以的为 STL 容器创建您自己的分配器并使它们更快。所有现代移动游戏、物理和音频引擎都使用 STL 容器来存储内部数据。只是因为他们真的很快。

    如果它不适合你——关于 NSValue 的人给出了很好的答案——我认为这是最可以接受的。

    【讨论】:

    • 这是一个有趣的说法。您是否有关于 STL 容器与 Cocoa 容器类的速度优势的文章的链接?
    • 这里有一篇关于 NSCFArray 与 std::vector 的有趣读物:ridiculousfish.com/blog/archives/2005/12/23/array 在您帖子的示例中,最大的损失是(通常)为每个元素创建一个 objc 对象表示(例如,NSValue、NSData ,或包含 Megapoint 的 Objc 类型需要分配并插入到引用计数系统中)。您实际上可以通过使用 Sedate Alien 的方法将 Megapoint 存储在一个特殊的 CFArray 中来避免这种情况,该 CFArray 使用连续分配的 Megapoints 的单独后备存储(尽管这两个示例都没有说明这种方法)。 (续)
    • 但随后使用 NSCFArray 与向量(或其他 stl 类型)将导致动态调度的额外开销、未内联的额外函数调用、大量类型安全以及许多机会优化器启动...那篇文章只关注插入、读取、遍历、删除。很有可能,你不会比 16 字节对齐的 c 数组 Megapoint pt[8]; 更快 - 这是 c++ 中的一个选项,以及专门的 c++ 容器(例如,std::array) - 另请注意,该示例没有添加特殊的对齐(选择 16 字节,因为它是 Megapoint 的大小)。 (续)
    • std::vector 将为此增加少量开销,并且一次分配(如果您知道您需要的大小)...但这比 99.9% 以上更接近金属的情况下需要。通常,除非大小是固定的或具有合理的最大值,否则您只会使用向量。
    【解决方案7】:

    您可以将它们作为 c 结构数组放入 NSData 或 NSMutableData 中,而不是尝试将 c 结构放入 NSArray。要访问它们,您需要这样做

    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;
    

    【讨论】:

      【解决方案8】:

      虽然使用 NSValue 可以很好地将结构存储为 Obj-C 对象,但您不能使用 NSArchiver/NSKeyedArchiver 对包含结构的 NSValue 进行编码。相反,您必须对单个结构成员进行编码...

      Apple's Archives and Serializations Programming Guide > Structures and Bit Fields

      【讨论】:

        【解决方案9】:

        对于您的结构,您可以添加 attribute 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)];
        

        【讨论】:

          【解决方案10】:

          除了使用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);
          }
          
          

          保留对象的内存需要您自担风险。

          【讨论】:

          • 这获得了十年前的问题徽章!
          【解决方案11】:

          Obj C 对象只是一个添加了一些元素的 C 结构。因此,只需创建一个自定义类,您将拥有 NSArray 所需的 C 结构类型。任何没有 NSObject 包含在其 C 结构中的额外内容的 C 结构都将无法被 NSArray 消化。

          使用 NSData 作为包装器可能只存储结构的副本而不是原始结构,如果这对您有影响的话。

          【讨论】:

            【解决方案12】:

            您可以使用 C-Structures 以外的 NSObject 类来存储信息。您可以轻松地将 NSObject 存储到 NSArray 中。

            【讨论】:

              猜你喜欢
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 2010-11-15
              • 2011-04-27
              • 1970-01-01
              • 1970-01-01
              • 2019-12-31
              • 1970-01-01
              相关资源
              最近更新 更多