В Objective-C, почему я должен проверить, если self = [super init] не ноль?

165

У меня есть общий вопрос о написании методов инициализации в Objective-C.

Я вижу везде (код Apple, книги, открытый исходный код и т. Д.), Что метод init должен проверить, не равен ли self = [super init] ноль, прежде чем продолжить инициализацию.

Шаблон Apple по умолчанию для метода init:

- (id) init
{
    self = [super init];

    if (self != nil)
    {
        // your code here
    }

    return self;
}

Зачем?

Я имею в виду, когда init когда-нибудь вернет ноль? Если я вызвал init в NSObject и получил nil обратно, то что-то должно быть действительно испорчено, верно? И в этом случае вы могли бы даже не написать программу ...

Действительно ли так часто, что метод init класса может возвращать ноль? Если да, то в каком случае и почему?

Jasarien
источник
1
Уил Шипли недавно опубликовал статью, связанную с этим. [self = [глупый init];] ( wilshipley.com/blog/2005/07/self-stupid-init.html ) Прочитайте также комментарии, некоторые полезные вещи.
Райан Тауншенд
6
Вы могли бы спросить Уила Шипли или Майка Эша или Мэтта Галлахера . В любом случае, это что-то из обсуждаемой темы. Но обычно хорошо придерживаться идиом Apple: в конце концов, это их рамки.
Джбреннан
1
Похоже, Уил делал больше аргументов для того, чтобы не слепо переназначать себя во время init, зная, что [super init] может не вернуть получателя.
Jasarien
3
Уил изменил свои мысли с тех пор, как этот пост был изначально сделан.
bbum
5
Я видел этот вопрос некоторое время назад и просто нашел его снова. Отлично. +1
Дэн Розенстарк

Ответы:

53

Например:

[[NSData alloc] initWithContentsOfFile:@"this/path/doesn't/exist/"];
[[NSImage alloc] initWithContentsOfFile:@"unsupportedFormat.sjt"];
[NSImage imageNamed:@"AnImageThatIsntInTheImageCache"];

... и так далее. (Примечание: NSData может выдать исключение, если файл не существует). Существует довольно много областей, в которых возвращение nil - это ожидаемое поведение при возникновении проблемы, и из-за этого стандартная практика заключается в том, чтобы проверять nil почти все время, для обеспечения согласованности.

iKenndac
источник
10
Да, но это не ВНУТРИ метода init соответствующего класса. NSData наследуется от NSObject. NSData проверяет, возвращает ли [super init] ноль? Это то, что я спрашиваю здесь. Извините, если я не ясно ...
Jasarien
9
Если бы вы создали подклассы этих классов, вполне вероятно, что [super init] вернет nil. Не все классы являются прямыми подклассами NSObject. Там нет никакой гарантии, что он не вернет ноль. Это просто защитная практика кодирования, которая обычно поощряется.
Чак
7
Я сомневаюсь, что NSObject init может вернуть nil в любом случае, когда-либо. Если у вас недостаточно памяти, alloc не будет работать, но если это удастся, я сомневаюсь, что init может потерпеть неудачу - NSObject даже не имеет переменных экземпляра, кроме Class. В GNUStep он реализован как просто «вернуть себя», а разборка на Mac выглядит одинаково. Все это, конечно, неуместно - просто следуйте стандартным идиомам, и вам не придется беспокоиться о том, может это или нет.
Питер Н Льюис
9
Я не склонен к тому, чтобы не следовать лучшим практикам. Однако я хотел бы знать, почему они являются наилучшей практикой в ​​первую очередь. Это как если бы мне сказали прыгнуть с башни. Вы не просто идете вперед и делаете это, если не знаете почему. Есть ли большая мягкая подушка для приземления внизу с огромным денежным вознаграждением? Если бы я знал, что прыгнул бы. Если нет, я бы не стал. Я не хочу просто слепо следовать за практикой, не зная, почему я следую за ней ...
Jasarien
4
Если alloc возвращает nil, init отправляется в nil, что всегда приводит к nil, в результате чего self равно nil.
Т.
50

Эта идиома является стандартной, потому что она работает во всех случаях.

Хотя необычно, будут случаи, когда ...

[super init];

... возвращает другой экземпляр, поэтому требует присваивания себе.

И будут случаи, когда он вернет nil, поэтому потребуется проверка nil, чтобы ваш код не пытался инициализировать слот переменной экземпляра, который больше не существует.

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

bbum
источник
3
Это все еще верно в свете спецификаторов обнуляемости? Если инициализатор моего суперкласса не нулевой, то стоит ли проверять дополнительный беспорядок? (Хотя сам -initNSObject, кажется, не имеет ничего для своего афикта ...)
natevw
Есть ли когда-нибудь случай, когда [super init]возвращается ноль, когда прямой суперкласс NSObject? Разве это не тот случай, когда "все сломано?"
Дэн Розенстарк
1
@DanRosenstark Нет, если NSObjectэто прямой суперкласс. Но ... даже если вы объявите NSObjectпрямой суперкласс, что-то могло быть изменено во время выполнения так, что NSObjectего реализация initне то, что на самом деле называется.
Bumum
1
Большое спасибо @bbum, это действительно помогло мне сориентироваться в исправлении некоторых ошибок. Хорошо исключить некоторые вещи!
Дэн Розенстарк
26

Я думаю, что в большинстве классов, если возвращаемое значение из [super init] равно nil, и вы проверяете его, как рекомендовано стандартными методами, а затем возвращаете преждевременно, если nil, в основном ваше приложение все еще не будет работать правильно. Если вы думаете об этом, несмотря на то, что если (само! = Ноль) проверка есть, для правильной работы вашего класса, 99,99% времени вы на самом деле делаете потребность себя , чтобы быть не равна нулю. Теперь, предположим, по какой-то причине, [super init] действительно возвратил nil, в основном, ваша проверка против nil в основном передает баг до вызывающей стороны вашего класса, где он, вероятно, в любом случае потерпит неудачу, поскольку он, естественно, предположит, что вызов был успешный.

По сути, я нахожусь в том, что в 99,99% случаев if (self! = Nil) ничего не покупает с точки зрения большей надежности, поскольку вы просто перекладываете деньги на своего призывателя. Чтобы по-настоящему справиться с этим, вам нужно будет проверять всю иерархию вызовов. И даже в этом случае единственное, что он может купить, - это то, что ваше приложение не сможет работать более чисто / надежно. Но все равно не получится.

Если библиотечный класс произвольно решил вернуть nil в результате [super init], вы в любом случае в значительной степени ошеломлены, и это скорее указывает на то, что автор библиотечного класса допустил ошибку реализации.

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

Но для кода уровня C я все равно обычно проверял бы возвращаемое значение malloc () по нулевому указателю. Принимая во внимание, что для Objective-C до тех пор, пока я не найду доказательства обратного, я думаю, что обычно я пропускаю проверки if (self! = Nil). Почему расхождение?

Потому что на уровнях C и malloc в некоторых случаях вы можете частично восстановиться. В то время как я думаю, что в Objective-C, в 99,99% случаев, если [super init] действительно возвращает nil, вы в сущности офигены, даже если вы пытаетесь справиться с этим. Вы могли бы просто позволить приложению аварийно завершить работу и справиться с последствиями.

Джино
источник
6
Хорошо сказано. Я второй это.
Роджер К.С. Вернерссон
3
+1 Я полностью согласен. Небольшое замечание: я не верю, что шаблон является результатом времен, когда распределение чаще всего заканчивалось неудачей. Распределение обычно уже выполняется во время вызова init. Если не удалось выделить alloc, init даже не был бы вызван.
Николай Рюэ
8

Это своего рода краткое изложение комментариев выше.

Допустим, суперкласс возвращается nil. Что будет дальше?

Если вы не следуете конвенциям

Ваш код потерпит крах в середине вашего initметода. (если initне делает ничего значимого)

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

Ваш код, вероятно, потерпит крах в какой-то момент позже, потому что ваш экземпляр находится там nil, где вы ожидали чего-то другого. Или ваша программа будет работать неожиданно без сбоев. О, Боже! Вы хотите, чтобы это? Я не знаю...

Если вы будете следовать соглашениям, добровольно разрешив вашему подклассу вернуть ноль

Ваша документация по коду (!) Должна четко указывать: «возвращает ... или ноль», а остальная часть вашего кода должна быть подготовлена ​​для обработки этого. Теперь это имеет смысл.

user123444555621
источник
5
Интересно отметить, что вариант № 1 явно предпочтительнее варианта № 2. Если есть действительно обстоятельства, при которых вы можете захотеть, чтобы init вашего подкласса возвратил nil, тогда №3 предпочтительнее. Если единственная причина, которая когда-либо случится, это ошибка в вашем коде, тогда используйте # 1. Использование опции № 2 просто задерживает взрыв вашего приложения до более позднего момента времени, и, таким образом, делает вашу работу, когда вы приходите к отладке ошибки, намного сложнее. Это все равно что молча ловить исключения и продолжать без их обработки.
Марк Эмери
Или вы переключаетесь на swift и просто используете дополнительные функции
AndrewSB
7

Как правило, если ваш класс является производным от NSObjectвас, вам это не нужно. Тем не менее, это хорошая привычка, как если бы ваш класс был производным от других классов, их инициализаторы могут возвращаться nil, и если это так, ваш инициализатор может затем захватить это и вести себя правильно.

И да, для протокола: я следую лучшей практике и пишу ее на всех своих уроках, даже на тех, которые происходят непосредственно от них NSObject.

Джон Руди
источник
1
Имея это в виду, будет ли хорошей практикой проверять nil после инициализации переменной и перед вызовом для нее функций? напр.Foo *bar = [[Foo alloc] init]; if (bar) {[bar doStuff];}
dev_does_software
Наследование от NSObjectне гарантирует, что оно -initдает и вам NSObject, если учесть экзотические среды выполнения, такие как более ранние версии GNUstep в (которые возвращаются GSObject), независимо от того, что, чек и присвоение.
Maxthon Chan
3

Вы правы, вы часто можете просто написать [super init], но это не сработает для подкласса всего. Люди предпочитают просто запомнить одну стандартную строку кода и использовать ее все время, даже когда это только иногда необходимо, и, таким образом, мы получаем стандарт if (self = [super init]), который использует как возможность возврата nil, так и возможность объекта, отличного от selfвозврата в учетную запись.

andyvn22
источник
3

Распространенная ошибка - написать

self = [[super alloc] init];

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

self = [super init]; 

необходим, если у суперкласса есть члены (переменные или другие объекты), чтобы сначала инициализировать перед настройкой членов подклассов. В противном случае среда выполнения objc инициализирует их все в 0 или в ноль . (в отличие от ANSI C, который часто выделяет куски памяти, не очищая их вообще )

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

Крис Рид
источник
2

Это делается для проверки того, что инициализация сработала, оператор if возвращает true, если метод init не возвратил nil, поэтому это способ проверить создание объекта, сработавшего корректно. Несколько причин, по которым я могу думать о том, что init может потерпеть неудачу, может быть, это переопределенный метод init, о котором суперкласс не знает, или что-то в этом роде, хотя я не думаю, что это так часто встречается. Но если это случится, лучше ничего не случиться, если я получу аварию, поэтому всегда проверяю ...

Даниил
источник
да, но они вызываются вместе, что будет, если выделить файлы?
Даниэль
Я полагаю, что если не удается выполнить alloc, тогда init будет отправляться на nil, а не на экземпляр какого-либо класса, для которого вы вызываете init. В этом случае ничего не произойдет, и никакой код не будет выполнен, чтобы даже проверить, вернул ли [super init] nil.
Jasarien
2
Распределение памяти не всегда выполняется в + alloc. Рассмотрим случай кластеров классов; строка NSString не знает, какой конкретный подкласс использовать, пока не будет вызван инициализатор.
bbum
1

В OS X это не так вероятно для -[NSObject init]сбоя по причинам памяти. Этого нельзя сказать о iOS.

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

MaddTheSane
источник
2
На оба прошивке и Mac OS -[NSObject init]является очень вряд ли удастся из - за соображения памяти , как он не выделяет память.
Николай Рюхе
Я предполагаю, что он имел в виду alloc, а не init :)
Томас Темпельманн