Jaki jest najlepszy sposób umieszczenia struktury c w tablicy NSArray?

89

Jaki jest typowy sposób przechowywania struktur c w pliku NSArray? Zalety, wady, obsługa pamięci?

W szczególności, jaka jest różnica między valueWithBytesa valueWithPointer - podniesionymi przez justina i suma poniżej.

Oto link do dyskusji Apple valueWithBytes:objCType:dla przyszłych czytelników ...

Jeśli chodzi o myślenie poboczne i przyjrzenie się wydajności, Evgen poruszył kwestię używania STL::vectorw C ++ .

(To rodzi interesujący problem: czy istnieje szybka biblioteka c, STL::vectorpodobna do tej, która jest dużo lżejsza, która pozwala na minimalną „uporządkowaną obsługę tablic”…?)

Więc oryginalne pytanie ...

Na przykład:

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

Więc: jaki jest normalny, najlepszy, idiomatyczny sposób przechowywania własnej struktury NSArrayw takim idiomie i jak radzisz sobie z pamięcią w tym idiomie?

Zwróć uwagę, że szczególnie szukam zwykłego idiomu do przechowywania struktur. Oczywiście można by uniknąć tego problemu, tworząc nową małą klasę. Jednak chcę wiedzieć, jak zwykły idiom faktycznie umieszcza struktury w tablicy, dzięki.

A tak przy okazji, oto podejście NSData, które może być? nie najlepiej ...

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 jako punkt odniesienia i dzięki Jarret Hardie, oto jak przechowywać CGPointsi podobne w NSArray:

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

(zobacz W jaki sposób mogę łatwo dodawać obiekty CGPoint do NSArray? )

Fattie
źródło
Twój kod do konwersji na NSData powinien być w porządku… i bez wycieków pamięci… jednak równie dobrze można by użyć standardowej tablicy struktur C ++ Megapoint p [3];
Swapnil Luktuke
Nie możesz dodać nagrody, dopóki pytanie nie będzie miało dwóch dni.
Matthew Frederick
1
valueWithCGPoint nie jest jednak dostępny dla OSX. To część UIKit
lppier
@Ippier valueWithPoint jest dostępny na OS X
Schpaencoder

Odpowiedzi:

159

NSValue obsługuje nie tylko struktury CoreGraphics - możesz go również używać do własnych celów. Poleciłbym to zrobić, ponieważ klasa jest prawdopodobnie lżejsza niż w NSDataprzypadku prostych struktur danych.

Po prostu użyj wyrażenia podobnego do następującego:

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

Aby odzyskać wartość:

Megapoint p;
[value getValue:&p];
Justin Spahr-Summers
źródło
4
@Joe Blow @Catfish_Man W rzeczywistości kopiuje strukturę p, a nie wskaźnik do niej. @encodeDyrektywa zawiera wszystkie niezbędne informacje o tym, jak duży jest struktura. Kiedy zwolnisz NSValue(lub gdy zrobi to tablica), jej kopia struktury zostanie zniszczona. Jeśli używałeś getValue:w międzyczasie, nic ci nie jest. Zobacz sekcję „Używanie wartości” w „Tematy programowania liczb i wartości”: developer.apple.com/library/ios/documentation/Cocoa/Conceptual/…
Justin Spahr-Summers,
1
@Joe Blow W większości poprawne, z wyjątkiem tego, że nie byłoby w stanie zmienić w czasie wykonywania. Określasz typ C, który zawsze musi być w pełni znany. Gdyby mógł się „powiększyć” poprzez odniesienie do większej ilości danych, to prawdopodobnie zaimplementowałbyś to za pomocą wskaźnika, a @encodeopisałbyś strukturę tym wskaźnikiem, ale nie w pełni opisałby wskazywane dane, które rzeczywiście mogłyby się zmienić.
Justin Spahr-Summers
1
Czy NSValueautomatycznie zwolni pamięć struktury po jej zwolnieniu? Dokumentacja jest trochę niejasna w tym zakresie.
devios1
1
Więc dla jasności, jest NSValuewłaścicielem danych, które kopiuje do siebie i nie muszę się martwić o ich zwolnienie (w ramach ARC)?
devios1
1
@devios Correct. NSValuetak naprawdę nie zajmuje się „zarządzaniem pamięcią” jako takim - możesz myśleć o tym jak o posiadaniu wewnętrznej kopii wartości struktury. Gdyby struktura zawierała na przykład zagnieżdżone wskaźniki, NSValuenie wiedziałaby, jak zwolnić, skopiować lub cokolwiek z nimi zrobić - pozostawiłoby je nietknięte, kopiując adres w takiej postaci, w jakiej jest.
Justin Spahr-Summers
7

Sugerowałbym trzymanie się NSValuetrasy, ale jeśli naprawdę chcesz przechowywać zwykłe structtypy danych ol w swoim NSArray (i innych obiektach kolekcji w Cocoa), możesz to zrobić - choć pośrednio, używając Core Foundation i bezpłatnego mostkowania .

CFArrayRef(i jego zmienny odpowiednik CFMutableArrayRef) zapewniają programiście większą elastyczność podczas tworzenia obiektu tablicy. Zobacz czwarty argument wyznaczonego inicjatora:

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

Pozwala to zażądać, aby CFArrayRefobiekt korzystał z procedur zarządzania pamięcią Core Foundation, bez żadnych lub nawet z własnych procedur zarządzania pamięcią.

Obowiązkowy przykład:

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

Powyższy przykład całkowicie ignoruje problemy związane z zarządzaniem pamięcią. Struktura myStructjest tworzona na stosie i dlatego jest niszczona po zakończeniu funkcji - tablica będzie zawierała wskaźnik do obiektu, którego już nie ma. Możesz obejść ten problem, używając własnych procedur zarządzania pamięcią - stąd dlaczego ta opcja jest dostępna - ale wtedy musisz wykonać ciężką pracę polegającą na liczeniu odwołań, alokowaniu pamięci, zwalnianiu jej i tak dalej.

Nie polecałbym tego rozwiązania, ale zatrzymam je tutaj na wypadek, gdyby zainteresował go ktoś inny. :-)


Używanie twojej struktury jako przydzielonej na stercie (zamiast stosu) jest pokazane tutaj:

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
źródło
Mutowalne tablice (przynajmniej w tym przypadku) są tworzone w stanie pustym i dlatego nie potrzebują wskaźnika do wartości, które mają być w nich przechowywane. Różni się to od niezmiennej tablicy, w której zawartość jest „zamrażana” podczas tworzenia, a zatem wartości muszą być przekazywane do procedury inicjalizacji.
Sedate Alien
@Joe Blow: To doskonała uwaga, jeśli chodzi o zarządzanie pamięcią. Masz rację, że jesteś zdezorientowany: przykładowy kod, który zamieściłem powyżej, powodowałby tajemnicze awarie, w zależności od tego, kiedy stos funkcji zostanie nadpisany. Zacząłem szczegółowo omawiać, w jaki sposób można wykorzystać moje rozwiązanie, ale zdałem sobie sprawę, że ponownie wdrażam własne liczenie odniesień w Objective-C. Przepraszam za zwięzły kod - to nie kwestia uzdolnień, ale lenistwa. Nie ma sensu pisać kodu, którego inni nie mogą odczytać. :)
Sedate Alien
Gdybyś z przyjemnością „wyciekł” (z braku lepszego słowa) struct, z pewnością mógłbyś go raz przydzielić i nie zwolnić w przyszłości. Podałem tego przykład w mojej zredagowanej odpowiedzi. Nie była to również literówka myStruct, ponieważ była to struktura przydzielona na stosie, różniąca się od wskaźnika do struktury przydzielonej na stercie.
Sedate Alien
4

Podobną metodą dodawania struktury c jest przechowywanie wskaźnika i usuwanie referencji ze wskaźnika w ten sposób;

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
źródło
3

jeśli czujesz się oszołomiony lub naprawdę masz wiele klas do stworzenia: czasami warto dynamicznie skonstruować klasę objc (ref:) class_addIvar. w ten sposób możesz tworzyć dowolne klasy objc z dowolnych typów. możesz określić pole po polu lub po prostu przekazać informacje o strukturze (ale to praktycznie replikuje NSData). czasem przydatne, ale prawdopodobnie bardziej „zabawny fakt” dla większości czytelników.

Jak mam to zastosować tutaj?

możesz wywołać class_addIvar i dodać zmienną instancji Megapoint do nowej klasy, lub możesz zsyntetyzować wariant objc klasy Megapoint w czasie wykonywania (np. zmienną instancji dla każdego pola Megapoint).

pierwsza jest równoważna skompilowanej klasie objc:

@interface MONMegapoint { Megapoint megapoint; } @end

ta ostatnia jest odpowiednikiem skompilowanej klasy objc:

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

po dodaniu ivars możesz dodać / zsyntetyzować metody.

aby odczytać wartości przechowywane po stronie odbierającej, użyj zsyntetyzowanych metod object_getInstanceVariablelub valueForKey:(które często konwertują te skalarne zmienne instancji na reprezentacje NSNumber lub NSValue).

btw: wszystkie otrzymane odpowiedzi są przydatne, niektóre są lepsze / gorsze / nieprawidłowe w zależności od kontekstu / scenariusza. specyficzne potrzeby dotyczące pamięci, szybkości, łatwości w utrzymaniu, łatwości przenoszenia lub archiwizacji itp. decydują o tym, które jest najlepsze w danym przypadku ... ale nie ma „idealnego” rozwiązania, które byłoby idealne pod każdym względem. nie ma „najlepszego sposobu umieszczenia struktury c w tablicy NSArray”, po prostu „najlepszego sposobu umieszczenia struktury c w tablicy NSArray dla określonego scenariusza, przypadku lub zestawu wymagań ” - co byś miał sprecyzować.

ponadto NSArray jest generalnie interfejsem tablicowym wielokrotnego użytku dla typów o rozmiarze wskaźnika (lub mniejszych), ale istnieją inne kontenery, które są lepiej dostosowane do struktur c z wielu powodów (typowym wyborem dla struktur c jest std :: vector).

Justin
źródło
w grę wchodzą również pochodzenie ludzi ... sposób, w jaki musisz użyć tej struktury, często eliminuje niektóre możliwości. 4 elementy zmiennoprzecinkowe są dość niezawodne, ale układy struktur różnią się w zależności od architektury / kompilatora zbytnio, aby używać ciągłej reprezentacji pamięci (np. NSData) i oczekiwać, że zadziała. serializator objc biedaka prawdopodobnie ma najwolniejszy czas wykonywania, ale jest najbardziej kompatybilny, jeśli chcesz zapisać / otworzyć / przesłać Megapoint na dowolnym urządzeniu z systemem OS X lub iOS. Z mojego doświadczenia wynika, że ​​najczęstszym sposobem jest po prostu umieszczenie struktury w klasie objc. jeśli przechodzisz przez to wszystko, aby (cd)
Justin
(cd) Jeśli przechodzisz przez ten problem tylko po to, aby uniknąć uczenia się nowego typu kolekcji - powinieneś nauczyć się nowego typu kolekcji =) std::vector(na przykład) jest bardziej odpowiedni do przechowywania typów, struktur i klas C / C ++ niż NSArray. Używając NSArray typów NSValue, NSData lub NSDictionary, tracisz dużo bezpieczeństwa typów, dodając mnóstwo alokacji i narzutów w czasie wykonywania. Jeśli chcesz trzymać się C, zwykle używają malloc i / lub tablic na stosie ... ale std::vectorukrywają przed tobą większość komplikacji.
justin
w rzeczywistości, jeśli chcesz manipulować tablicami / iteracji, jak wspomniałeś - stl (część standardowych bibliotek C ++) jest do tego świetny. masz więcej typów do wyboru (np. jeśli wstawianie / usuwanie jest ważniejsze niż czasy dostępu do odczytu) i mnóstwo istniejących sposobów manipulowania kontenerami. również - to nie jest czysta pamięć w C ++ - kontenery i funkcje szablonów są świadome typów i sprawdzane podczas kompilacji - znacznie bezpieczniejsze niż wyciąganie dowolnego ciągu bajtów z reprezentacji NSData / NSValue. mają również sprawdzanie granic i głównie automatyczne zarządzanie pamięcią. (ciąg dalszy)
justin
(cd.) jeśli spodziewasz się, że będziesz miał dużo pracy na niskim poziomie, to powinieneś się tego nauczyć teraz - ale nauka zajmie trochę czasu. pakując to wszystko w reprezentacje objc, tracisz dużo wydajności i bezpieczeństwa typów, jednocześnie zmuszając siebie do pisania znacznie bardziej standardowego kodu, aby uzyskać dostęp do kontenerów i ich wartości i zinterpretować je dokładnie to, co chcesz zrobić).
justin
to tylko kolejne narzędzie do Twojej dyspozycji. mogą wystąpić komplikacje w przypadku integracji objc z c ++, c ++ z objc, c z c ++ lub dowolną z kilku innych kombinacji. dodawanie funkcji językowych i używanie wielu języków w każdym przypadku wiąże się z niewielkim kosztem. to idzie na wszystkie sposoby. na przykład czas kompilacji wydłuża się podczas kompilacji jako objc ++. co więcej, źródła te nie są tak łatwo ponownie wykorzystywane w innych projektach. Oczywiście, możesz ponownie zaimplementować funkcje językowe ... ale nie jest to często najlepsze rozwiązanie. integracja języka c ++ w projekcie objc jest w porządku, jest mniej więcej tak „niechlujna”, jak używanie źródeł objc i c w tym samym projekcie. (ciąg dalszy
justin
3

najlepiej byłoby użyć serializatora objc biedaka, jeśli udostępniasz te dane na wielu abis / architekturach:

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

... zwłaszcza jeśli struktura może się zmieniać w czasie (lub w zależności od platformy docelowej). nie jest tak szybki jak inne opcje, ale jest mniej prawdopodobne, że zepsuje się w niektórych warunkach (które nie zostały określone jako ważne lub nie).

jeśli serializowana reprezentacja nie kończy procesu, to rozmiar / kolejność / wyrównanie dowolnych struktur nie powinno się zmieniać, a istnieją opcje, które są prostsze i szybsze.

w obu przypadkach już dodajesz obiekt zliczany przez odniesienie (w porównaniu do NSData, NSValue), więc ... utworzenie klasy objc, która przechowuje Megapoint, jest w wielu przypadkach właściwą odpowiedzią.

Justin
źródło
@Joe Blow coś, co wykonuje serializację. dla odniesienia: en.wikipedia.org/wiki/Serialization , parashift.com/c++-faq-lite/serialization.html , a także „Przewodnik programowania archiwów i serializacji” firmy Apple.
justin,
zakładając, że plik xml właściwie coś reprezentuje, to tak - jest to jedna powszechna forma serializacji czytelnej dla człowieka.
justin
0

Proponuję użyć std :: vector lub std :: list dla typów C / C ++, ponieważ na początku jest po prostu szybszy niż NSArray, a po drugie, jeśli nie będzie dla Ciebie wystarczającej prędkości - zawsze możesz stworzyć własną podzielniki do kontenerów STL i uczynić je jeszcze szybszymi. Wszystkie nowoczesne silniki gier mobilnych, fizyki i audio używają kontenerów STL do przechowywania danych wewnętrznych. Tylko dlatego, że naprawdę szybko.

Jeśli to nie dla ciebie - są dobre odpowiedzi od facetów na temat NSValue - myślę, że jest to najbardziej akceptowalne.

Evgen Bodunov
źródło
STL to biblioteka częściowo zawarta w standardowej bibliotece C ++. en.wikipedia.org/wiki/Standard_Template_Library cplusplus.com/reference/stl/vector
Evgen Bodunov
To interesujące twierdzenie. Czy masz link do artykułu o przewadze szybkości kontenerów STL w porównaniu z klasami kontenerów Cocoa?
Sedate Alien
oto interesująca lektura na temat NSCFArray vs std :: vector: ridiculousfish.com/blog/archives/2005/12/23/array w przykładzie w twoim poście, największą stratą jest (zazwyczaj) utworzenie reprezentacji obiektu objc na element (np. , NSValue, NSData lub Objc zawierający Megapoint wymaga alokacji i wstawienia do systemu zliczanego ref). można by tego uniknąć, stosując podejście Sedate Alien do przechowywania Megapoint w specjalnym CFArray, który wykorzystuje oddzielny magazyn zapasowy z ciągłymi przydzielonymi Megapointami (chociaż żaden przykład nie ilustruje tego podejścia). (cd.)
justin
ale wtedy użycie NSCFArray vs vector (lub innego typu stl) spowoduje dodatkowe obciążenie dla dynamicznego wysyłania, dodatkowe wywołania funkcji, które nie są wbudowane, mnóstwo bezpieczeństwa typów i wiele szans na uruchomienie optymalizatora ... to artykuł skupia się tylko na wstawianiu, czytaniu, spacerowaniu, usuwaniu. jest szansa, że ​​nie dostaniesz szybciej niż 16-bajtowa wyrównana tablica c Megapoint pt[8];- jest to opcja w c ++ i wyspecjalizowanych kontenerach c ++ (np. std::array) - również zauważ, że przykład nie dodaje specjalnego wyrównania (wybrano 16 bajtów ponieważ jest wielkości Megapoint). (ciąg dalszy)
justin
std::vectordoda do tego niewielką ilość narzutów i jedną alokację (jeśli znasz rozmiar, którego potrzebujesz) ... ale jest to bliższe metalowi niż potrzebuje ponad 99,9% przypadków. zazwyczaj używałbyś po prostu wektora, chyba że rozmiar jest stały lub ma rozsądne maksimum.
justin
0

Zamiast próbować umieścić c struct w NSArray, możesz umieścić je w NSData lub NSMutableData jako tablicę ac struktur. Aby uzyskać do nich dostęp, wystarczy

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

albo wtedy ustawić

struct MyStruct    * theStruct = (struct MyStruct*)[myData mutableBytes];
theStruct[2].integerNumber = 10;
Nathan Day
źródło
0

Do swojej struktury możesz dodać atrybut objc_boxable i użyć @()składni, aby umieścić swoją strukturę w instancji NSValue bez wywoływania 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)];
Andrzeja Romanowa
źródło
-2

Obiekt Obj C to po prostu struktura C z kilkoma dodanymi elementami. Więc po prostu utwórz niestandardową klasę, a otrzymasz typ struktury C, którego wymaga NSArray. Każda struktura C, która nie ma dodatkowej skorupy, jaką NSObject zawiera w swojej strukturze C, będzie niestrawna dla NSArray.

Używanie NSData jako opakowania może oznaczać tylko przechowywanie kopii struktur, a nie oryginalnych struktur, jeśli ma to dla Ciebie znaczenie.

hotpaw2
źródło
-3

Do przechowywania informacji można używać klas NSObject innych niż C-Structures. I możesz łatwo przechowywać ten obiekt NSObject w NSArray.

Satya
źródło