Зачем тебе использовать ивар?
Я обычно вижу этот вопрос, заданный по-другому, например, должен ли каждый ивар быть собственностью? (и мне нравится ответ bbum на этот вопрос).
Я использую свойства почти исключительно в моем коде. Однако очень часто я работаю с подрядчиком, который долгое время разрабатывал для iOS и являлся традиционным программистом игр. Он пишет код, который практически не объявляет свойства и опирается на ivars. Я предполагаю, что он делает это, потому что 1.) он привык к этому, поскольку свойства не всегда существовали до Objective C 2.0 (октябрь '07) и 2.) для минимального прироста производительности, не проходя через геттер / сеттер.
Хотя он пишет код, который не пропускает, я бы предпочел, чтобы он использовал свойства вместо ivars. Мы говорили об этом, и он более или менее не видит причин использовать свойства, так как мы не использовали KVO, и у него есть опыт решения проблем с памятью.
Мой вопрос больше... Почему вы хотите использовать период ивара - опытный или нет. Неужели разница в производительности настолько велика, что использование ивара было бы оправданным?
Также в качестве пояснения я перезаписываю сеттеры и геттеры по мере необходимости и использую ивар, который соотносится с этим свойством внутри геттера / сеттера. Тем не менее, за пределами getter / setter или init, я всегда использую self.myProperty
синтаксис.
Редактировать 1
Я ценю все хорошие ответы. Единственное, на что я хотел бы обратить внимание, - это то, что с помощью ивара вы получаете инкапсуляцию, а с свойством - нет. Просто определите свойство в продолжении класса. Это скроет собственность от посторонних. Вы также можете объявить свойство только для чтения в интерфейсе и переопределить его как readwrite в реализации, например:
// readonly for outsiders
@property (nonatomic, copy, readonly) NSString * name;
и иметь в классе продолжение:
// readwrite within this file
@property (nonatomic, copy) NSString * name;
Чтобы он был полностью "приватным", объявите его только в продолжении класса.
7 ответов
Инкапсуляция
Если ivar является приватным, другие части программы не могут получить его так легко. С объявленной собственностью умные люди могут легко получить доступ и видоизменяться через средства доступа.
Спектакль
Да, это может иметь значение в некоторых случаях. Некоторые программы имеют ограничения, при которых они не могут использовать какой-либо объектный обмен сообщениями в определенных частях программы (думайте в реальном времени). В других случаях вы можете получить доступ к нему напрямую для скорости. В других случаях это происходит потому, что обмен сообщениями objc действует как межсетевой экран оптимизации. Наконец, это может уменьшить количество операций по подсчету ссылок и минимизировать пиковое использование памяти (если все сделано правильно).
Нетривиальные типы
Пример: если у вас тип C++, прямой доступ иногда является лучшим подходом. Тип может не быть копируемым, или копировать может быть нетривиальным.
Многопоточность
Многие из ваших иваров зависят от кода. Вы должны обеспечить целостность данных в многопоточном контексте. Таким образом, вы можете предпочесть прямой доступ к нескольким участникам в критических разделах. Если вы придерживаетесь аксессоров для зависимых от кода данных, ваши блокировки обычно должны быть реентерабельными, и вы часто будете делать гораздо больше приобретений (значительно больше в разы).
Корректность программы
Поскольку подклассы могут переопределять любой метод, вы можете со временем увидеть семантическое различие между записью в интерфейс и соответствующим управлением своим состоянием. Прямой доступ к корректности программы особенно распространен в частично построенных состояниях - в ваших инициализаторах и в dealloc
Лучше всего использовать прямой доступ. Вы также можете найти это общее в реализациях средства доступа, удобного конструктора, copy
, mutableCopy
и реализации архивирования / сериализации.
Это также происходит чаще, так как человек переходит от общедоступного мышления аксессора readwrite к тому, что хорошо скрывает детали реализации / данные. Иногда вам нужно правильно обходить побочные эффекты, которые может внести переопределение подкласса, чтобы сделать правильные вещи.
Бинарный размер
Объявление всего readwrite по умолчанию обычно приводит ко многим методам доступа, которые вам никогда не понадобятся, если вы задумаетесь о выполнении вашей программы на мгновение. Так что это добавит немного жира в вашу программу и время загрузки.
Минимизирует Сложность
В некоторых случаях просто нет необходимости добавлять + type + для поддержки всех этих дополнительных скаффолдингов для простой переменной, такой как private bool, которая записывается в одном методе и читается в другом.
Нельзя сказать, что использование свойств или методов доступа - это плохо - у каждого есть важные преимущества и ограничения. Как и многие ОО-языки и подходы к проектированию, вы также должны отдавать предпочтение методам доступа с соответствующей видимостью в ObjC. Будут времена, когда вам нужно отклоняться. По этой причине, я думаю, что часто лучше ограничить прямой доступ к реализации, которая объявляет ivar (например, объявить его @private
).
повторно редактировать 1:
Большинство из нас запомнили, как динамически вызывать скрытый метод доступа (если мы знаем имя…). Между тем, большинство из нас не запомнили, как правильно обращаться к иварам, которые не видны (за пределами KVC). Продолжение класса помогает, но оно создает уязвимости.
Этот обходной путь очевиден:
if ([obj respondsToSelector:(@selector(setName:)])
[(id)obj setName:@"Al Paca"];
Теперь попробуйте это только с помощью ивара и без KVC.
Для меня это обычно производительность. Доступ к ивару объекта так же быстр, как доступ к члену структуры в 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
Семантика
- Какие
@property
могу выразить, что ивар не может:nonatomic
а такжеcopy
, - Какие ивары могут выразить это
@property
не может:@protected
: public на подклассах, private снаружи.@package
: общедоступный на фреймворках на 64 бита, приватный снаружи. Такой же как@public
на 32 бита. См. 64-битный контроль доступа к классам и экземплярам Apple.- Отборочные. Например, массивы ссылок на сильный объект:
id __strong *_objs
,
Спектакль
Короткая история: ивары быстрее, но это не имеет значения для большинства применений. 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
Свойства влияют на производительность во многих отношениях:
Как уже обсуждалось, отправка сообщения для загрузки / сохранения выполняется медленнее, чем просто встроенная загрузка / сохранение.
Отправка сообщения для загрузки / сохранения - это также немного больше кода, который нужно хранить в i-cache: даже если получатель / установщик добавил ноль дополнительных инструкций, помимо загрузки / сохранения, это будет сплошная половина -десятки дополнительных инструкций в вызывающей программе, чтобы настроить отправку сообщения и обработать результат.
Отправка сообщения заставляет запись для этого селектора оставаться в кеше метода, и эта память обычно остается в d-кеше. Это увеличивает время запуска, увеличивает использование статической памяти вашего приложения и делает переключение контекста более болезненным. Поскольку кэш метода специфичен для динамического класса объекта, эта проблема возрастает по мере того, как вы используете для него KVO.
Отправка сообщения заставляет все значения в функции передаваться в стек (или храниться в регистрах сохранения вызываемого абонента, что просто означает проливание в другое время).
Отправка сообщения может иметь произвольные побочные эффекты и, следовательно,
- заставляет компилятор сбросить все свои предположения о нелокальной памяти
- не может быть поднят, затоплен, переупорядочен, объединен или ликвидирован.
В ARC результат отправки сообщения всегда будет сохраняться либо вызываемым, либо вызывающим, даже для +0 возвратов: даже если метод не сохраняет / автоматически высвобождает свой результат, вызывающий не знает этого и имеет попытаться предпринять действия, чтобы предотвратить автоматическое освобождение результата. Это никогда не может быть устранено, потому что отправка сообщений не подвергается статическому анализу.
В ARC, поскольку метод установки обычно принимает свой аргумент в +0, нет никакого способа "передать" сохранение этого объекта (которое, как обсуждалось выше, обычно имеет ARC) в ivar, поэтому значение обычно должно получить сохранить / освободить дважды.
Ничто из этого не означает, что они всегда плохие, конечно - есть много веских причин для использования свойств. Просто помните, что, как и многие другие языковые функции, они не бесплатны.
Джон.
Наиболее важной причиной является концепция сокрытия информации ООП: если вы выставляете все через свойства и тем самым позволяете внешним объектам просматривать внутренние объекты другого объекта, то вы будете использовать их и таким образом усложнять изменение реализации.
Прибыль "минимальной производительности" может быстро подвести итог и стать проблемой. Я знаю из опыта; Я работаю над приложением, которое действительно доводит iDevices до предела, и поэтому мы должны избегать ненужных вызовов методов (конечно, только там, где это возможно). Чтобы помочь в достижении этой цели, мы также избегаем точечного синтаксиса, так как на первый взгляд сложно увидеть количество вызовов методов: например, сколько вызовов методов делает выражение self.image.size.width
спусковой крючок? Напротив, вы можете сразу сказать с [[self image] size].width
,
Кроме того, при правильном присвоении имен ivar KVO возможно без свойств (IIRC, я не эксперт KVO).
Свойства по сравнению с переменными экземпляра - это компромисс, в конце концов выбор зависит от приложения.
Инкапсуляция / сокрытие информации Это хорошая вещь с точки зрения дизайна, узкие интерфейсы и минимальная связь - вот что делает программное обеспечение удобным и понятным. В Obj-C довольно сложно что-либо скрыть, но переменные экземпляра, объявленные в реализации, настолько близки, насколько это возможно.
Производительность Хотя "преждевременная оптимизация" - это плохая вещь, писать плохо работающий код только потому, что вы можете, по крайней мере так же плохо. Трудно поспорить, что вызов метода дороже, чем загрузка или хранение, а в коде, интенсивно использующем вычислительные ресурсы, стоимость скоро возрастает.
В статическом языке со свойствами, такими как C#, вызовы к установщикам / получателям часто могут быть оптимизированы компилятором. Однако Obj-C динамичен, и удаление таких вызовов намного сложнее.
Абстракция Аргументом против переменных экземпляра в Obj-C традиционно является управление памятью. Поскольку переменные экземпляра MRC требуют, чтобы вызовы сохраняли / освобождали / авто-релиз распространялись по всему коду, свойства (синтезированные или нет) хранят код MRC в одном месте - принцип абстракции, который является хорошей вещью (TM). Однако с GC или ARC этот аргумент пропадает, поэтому абстракция для управления памятью больше не является аргументом против переменных экземпляра.
Свойства выставляют ваши переменные другим классам. Если вам просто нужна переменная, относящаяся только к классу, который вы создаете, используйте переменную экземпляра. Вот небольшой пример: классы XML для синтаксического анализа RSS и тому подобное циклически перебирают кучу методов-делегатов и тому подобное. Целесообразно иметь экземпляр NSMutableString для хранения результата каждого прохода анализа. Нет причины, по которой внешний класс должен был бы когда-либо обращаться к этой строке или манипулировать ею. Итак, вы просто объявляете это в заголовке или в частном порядке и обращаетесь к нему по всему классу. Установка свойства для него может быть полезна только для того, чтобы убедиться в отсутствии проблем с памятью, используя self.mutableString для вызова метода получения / установки.
Обратная совместимость была фактором для меня. Я не мог использовать какие-либо функции Objective C 2.0, потому что я разрабатывал программное обеспечение и драйверы принтеров, которые должны были работать на Mac OS X 10.3 как часть требования. Я знаю, что ваш вопрос, казалось, был нацелен на iOS, но я решил поделиться причинами, по которым я не использовал свойства.