Зачем тебе использовать ивар? - PullRequest
149 голосов
/ 01 февраля 2012

Обычно я вижу этот вопрос, заданный другим способом, например Должен ли каждый ivar быть свойством? (и мне нравится ответ bbum на этот вопрос).

Я использую свойства почти исключительно в моем коде. Однако очень часто я работаю с подрядчиком, который долгое время разрабатывал для iOS и являлся традиционным программистом игр. Он пишет код, который практически не объявляет свойства и опирается на ivars. Я предполагаю, что он делает это, потому что: 1.) он привык к этому, поскольку свойства не всегда существовали до Objective C 2.0 (октябрь '07) и 2.) для минимального прироста производительности без прохождения через геттер / сеттер.

Хотя он пишет код, который не пропускает, я все же предпочел бы, чтобы он использовал свойства вместо ivars. Мы говорили об этом, и он более или менее не видит причин использовать свойства, так как мы не использовали KVO, и у него есть опыт решения проблем с памятью.

Мой вопрос больше ... Почему вы хотите использовать период ивара - опытный или нет. Неужели разница в производительности настолько велика, что использование ивара было бы оправданным?

Также в качестве пояснения я перезаписываю сеттеры и геттеры по мере необходимости и использую ивар, который коррелирует с этим свойством внутри геттера / сеттера. Однако вне геттера / сеттера или инициализации я всегда использую синтаксис self.myProperty.


Редактировать 1

Я ценю все хорошие ответы. Единственное, на что я хотел бы обратить внимание, - это то, что с помощью ивара вы получаете инкапсуляцию, а с свойством - нет. Просто определите свойство в продолжении класса. Это скроет собственность от посторонних. Вы также можете объявить свойство только для чтения в интерфейсе и переопределить его как readwrite в реализации, например:

// readonly for outsiders
@property (nonatomic, copy, readonly) NSString * name;

и иметь в своем классе продолжение:

// readwrite within this file
@property (nonatomic, copy) NSString * name;

Чтобы он был полностью "закрытым", объявите его только в продолжении класса.

Ответы [ 7 ]

98 голосов
/ 01 февраля 2012

Инкапсуляция

Если ivar является приватным, другие части программы не могут получить его так легко. С объявленным свойством умные люди могут легко получить доступ и видоизменяться через средства доступа.

Performance

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

Нетривиальные типы

Пример: если у вас тип C ++, прямой доступ иногда является лучшим подходом. Тип может быть недоступен для копирования или копировать не просто.

Многопоточность

Многие из ваших иваров зависят от кода. Вы должны обеспечить целостность данных в многопоточном контексте. Таким образом, вы можете предпочесть прямой доступ к нескольким участникам в критических разделах. Если вы придерживаетесь методов доступа к зависимым от кода данным, ваши блокировки обычно должны быть реентерабельными, и вы часто будете совершать гораздо больше приобретений (значительно больше в разы).

Корректность программы

Поскольку подклассы могут переопределять любой метод, вы можете со временем увидеть семантическое различие между записью в интерфейс и соответствующим управлением своим состоянием. Прямой доступ для корректности программы особенно распространен в частично сконструированных состояниях - в ваших инициализаторах и в dealloc лучше всего использовать прямой доступ. Вы также можете найти это общее в реализациях средства доступа, удобного конструктора, copy, mutableCopy и реализации архивирования / сериализации.

Это также происходит чаще, когда вы переходите с на все, что имеет общедоступный менталитет readwrite, на тот, который хорошо скрывает детали реализации / данные. Иногда вам нужно правильно обходить побочные эффекты, которые может внести переопределение подкласса, чтобы делать правильные вещи.

Двоичный размер

Объявление всего readwrite по умолчанию обычно приводит ко многим методам доступа, которые вам никогда не понадобятся, если вы задумаетесь о выполнении вашей программы на мгновение. Так что это добавит немного жира в вашу программу и время загрузки.

Минимизирует сложность

В некоторых случаях просто нет необходимости добавлять + type + для поддержки всех этих дополнительных лесов для простой переменной, такой как private bool, которая записывается в одном методе и читается в другом.


Нельзя сказать, что использование свойств или методов доступа - это плохо - у каждого есть важные преимущества и ограничения. Как и многие ОО-языки и подходы к проектированию, вы также должны отдавать предпочтение методам доступа с соответствующей видимостью в ObjC. Будут времена, когда вам нужно отклоняться. По этой причине, я думаю, что часто лучше ограничить прямой доступ к реализации, которая объявляет ivar (например, объявляет его @private).


re Edit 1:

Большинство из нас запомнили, как динамически вызывать скрытый метод доступа (если мы знаем имя ...). Между тем, большинство из нас не запомнили, как правильно обращаться к иварам, которые не видны (за пределами KVC). Продолжение класса помогает , но создает уязвимости.

Этот обходной путь очевиден:

if ([obj respondsToSelector:(@selector(setName:)])
  [(id)obj setName:@"Al Paca"];

Теперь попробуйте это только с использованием ивара и без KVC.

73 голосов
/ 11 июля 2012

Для меня это обычно производительность.Доступ к ивару объекта такой же быстрый, как и доступ к элементу структуры в C с использованием указателя на память, содержащую такую ​​структуру.Фактически, объекты Objective-C в основном являются структурами C, расположенными в динамически распределенной памяти.Обычно это так быстро, как может получить ваш код, даже не оптимизированный вручную код сборки не может быть быстрее, чем это.

Доступ к ивару через метод получения / настройки включает вызов метода Objective-C, который намного медленнее (по крайней мере, в 3-4 раза), чем «нормальный» вызов функции C, и даже обычный вызов функции C будетуже будет в несколько раз медленнее, чем доступ к члену структуры.В зависимости от атрибутов вашего свойства, реализация метода set / getter, сгенерированная компилятором, может включать в себя другой вызов функции C для функций objc_getProperty / objc_setProperty, так как они должны будут retain / copy / autoreleaseобъекты по мере необходимости и далее выполняют спин-блокировки для атомных свойств, где это необходимо.Это может легко стать очень дорогим, и я не говорю о том, чтобы быть на 50% медленнее.

Давайте попробуем это:

CFAbsoluteTime cft;
unsigned const kRuns = 1000 * 1000 * 1000;

cft = CFAbsoluteTimeGetCurrent();
for (unsigned i = 0; i < kRuns; i++) {
    testIVar = i;
}
cft = CFAbsoluteTimeGetCurrent() - cft;
NSLog(@"1: %.1f picoseconds/run", (cft * 10000000000.0) / kRuns);

cft = CFAbsoluteTimeGetCurrent();
for (unsigned i = 0; i < kRuns; i++) {
    [self setTestIVar:i];
}
cft = CFAbsoluteTimeGetCurrent() - cft;
NSLog(@"2: %.1f picoseconds/run", (cft * 10000000000.0) / kRuns);

Вывод:

1: 23.0 picoseconds/run
2: 98.4 picoseconds/run

ЭтоВ 4,28 раза медленнее, и это был неатомарный примитив int, в значительной степени лучший случай ;большинство других случаев еще хуже (попробуйте атомарное свойство NSString *!).Таким образом, если вы согласны с тем фактом, что каждый доступ к ivar-файлам в 4-5 раз медленнее, чем он мог бы быть, использование свойств - это нормально (по крайней мере, когда речь идет о производительности), однако, существует множество ситуаций, когда такое снижение производительностисовершенно неприемлемо.

Обновление 2015-10-20

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

Приведенный ниже код определяет Account объекты.У учетной записи есть свойства, которые описывают имя (NSString *), пол (enum) и возраст (unsigned) его владельца, а также баланс (int64_t).Объект учетной записи имеет метод init и метод compare:.Метод compare: определяется следующим образом: женские порядки перед мужскими, имена в алфавитном порядке, молодые порядки перед старыми, балансные порядки от низкого до высокого.

На самом деле существует два класса счетов: AccountA и AccountB.Если вы посмотрите на их реализацию, вы заметите, что они почти полностью идентичны, за одним исключением: метод compare:.AccountA объекты получают доступ к их собственным свойствам методом (getter), в то время как AccountB объекты получают доступ к их собственным свойствам ivar.Это действительно единственная разница!Они оба обращаются к свойствам другого объекта для сравнения с помощью получателя (доступ к нему с помощью ivar не будет безопасным! Что, если другой объект является подклассом и переопределил получатель?).Также обратите внимание, что доступ к вашим собственным свойствам с использованием ivars не нарушает инкапсуляцию (ivars все еще не доступны).

Настройка теста действительно проста: создайте случайные учетные записи 1 Mio, добавьте ихмассив и сортировать этот массив.Вот и все.Конечно, есть два массива, один для AccountA объектов и один для AccountB объектов, и оба массива заполнены одинаковыми учетными записями (один и тот же источник данных).Время, необходимое для сортировки массивов.

Вот результат нескольких запусков, которые я сделал вчера:

runTime 1: 4.827070, 5.002070, 5.014527, 5.019014, 5.123039
runTime 2: 3.835088, 3.804666, 3.792654, 3.796857, 3.871076

Как видите, сортировка массива объектов AccountB всегда значительно быстрее , чем сортировка массива AccountA объектов.

Кто бы ни утверждал, что различия во времени выполнения до 1,32 секунды не имеют значения, лучше никогда не заниматься программированием пользовательского интерфейса.Например, если я хочу изменить порядок сортировки большой таблицы, такие различия во времени имеют огромное значение для пользователя (разница между приемлемым и вялым пользовательским интерфейсом).

Также в этом случае пример кода - единственная реальная работа, выполняемая здесь, но как часто ваш код - это всего лишь маленький механизм сложного часового механизма?И если каждая передача замедляет весь процесс, как это, что это означает для скорости всего часового механизма в конце?Особенно, если один рабочий шаг зависит от результата другого, что означает, что все неэффективности будут суммироваться.Большинство неэффективностей сами по себе не являются проблемой, их сумма становится проблемой для всего процесса.И такая проблема - это не то, что профилировщик легко покажет, потому что профилировщик занимается поиском критических горячих точек, но ни одна из этих неэффективностей не является горячими точками сама по себе.Процессорное время распределяется между ними в среднем, но у каждого из них есть лишь небольшая доля, и оптимизация кажется пустой тратой времени.И это правда, оптимизация только одного из них абсолютно ничего не поможет, оптимизация всех из них может помочь кардинально.

И даже если вы не думаете с точки зрения процессорного времени, потому что вы полагаете, что потеря процессорного времени - это полностьюприемлемо, в конце концов "это бесплатно", тогда как насчет затрат на хостинг сервера, вызванных энергопотреблением?Как насчет времени автономной работы мобильных устройств?Если вы напишете одно и то же мобильное приложение дважды (например, собственный мобильный веб-браузер), то однажды версия, в которой все классы будут получать доступ к своим свойствам только через геттеры, и однажды, когда все классы будут обращаться к ним только через ivars, использование первого из них постоянно истощаетбатарея намного быстрее, чем при использовании второго, даже если они функционально эквивалентны, и пользователю, возможно, даже будет казаться, что он немного быстрее.

Теперь вот код для вашего файла main.m (код зависит от того, включен ли ARC, и обязательно используйте оптимизацию при компиляции, чтобы увидеть полный эффект):

#import <Foundation/Foundation.h>

typedef NS_ENUM(int, Gender) {
    GenderMale,
    GenderFemale
};


@interface AccountA : NSObject
    @property (nonatomic) unsigned age;
    @property (nonatomic) Gender gender;
    @property (nonatomic) int64_t balance;
    @property (nonatomic,nonnull,copy) NSString * name;

    - (NSComparisonResult)compare:(nonnull AccountA *const)account;

    - (nonnull instancetype)initWithName:(nonnull NSString *const)name
        age:(const unsigned)age gender:(const Gender)gender
        balance:(const int64_t)balance;
@end


@interface AccountB : NSObject
    @property (nonatomic) unsigned age;
    @property (nonatomic) Gender gender;
    @property (nonatomic) int64_t balance;
    @property (nonatomic,nonnull,copy) NSString * name;

    - (NSComparisonResult)compare:(nonnull AccountB *const)account;

    - (nonnull instancetype)initWithName:(nonnull NSString *const)name
        age:(const unsigned)age gender:(const Gender)gender
        balance:(const int64_t)balance;
@end


static
NSMutableArray * allAcocuntsA;

static
NSMutableArray * allAccountsB;

static
int64_t getRandom ( const uint64_t min, const uint64_t max ) {
    assert(min <= max);
    uint64_t rnd = arc4random(); // arc4random() returns a 32 bit value only
    rnd = (rnd << 32) | arc4random();
    rnd = rnd % ((max + 1) - min); // Trim it to range
    return (rnd + min); // Lift it up to min value
}

static
void createAccounts ( const NSUInteger ammount ) {
    NSArray *const maleNames = @[
        @"Noah", @"Liam", @"Mason", @"Jacob", @"William",
        @"Ethan", @"Michael", @"Alexander", @"James", @"Daniel"
    ];
    NSArray *const femaleNames = @[
        @"Emma", @"Olivia", @"Sophia", @"Isabella", @"Ava",
        @"Mia", @"Emily", @"Abigail", @"Madison", @"Charlotte"
    ];
    const NSUInteger nameCount = maleNames.count;
    assert(maleNames.count == femaleNames.count); // Better be safe than sorry

    allAcocuntsA = [NSMutableArray arrayWithCapacity:ammount];
    allAccountsB = [NSMutableArray arrayWithCapacity:ammount];

    for (uint64_t i = 0; i < ammount; i++) {
        const Gender g = (getRandom(0, 1) == 0 ? GenderMale : GenderFemale);
        const unsigned age = (unsigned)getRandom(18, 120);
        const int64_t balance = (int64_t)getRandom(0, 200000000) - 100000000;

        NSArray *const nameArray = (g == GenderMale ? maleNames : femaleNames);
        const NSUInteger nameIndex = (NSUInteger)getRandom(0, nameCount - 1);
        NSString *const name = nameArray[nameIndex];

        AccountA *const accountA = [[AccountA alloc]
            initWithName:name age:age gender:g balance:balance
        ];
        AccountB *const accountB = [[AccountB alloc]
            initWithName:name age:age gender:g balance:balance
        ];

        [allAcocuntsA addObject:accountA];
        [allAccountsB addObject:accountB];
    }
}


int main(int argc, const char * argv[]) {
    @autoreleasepool {
        @autoreleasepool {
            NSUInteger ammount = 1000000; // 1 Million;
            if (argc > 1) {
                unsigned long long temp = 0;
                if (1 == sscanf(argv[1], "%llu", &temp)) {
                    // NSUIntegerMax may just be UINT32_MAX!
                    ammount = (NSUInteger)MIN(temp, NSUIntegerMax);
                }
            }
            createAccounts(ammount);
        }

        // Sort A and take time
        const CFAbsoluteTime startTime1 = CFAbsoluteTimeGetCurrent();
        @autoreleasepool {
            [allAcocuntsA sortedArrayUsingSelector:@selector(compare:)];
        }
        const CFAbsoluteTime runTime1 = CFAbsoluteTimeGetCurrent() - startTime1;

        // Sort B and take time
        const CFAbsoluteTime startTime2 = CFAbsoluteTimeGetCurrent();
        @autoreleasepool {
            [allAccountsB sortedArrayUsingSelector:@selector(compare:)];
        }
        const CFAbsoluteTime runTime2 = CFAbsoluteTimeGetCurrent() - startTime2;

        NSLog(@"runTime 1: %f", runTime1);
        NSLog(@"runTime 2: %f", runTime2);
    }
    return 0;
}



@implementation AccountA
    - (NSComparisonResult)compare:(nonnull AccountA *const)account {
        // Sort by gender first! Females prior to males.
        if (self.gender != account.gender) {
            if (self.gender == GenderFemale) return NSOrderedAscending;
            return NSOrderedDescending;
        }

        // Otherwise sort by name
        if (![self.name isEqualToString:account.name]) {
            return [self.name compare:account.name];
        }

        // Otherwise sort by age, young to old
        if (self.age != account.age) {
            if (self.age < account.age) return NSOrderedAscending;
            return NSOrderedDescending;
        }

        // Last ressort, sort by balance, low to high
        if (self.balance != account.balance) {
            if (self.balance < account.balance) return NSOrderedAscending;
            return NSOrderedDescending;
        }

        // If we get here, the are really equal!
        return NSOrderedSame;
    }

    - (nonnull instancetype)initWithName:(nonnull NSString *const)name
        age:(const unsigned)age gender:(const Gender)gender
        balance:(const int64_t)balance
    {
        self = [super init];
        assert(self); // We promissed to never return nil!

        _age = age;
        _gender = gender;
        _balance = balance;
        _name = [name copy];

        return self;
    }
@end


@implementation AccountB
    - (NSComparisonResult)compare:(nonnull AccountA *const)account {
        // Sort by gender first! Females prior to males.
        if (_gender != account.gender) {
            if (_gender == GenderFemale) return NSOrderedAscending;
            return NSOrderedDescending;
        }

        // Otherwise sort by name
        if (![_name isEqualToString:account.name]) {
            return [_name compare:account.name];
        }

        // Otherwise sort by age, young to old
        if (_age != account.age) {
            if (_age < account.age) return NSOrderedAscending;
            return NSOrderedDescending;
        }

        // Last ressort, sort by balance, low to high
        if (_balance != account.balance) {
            if (_balance < account.balance) return NSOrderedAscending;
            return NSOrderedDescending;
        }

        // If we get here, the are really equal!
        return NSOrderedSame;
    }

    - (nonnull instancetype)initWithName:(nonnull NSString *const)name
        age:(const unsigned)age gender:(const Gender)gender
        balance:(const int64_t)balance
    {
        self = [super init];
        assert(self); // We promissed to never return nil!

        _age = age;
        _gender = gender;
        _balance = balance;
        _name = [name copy];

        return self;
    }
@end
9 голосов
/ 21 мая 2013

Семантика

  • Что @property может выразить то, что ивы не могут: nonatomic и copy.
  • Что ивары могут выразить, что @property не может:

Производительность

Короткая история: ивары быстрее, но это не имеет значения для большинства применений. nonatomic свойства не используют блокировки, но прямой ivar быстрее, потому что он пропускает вызов accessors. Для получения подробной информации прочитайте следующую электронная почта от lists.apple.com.

Subject: Re: when do you use properties vs. ivars?
From: John McCall <email@hidden>
Date: Sun, 17 Mar 2013 15:10:46 -0700

Свойства влияют на производительность разными способами:

  1. Как уже обсуждалось, отправка сообщения для загрузки / сохранения выполняется на медленнее, чем просто выполнение загрузки / сохранения в строке .

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

  3. При отправке сообщения запись этого селектора принудительно сохраняется в кэше метода , и эта память обычно остается в памяти. D-кэш. Это увеличивает время запуска, увеличивает статическую память использование вашего приложения, и делает переключение контекста более болезненным. Поскольку Кэш метода специфичен для динамического класса объекта, это проблема увеличивается, чем больше вы используете КВО на нем.

  4. Отправка сообщения заставляет все значения в функции передаваться в стек (или храниться в регистрах сохранения вызовов, что просто означает разлив в другое время).

  5. Отправка сообщения может иметь произвольные побочные эффекты и, следовательно,

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

  6. В ARC, результат отправки сообщения всегда будет сохраняться либо вызываемым, либо вызывающим, даже для +0 возвратов: даже если метод не сохраняет / автоматически выпускает свой результат, вызывающий не знает это и должно попытаться принять меры, чтобы предотвратить получение результата autoreleased. Это никогда не может быть устранено, потому что сообщения отправляются не анализируется статически.

  7. В ARC, поскольку метод установки обычно принимает свой аргумент в +0, нет способа «передать» сохранение этого объекта (которое, как как обсуждалось выше, ARC обычно имеет) в ivar, поэтому значение как правило, должен быть сохранен / освобожден дважды .

Ничто из этого не означает, что они всегда плохие, конечно - есть много веских причин для использования свойств. Просто имейте в виду, что, как многие другие языковые функции, они не бесплатны.


Джон.

9 голосов
/ 01 февраля 2012

Наиболее важной причиной является концепция ООП сокрытия информации : если вы выставляете все через свойства и тем самым позволяете внешним объектам просматривать внутренние объекты другого объекта, то вы будете использовать эти внутренние и, таким образом, усложнятьизменение реализации.

Прирост "минимальной производительности" может быстро подвести итог и стать проблемой.Я знаю из опыта;Я работаю над приложением, которое действительно доводит iDevices до предела, и поэтому мы должны избегать ненужных вызовов методов (конечно, только там, где это возможно).Чтобы помочь в достижении этой цели, мы также избегаем точечного синтаксиса, поскольку с первого взгляда сложно увидеть количество вызовов методов: например, сколько вызовов методов вызывает выражение self.image.size.width?Напротив, вы можете сразу сказать с помощью [[self image] size].width.

Кроме того, при правильном присвоении имен ивару KVO возможен без свойств (IIRC, я не эксперт по KVO).

6 голосов
/ 01 февраля 2012

Свойства по сравнению с переменными экземпляра - это компромисс, в конце концов выбор зависит от приложения.

Инкапсуляция / скрытие информации Это хорошая вещь (TM) отПерспектива дизайна, узкие интерфейсы и минимальная связь - вот что делает программное обеспечение удобным и понятным.В Obj-C довольно сложно что-либо скрыть, но переменные экземпляра, объявленные в реализации , настолько близки, насколько это возможно.

Производительность Пока "преждевременноОптимизация "- это плохо (TM), писать плохо работающий код только потому, что вы можете, по крайней мере, плохо.Трудно утверждать, что вызов метода дороже, чем загрузка или хранилище, а в коде с интенсивными вычислениями стоимость скоро возрастает.

В статическом языке со свойствами, такими как C #, вызовы сеттеров / геттеровчасто может быть оптимизирован компилятором.Однако Obj-C динамичен, и удаление таких вызовов намного сложнее.

Абстракция Аргументом против переменных экземпляра в Obj-C традиционно является управление памятью.Поскольку переменные экземпляра MRC требуют, чтобы вызовы сохраняли / освобождали / авто-релиз распространялись по всему коду, свойства (синтезированные или нет) хранят код MRC в одном месте - принцип абстракции, который является хорошей вещью (TM).Однако с GC или ARC этот аргумент пропадает, поэтому абстракция для управления памятью больше не является аргументом против переменных экземпляра.

5 голосов
/ 01 февраля 2012

Обратная совместимость был фактором для меня.Я не мог использовать какие-либо функции Objective C 2.0, потому что я разрабатывал программное обеспечение и драйверы принтеров, которые должны были работать на Mac OS X 10.3 как часть требования.Я знаю, что ваш вопрос, казалось, был нацелен на iOS, но я решил поделиться причинами, по которым я не использовал свойства.

5 голосов
/ 01 февраля 2012

Свойства предоставляют ваши переменные другим классам.Если вам просто нужна переменная, относящаяся только к классу, который вы создаете, используйте переменную экземпляра.Вот небольшой пример: классы XML для синтаксического анализа RSS и тому подобное циклически перебирают кучу методов-делегатов и тому подобное.Практически иметь экземпляр NSMutableString для хранения результата каждого прохода анализа.Нет причины, по которой внешний класс должен был бы когда-либо обращаться к этой строке или манипулировать ею.Итак, вы просто объявляете это в заголовке или в частном порядке и получаете доступ к нему по всему классу.Установка свойства для него может быть полезна только для того, чтобы убедиться в отсутствии проблем с памятью, используя self.mutableString для вызова метода получения / установки.

...