Объявление / определение местоположения переменных в ObjectiveC?

113

С тех пор, как я начал работать над приложениями для iOS и с целью C, я был озадачен тем, что в разных местах можно объявлять и определять переменные. С одной стороны, у нас есть традиционный подход C, с другой - новые директивы ObjectiveC, которые добавляют объектно-ориентированный подход к этому. Не могли бы вы, ребята, помочь мне понять лучшие практики и ситуации, в которых я хотел бы использовать эти местоположения для своих переменных и, возможно, исправить мое нынешнее понимание?

Вот образец класса (.h и .m):

#import <Foundation/Foundation.h>

// 1) What do I declare here?

@interface SampleClass : NSObject
{
    // 2) ivar declarations
    // Pretty much never used?
}

// 3) class-specific method / property declarations

@end

и

#import "SampleClass.h"

// 4) what goes here?

@interface SampleClass()

// 5) private interface, can define private methods and properties here

@end

@implementation SampleClass
{
    // 6) define ivars
}

// 7) define methods and synthesize properties from both public and private
//    interfaces

@end
  • Насколько я понимаю, пункты 1 и 4 состоят в том, что это декларации и определения на основе файлов в стиле C, которые не имеют никакого представления о концепции класса и, следовательно, должны использоваться именно так, как они будут использоваться в C. Я видел их ранее использовался для реализации статических синглтонов на основе переменных. Есть ли другие удобные способы использования, которые мне не хватает?
  • Мой вывод из работы с iOS заключается в том, что ivars были полностью отменены за пределами директивы @synthesize и, таким образом, могут в основном игнорироваться. Так ли это?
  • По поводу 5: зачем мне вообще объявлять методы в частных интерфейсах? Мои методы частного класса отлично компилируются без объявления в интерфейсе. Это в основном для удобства чтения?

Спасибо большое, ребята!

Александр Курилин
источник

Ответы:

154

Я понимаю ваше замешательство. Тем более, что недавние обновления Xcode и нового компилятора LLVM изменили способ объявления ivars и свойств.

До «современного» Objective-C (в «старом» Obj-C 2.0) у вас не было большого выбора. Раньше переменные экземпляра объявлялись в заголовке между фигурными скобками { }:

// MyClass.h
@interface MyClass : NSObject {
    int myVar;
}
@end

Вы могли получить доступ к этим переменным только в своей реализации, но не из других классов. Для этого вам нужно было объявить методы доступа, которые выглядят примерно так:

// MyClass.h
@interface MyClass : NSObject {
    int myVar;
}

- (int)myVar;
- (void)setMyVar:(int)newVar;

@end


// MyClass.m
@implementation MyClass

- (int)myVar {
   return myVar;
}

- (void)setMyVar:(int)newVar {
   if (newVar != myVar) {
      myVar = newVar;
   }
}

@end

Таким образом, вы смогли получить и установить эту переменную экземпляра из других классов, используя обычный синтаксис квадратных скобок для отправки сообщений (методов вызова):

// OtherClass.m
int v = [myClass myVar];  // assuming myClass is an object of type MyClass.
[myClass setMyVar:v+1];

Поскольку ручное объявление и реализация каждого метода доступа было довольно утомительным @propertyи @synthesizeбыло введено для автоматического создания методов доступа:

// MyClass.h
@interface MyClass : NSObject {
    int myVar;
}
@property (nonatomic) int myVar;
@end

// MyClass.m
@implementation MyClass
@synthesize myVar;
@end

В результате код становится более четким и коротким. Методы доступа будут реализованы за вас, и вы по-прежнему можете использовать синтаксис скобок, как и раньше. Но кроме того, вы также можете использовать синтаксис с точкой для доступа к свойствам:

// OtherClass.m
int v = myClass.myVar;   // assuming myClass is an object of type MyClass.
myClass.myVar = v+1;

Начиная с Xcode 4.4 вам больше не нужно самостоятельно объявлять переменную экземпляра, и вы @synthesizeтоже можете пропустить . Если вы не объявите ivar, компилятор добавит его за вас, а также сгенерирует методы доступа без вашего использования @synthesize.

Имя по умолчанию для автоматически созданного ivar - это имя или ваше свойство, начинающееся с символа подчеркивания. Вы можете изменить сгенерированное имя ivar, используя@synthesize myVar = iVarName;

// MyClass.h
@interface MyClass : NSObject 
@property (nonatomic) int myVar;
@end

// MyClass.m
@implementation MyClass
@end

Это будет работать точно так же, как приведенный выше код. В целях совместимости вы все равно можете объявлять ivars в заголовке. Но поскольку единственная причина, по которой вы хотели бы сделать это (а не объявлять свойство), - это создать частную переменную, теперь вы можете сделать это и в файле реализации, и это предпочтительный способ.

@interfaceБлок в файле реализация на самом деле является расширение и может использоваться для передачи методов объявить , то (больше не нужно) и (перо) свойства DECLARE. Например, вы можете объявить readonlyсвойство в заголовке.

@property (nonatomic, readonly) myReadOnlyVar;

и повторно объявите его в своем файле реализации, readwriteчтобы можно было установить его, используя синтаксис свойств, а не только через прямой доступ к ivar.

Что касается объявления переменных полностью вне блока any @interfaceor @implementation, да, это простые переменные C, которые работают точно так же.

DrummerB
источник
2
отличный ответ! Также обратите внимание: stackoverflow.com/questions/9859719/…
nycynik
44

Сначала прочтите ответ @DrummerB. Это хороший обзор того, почему и что вам следует делать. Имея это в виду, к вашим конкретным вопросам:

#import <Foundation/Foundation.h>

// 1) What do I declare here?

Здесь нет фактических определений переменных (это технически законно, если вы точно знаете, что делаете, но никогда не делайте этого). Вы можете определить несколько других типов вещей:

  • typdefs
  • перечислений
  • экстернов

Экстерны выглядят как объявления переменных, но это просто обещание объявить их где-то еще. В ObjC их следует использовать только для объявления констант и, как правило, только строковых констант. Например:

extern NSString * const MYSomethingHappenedNotification;

Затем вы должны в своем .mфайле объявить фактическую константу:

NSString * const MYSomethingHappenedNotification = @"MYSomethingHappenedNotification";

@interface SampleClass : NSObject
{
    // 2) ivar declarations
    // Pretty much never used?
}

Как отмечает DrummerB, это наследие. Не кладите сюда ничего.


// 3) class-specific method / property declarations

@end

Ага.


#import "SampleClass.h"

// 4) what goes here?

Внешние константы, как описано выше. Также сюда могут быть помещены статические переменные файла. Это эквивалент переменных класса в других языках.


@interface SampleClass()

// 5) private interface, can define private methods and properties here

@end

Ага


@implementation SampleClass
{
    // 6) define ivars
}

Но очень редко. Почти всегда вы должны позволять clang (Xcode) создавать переменные за вас. Исключения обычно связаны с ivars, не относящимися к ObjC (например, с объектами Core Foundation и особенно с объектами C ++, если это класс ObjC ++), или с ivars, которые имеют странную семантику хранения (например, ivars, которые по какой-то причине не соответствуют свойству).


// 7) define methods and synthesize properties from both public and private
//    interfaces

Как правило, вы больше не должны использовать @synthesize. Clang (Xcode) сделает это за вас, и вы должны позволить.

За последние несколько лет все стало намного проще. Побочным эффектом является то, что теперь существует три разных эпохи (Fragile ABI, Non-fragile ABI, Non-fragile ABI + auto-syntheisze). Поэтому, когда вы видите старый код, это может немного сбить с толку. Таким образом, путаница возникает из-за простоты: D

Роб Напье
источник
Просто интересно, а почему бы нам не синтезировать явно? Я делаю это, потому что считаю, что мой код легче понять, особенно когда некоторые свойства имеют синтезированные аксессоры, а некоторые имеют собственные реализации, поскольку я привык к синтезу. Есть ли недостатки у явного синтеза?
Metabble
Проблема с его использованием в качестве документации в том, что на самом деле он ничего не документирует. Несмотря на использование синтеза, вы могли переопределить один или оба метода доступа. По строке синтеза невозможно отличить что-нибудь действительно полезное. Единственное, что хуже отсутствия документации - это вводящая в заблуждение документация. Пропусти это.
Роб Нэпир,
3
Почему №6 редко? Разве это не самый простой способ получить частную переменную?
pfrank
Самый простой и лучший способ получить частную собственность - №5.
Роб Напье
1
@RobNapier Иногда по-прежнему необходимо использовать @ synthesize (например, если свойство доступно только для чтения, его метод доступа переопределен)
Энди
6

Я тоже новенький, так что, надеюсь, я ничего не напортачил.

1 и 4: Глобальные переменные в стиле C: они имеют файловую область видимости. Разница между ними в том, что, поскольку они имеют размер файла, первый будет доступен любому, кто импортирует заголовок, а второй - нет.

2: переменные экземпляра. Большинство переменных экземпляра синтезируются и извлекаются / устанавливаются с помощью средств доступа с использованием свойств, поскольку это делает управление памятью приятным и простым, а также дает вам легкую для понимания точечную нотацию.

6: ivars реализации несколько новы. Это хорошее место для размещения частных ivars, поскольку вы хотите отображать только то, что необходимо в общедоступном заголовке, но подклассы не наследуют их AFAIK.

3 и 7: объявления открытых методов и свойств, затем их реализации.

5: Частный интерфейс. Я всегда использую частные интерфейсы, когда могу, чтобы поддерживать чистоту и создавать своего рода эффект черного ящика. Если им не нужно об этом знать, напишите об этом. Тоже делаю для читабельности, не знаю, есть ли другие причины.

Metabble
источник
1
Не думайте, что вы что-то напортачили :) Несколько комментариев - №1 и №4, особенно с №4, часто вы видите статические переменные хранилища. # 1 часто вы видите указанное внешнее хранилище, а затем фактическое хранилище, выделенное в # 4. # 2) только обычно, если это необходимо подклассу по какой-либо причине. # 5 больше не нужно пересылать объявление частных методов.
Карл Визи,
Да, я сам только что проверил форвардное объявление. Раньше он выдавал предупреждение, если один частный метод вызывал другой, который был определен после него без предварительного объявления, верно? Я был немного удивлен, когда меня не предупредили.
Metabble
Ага, это новая часть компилятора. За последнее время они действительно добились большого прогресса.
Карл Визи,
6

Это пример всех видов переменных, объявленных в Objective-C. Имя переменной указывает на ее доступ.

Файл: Animal.h

@interface Animal : NSObject
{
    NSObject *iProtected;
@package
    NSObject *iPackage;
@private
    NSObject *iPrivate;
@protected
    NSObject *iProtected2; // default access. Only visible to subclasses.
@public
    NSObject *iPublic;
}

@property (nonatomic,strong) NSObject *iPublic2;

@end

Файл: Animal.m

#import "Animal.h"

// Same behaviour for categories (x) than for class extensions ().
@interface Animal(){
@public
    NSString *iNotVisible;
}
@property (nonatomic,strong) NSObject *iNotVisible2;
@end

@implementation Animal {
@public
    NSString *iNotVisible3;
}

-(id) init {
    self = [super init];
    if (self){
        iProtected  = @"iProtected";
        iPackage    = @"iPackage";
        iPrivate    = @"iPrivate";
        iProtected2 = @"iProtected2";
        iPublic     = @"iPublic";
        _iPublic2    = @"iPublic2";

        iNotVisible   = @"iNotVisible";
        _iNotVisible2 = @"iNotVisible2";
        iNotVisible3  = @"iNotVisible3";
    }
    return self;
}

@end

Обратите внимание, что переменные iNotVisible не видны из других классов. Это проблема видимости, поэтому объявление их с @propertyили @publicне меняет.

Внутри конструктора рекомендуется обращаться к переменным, объявленным с @propertyиспользованием подчеркивания, selfчтобы избежать побочных эффектов.

Попробуем получить доступ к переменным.

Файл: Cow.h

#import "Animal.h"
@interface Cow : Animal
@end

Файл: Cow.m

#import "Cow.h"
#include <objc/runtime.h>

@implementation Cow

-(id)init {
    self=[super init];
    if (self){
        iProtected    = @"iProtected";
        iPackage      = @"iPackage";
        //iPrivate    = @"iPrivate"; // compiler error: variable is private
        iProtected2   = @"iProtected2";
        iPublic       = @"iPublic";
        self.iPublic2 = @"iPublic2"; // using self because the backing ivar is private

        //iNotVisible   = @"iNotVisible";  // compiler error: undeclared identifier
        //_iNotVisible2 = @"iNotVisible2"; // compiler error: undeclared identifier
        //iNotVisible3  = @"iNotVisible3"; // compiler error: undeclared identifier
    }
    return self;
}
@end

Мы все еще можем получить доступ к невидимым переменным, используя среду выполнения.

Файл: Cow.m (часть 2)

@implementation Cow(blindAcess)

- (void) setIvar:(NSString*)name value:(id)value {
    Ivar ivar = class_getInstanceVariable([self class], [name UTF8String]);
    object_setIvar(self, ivar, value);
}

- (id) getIvar:(NSString*)name {
    Ivar ivar = class_getInstanceVariable([self class], [name UTF8String]);
    id thing = object_getIvar(self, ivar);
    return thing;
}

-(void) blindAccess {
    [self setIvar:@"iNotVisible"  value:@"iMadeVisible"];
    [self setIvar:@"_iNotVisible2" value:@"iMadeVisible2"];
    [self setIvar:@"iNotVisible3" value:@"iMadeVisible3"];
    NSLog(@"\n%@ \n%@ \n%@",
          [self getIvar:@"iNotVisible"],
          [self getIvar:@"_iNotVisible2"],
          [self getIvar:@"iNotVisible3"]);
}

@end

Попробуем получить доступ к невидимым переменным.

Файл: main.m

#import "Cow.h"
#import <Foundation/Foundation.h>
int main(int argc, char *argv[]) {
    @autoreleasepool {
        Cow *cow = [Cow new];
        [cow performSelector:@selector(blindAccess)];
    }
}

Это печатает

iMadeVisible 
iMadeVisible2 
iMadeVisible3

Обратите внимание, что мне удалось получить доступ к резервному ivar, _iNotVisible2который является частным для подкласса. В Objective-C можно читать или устанавливать все переменные, даже отмеченные @private, без исключений.

Я не включил связанные объекты или переменные C, так как это разные птицы. Что касается переменных C, любая переменная определена вне @interface X{}или @implementation X{}является переменной C с областью файла и статическим хранилищем.

Я не обсуждал атрибуты управления памятью или атрибуты только для чтения / чтения / записи, получения и установки.

Яно
источник