Gdzie umieścić iVars w „nowoczesnym” Objective-C?

82

Książka „iOS6 by Tutorials” autorstwa Raya Wenderlicha zawiera bardzo fajny rozdział o pisaniu bardziej „nowoczesnego” kodu Objective-C. W jednej sekcji książki opisują, jak przenieść iVars z nagłówka klasy do pliku implementacji. Ponieważ wszystkie iVary powinny być prywatne, wydaje się to słuszne.

Ale do tej pory znalazłem 3 sposoby na zrobienie tego. Każdy robi to inaczej.

1.) Umieść iVars pod @implementantion wewnątrz bloku nawiasów klamrowych (tak jest to zrobione w książce).

2.) Umieść iVars pod @implementantion bez bloku nawiasów klamrowych

3.) Umieść iVars wewnątrz prywatnego interfejsu powyżej @implementantion (rozszerzenie klasy)

Wszystkie te rozwiązania wydają się działać dobrze i do tej pory nie zauważyłem żadnej różnicy w zachowaniu mojej aplikacji. Wydaje mi się, że nie ma „właściwego” sposobu na zrobienie tego, ale muszę napisać kilka tutoriali i wybrać tylko jeden sposób dla mojego kodu.

Którędy mam iść?

Edycja: mówię tutaj tylko o iVars. Nie właściwości. Tylko dodatkowe zmienne, których obiekt potrzebuje tylko dla siebie i które nie powinny być wystawiane na zewnątrz.

Przykłady kodu

1)

#import "Person.h"

@implementation Person
{
    int age;
    NSString *name;
}

- (id)init
{
    self = [super init];
    if (self)
    {
        age = 40;
        name = @"Holli";
    }
    return self;
}
@end

2)

#import "Person.h"

@implementation Person

int age;
NSString *name;


- (id)init
{
    self = [super init];
    if (self)
    {
        age = 40;
        name = @"Holli";
    }
    return self;
}
@end

3)

#import "Person.h"

@interface Person()
{
    int age;
    NSString *name;
}
@end

@implementation Person

- (id)init
{
    self = [super init];
    if (self)
    {
        age = 40;
        name = @"Holli";
    }
    return self;
}
@end
TalkingCode
źródło
4
Żadnego komentarza na temat „dobrze”, ale inną opcją - do której wydaje mi się zmierzać - jest całkowite zrzucenie iVars i uczynienie wszystkiego właściwością (głównie w rozszerzeniu klasy w pliku .m). Spójność oszczędza mi myślenia o szczegółach implementacji stanu.
Phillip Mills
1
To pytanie przyniosłoby wiele korzyści innym, gdybyś zaktualizował je, aby pokazać prawdziwe przykłady kodu demonstrujące każdą z 3 opcji. Osobiście używam tylko opcji 1, widziałem opcję 3, ale nie znam opcji 2.
rmaddy
1
Dzięki, maddy. Dodałem przykładowy kod.
TalkingCode
4
Przepraszamy, właśnie zauważyłem aktualizację. Opcja 2 jest nieprawidłowa. To nie tworzy bluszczów. To tworzy zmienne globalne pliku. Każda instancja tej klasy będzie współużytkować ten jeden zestaw zmiennych. Możesz użyć zmiennych klasowych, a nie zmiennych instancji.
rmaddy

Odpowiedzi:

162

Możliwość umieszczania zmiennych instancji w @implementationbloku lub w rozszerzeniu klasy jest funkcją „nowoczesnego środowiska uruchomieniowego Objective-C”, z którego korzysta każda wersja systemu iOS oraz 64-bitowe programy systemu Mac OS X.

Jeśli chcesz pisać 32-bitowe aplikacje Mac OS X, musisz umieścić zmienne instancji w @interfacedeklaracji. Prawdopodobnie nie musisz jednak obsługiwać 32-bitowej wersji swojej aplikacji. OS X obsługuje aplikacje 64-bitowe od wersji 10.5 (Leopard), która została wydana ponad pięć lat temu.

Załóżmy więc, że piszesz tylko aplikacje, które będą korzystać z nowoczesnego środowiska wykonawczego. Gdzie powinieneś położyć bluszcz?

Opcja 0: w @interface(Nie rób tego)

Najpierw przyjrzyjmy się, dlaczego nie chcemy umieszczać zmiennych instancji w @interfacedeklaracji.

  1. Umieszczenie zmiennych instancji w @interfaceankiecie ujawnia szczegóły implementacji użytkownikom tej klasy. Może to doprowadzić tych użytkowników (nawet Ciebie, gdy używasz własnych klas!) Do polegania na szczegółach implementacji, których nie powinni. (Jest to niezależne od tego, czy deklarujemy ivars @private).

  2. Umieszczenie zmiennych instancji w an @interfacepowoduje, że kompilacja trwa dłużej, ponieważ za każdym razem, gdy dodajemy, zmieniamy lub usuwamy deklarację ivar, musimy rekompilować każdy .mplik, który importuje interfejs.

Dlatego nie chcemy umieszczać zmiennych instancji w @interface. Gdzie powinniśmy je umieścić?

Opcja 2: @implementationbez szelek (nie rób tego)

Następnie omówmy twoją opcję 2, „Umieść iVars pod @implementantion bez bloku nawiasów klamrowych”. To nie deklaruje zmiennych instancji! Mówisz o tym:

@implementation Person

int age;
NSString *name;

...

Ten kod definiuje dwie zmienne globalne. Nie deklaruje żadnych zmiennych instancji.

Dobrze jest zdefiniować zmienne globalne w swoim .mpliku, nawet w swoim @implementation, jeśli potrzebujesz zmiennych globalnych - na przykład, ponieważ chcesz, aby wszystkie Twoje instancje miały wspólny stan, na przykład pamięć podręczną. Ale nie możesz użyć tej opcji do zadeklarowania ivarów, ponieważ nie deklaruje ona ivarów. (Ponadto zmienne globalne prywatne dla implementacji powinny być zwykle deklarowane, staticaby uniknąć zanieczyszczania globalnej przestrzeni nazw i ryzyka błędów czasu łącza).

To pozostawia opcje 1 i 3.

Opcja 1: w @implementationz szelkami (zrób to)

Zwykle chcemy skorzystać z opcji 1: umieść je w swoim głównym @implementationbloku, w nawiasach klamrowych, na przykład:

@implementation Person {
    int age;
    NSString *name;
}

Umieściliśmy je tutaj, ponieważ utrzymuje to ich istnienie w tajemnicy, zapobiegając problemom, które opisałem wcześniej, i ponieważ zwykle nie ma powodu, aby umieszczać je w rozszerzeniu klasy.

Kiedy więc chcemy użyć Twojej opcji 3, umieszczając je w rozszerzeniu klasy?

Opcja 3: w rozszerzeniu klasy (rób to tylko wtedy, gdy jest to konieczne)

Prawie nigdy nie ma powodu, aby umieszczać je w rozszerzeniu klasy w tym samym pliku, co klasa @implementation. Równie dobrze moglibyśmy po prostu umieścić je @implementationw tym przypadku.

Ale czasami możemy napisać klasę, która jest na tyle duża, że ​​chcemy podzielić jej kod źródłowy na wiele plików. Możemy to zrobić za pomocą kategorii. Na przykład, gdybyśmy implementowali UICollectionView(dość dużą klasę), moglibyśmy zdecydować, że chcemy umieścić kod zarządzający kolejkami widoków wielokrotnego użytku (komórki i widoki uzupełniające) w oddzielnym pliku źródłowym. Moglibyśmy to zrobić, oddzielając te wiadomości do kategorii:

// UICollectionView.h

@interface UICollectionView : UIScrollView

- (id)initWithFrame:(CGRect)frame collectionViewLayout:(UICollectionViewLayout *)layout;
@property (nonatomic, retain) UICollectionView *collectionViewLayout;
// etc.

@end

@interface UICollectionView (ReusableViews)

- (void)registerClass:(Class)cellClass forCellWithReuseIdentifier:(NSString *)identifier;
- (void)registerNib:(UINib *)nib forCellWithReuseIdentifier:(NSString *)identifier;

- (void)registerClass:(Class)viewClass forSupplementaryViewOfKind:(NSString *)elementKind withReuseIdentifier:(NSString *)identifier;
- (void)registerNib:(UINib *)nib forSupplementaryViewOfKind:(NSString *)kind withReuseIdentifier:(NSString *)identifier;

- (id)dequeueReusableCellWithReuseIdentifier:(NSString *)identifier forIndexPath:(NSIndexPath*)indexPath;
- (id)dequeueReusableSupplementaryViewOfKind:(NSString*)elementKind withReuseIdentifier:(NSString *)identifier forIndexPath:(NSIndexPath*)indexPath;

@end

OK, teraz możemy zaimplementować główne UICollectionViewmetody programu UICollectionView.mi możemy zaimplementować metody, które zarządzają widokami wielokrotnego użytku UICollectionView+ReusableViews.m, co sprawia, że ​​nasz kod źródłowy jest nieco łatwiejszy w zarządzaniu.

Ale nasz kod zarządzania widokiem wielokrotnego użytku wymaga pewnych zmiennych instancji. Te zmienne muszą być uwidocznione w głównej klasie @implementationw UICollectionView.m, więc kompilator wyemituje je w .opliku. Musimy również udostępnić te zmienne instancji kodowi w UICollectionView+ReusableViews.m, aby te metody mogły używać funkcji ivars.

W tym miejscu potrzebujemy rozszerzenia klasy. Możemy umieścić moduły Ivars do zarządzania widokiem wielokrotnego użytku w rozszerzeniu klasy w prywatnym pliku nagłówkowym:

// UICollectionView_ReusableViewsSupport.h

@interface UICollectionView () {
    NSMutableDictionary *registeredCellSources;
    NSMutableDictionary *spareCellsByIdentifier;

    NSMutableDictionary *registeredSupplementaryViewSources;
    NSMutableDictionary *spareSupplementaryViewsByIdentifier;
}

- (void)initReusableViewSupport;

@end

Nie będziemy wysyłać tego pliku nagłówkowego do użytkowników naszej biblioteki. Po prostu zaimportujemy go do UICollectionView.mi do środka UICollectionView+ReusableViews.m, aby wszystko, co musi zobaczyć te ivars, mogło je zobaczyć. Dodaliśmy również metodę, którą chcemy init, aby wywoływana była metoda main w celu zainicjowania kodu zarządzania widokiem wielokrotnego użytku. Wywołamy tę metodę z -[UICollectionView initWithFrame:collectionViewLayout:]in UICollectionView.mi zaimplementujemy ją w UICollectionView+ReusableViews.m.

rob mayoff
źródło
4
Opcja 3: użyteczne również, gdy 1) chcesz wymusić używanie właściwości dla swoich ivars, nawet w ramach własnej implementacji (@property int age), i 2) chcesz zastąpić właściwość publiczną tylko do odczytu (@property (readonly) int age) w nagłówku, aby umożliwić kodowi dostęp do właściwości jako readwrite w implementacji (@property (readwrite) int age). Chyba że istnieją inne sposoby, aby to zrobić, @rob mayoff?
leanne
@leanne Jeśli chcesz przesłonić readonlywłaściwość jako prywatną readwrite, musisz użyć rozszerzenia klasy. Ale pytanie dotyczyło tego, gdzie umieścić blaszki, a nie gdzie umieścić oświadczenia majątkowe. Nie widzę, jak użycie rozszerzenia klasy może uniemożliwić implementacji dostęp do ivars. Aby to zrobić, należałoby umieścić implementacje metod w kategorii, ponieważ kompilator musi zobaczyć wszystkie rozszerzenia klas deklarujące ivar, zanim zobaczy rozszerzenie @implementation.
rob mayoff
@rob mayoff, prawda i prawda - zdobywasz ważne punkty. Holli stwierdziła, że ​​nie chodzi tu o właściwości, a „egzekwowanie” było z mojej strony nieco mocne, jeśli chodzi o ich wykorzystanie. Niezależnie od tego, jeśli ktoś chce użyć właściwości, aby uzyskać dostęp do ich bluszczów, zamiast robić to bezpośrednio, byłby to sposób na zrobienie tego. I oczywiście nadal możesz uzyskać bezpośredni dostęp do ivars za pomocą '_', (_age = someAge). Dodałem ten komentarz, ponieważ uważałem, że warto wspomnieć o używaniu własności przy omawianiu ivarów, ponieważ wiele osób używa ich razem, a to byłby sposób na zrobienie tego.
leanne
To jest „Opcja 0: w @interface (nie rób tego)”.
rob mayoff
5

Opcja 2 jest całkowicie błędna. Są to zmienne globalne, a nie zmienne instancji.

Opcje 1 i 3 są zasadniczo identyczne. To nie ma żadnego znaczenia.

Wybór polega na tym, czy umieścić zmienne instancji w pliku nagłówkowym, czy w pliku implementacji. Zaletą korzystania z pliku nagłówkowego jest to, że masz szybki i łatwy skrót klawiaturowy (Command + Control + Up w Xcode) do przeglądania i edytowania zmiennych instancji oraz deklaracji interfejsu.

Wadą jest to, że ujawniasz prywatne szczegóły swojej klasy w publicznym nagłówku. W niektórych przypadkach jest to niepożądane, szczególnie jeśli piszesz kod do użytku innych. Innym potencjalnym problemem jest to, że jeśli używasz Objective-C ++, dobrze jest unikać umieszczania jakichkolwiek typów danych C ++ w pliku nagłówkowym.

Zmienne instancji implementacji są świetną opcją w niektórych sytuacjach, ale dla większości mojego kodu nadal umieszczam zmienne instancji w nagłówku tylko dlatego, że jest to wygodniejsze dla mnie jako programisty pracującego w Xcode. Radzę robić to, co uważasz za wygodniejsze.

Darren
źródło
4

W dużej mierze ma to związek z widocznością ivar dla podklas. Podklasy nie będą miały dostępu do zmiennych instancji zdefiniowanych w@implementation bloku.

W przypadku kodu wielokrotnego użytku, który planuję rozpowszechniać (np. Kod biblioteki lub frameworka), w którym wolę nie ujawniać zmiennych instancji do publicznego wglądu, skłaniam się do umieszczania ivarów w bloku implementacji (opcja 1).

FluffulousChimp
źródło
3

Należy umieścić zmienne instancji w prywatnym interfejsie powyżej implementacji. Wariant 3.

Dokumentacją do przeczytania na ten temat jest przewodnik Programming in Objective-C .

Z dokumentacji:

Zmienne instancji można definiować bez właściwości

Najlepszą praktyką jest używanie właściwości obiektu za każdym razem, gdy chcesz śledzić wartość lub inny obiekt.

Jeśli musisz zdefiniować własne zmienne instancji bez deklarowania właściwości, możesz dodać je w nawiasach klamrowych u góry interfejsu lub implementacji klasy, na przykład:

JoePasq
źródło
1
Zgadzam się z tobą: jeśli miałbyś zdefiniować ivar, rozszerzenie klasy prywatnej jest najlepszym miejscem. Co ciekawe, dokumentacja, do której się odwołujesz, nie tylko nie określa miejsca, w którym należy zdefiniować bluszcze (przedstawia jedynie wszystkie trzy alternatywy), ale idzie dalej i zaleca, aby w ogóle nie używać bluszczów, ale raczej „najlepszym sposobem jest używanie właściwości”. To najlepsza rada, jaką do tej pory widziałem.
Rob
1

Publiczne ivary powinny być naprawdę zadeklarowane jako właściwości w @interface (prawdopodobnie to, o czym myślisz w 1). Prywatne ivars, jeśli korzystasz z najnowszego Xcode i nowoczesnego środowiska uruchomieniowego (64-bitowy system OS X lub iOS), można zadeklarować w @implementation (2), a nie w rozszerzeniu klasy, co prawdopodobnie jest tym, co ' myślę o w 3.

Jon Shier
źródło