Цель C - Пользовательское представление и реализация метода init?

У меня есть пользовательское представление, которое я хочу иметь возможность инициализировать оба in-code И в nib,

Как правильно написать оба initWithFrame а также initWithCoder методы? Они оба разделяют блок кода, который используется для некоторой инициализации.

3 ответа

Решение

В этом случае правильно создать другой метод, содержащий код, общий для обоих -initWithFrame: а также -initWithCoder:и затем вызовите этот метод из обоих -initWithFrame: а также -initWithCoder::

- (void)commonInit
{
    // do any initialization that's common to both -initWithFrame:
    // and -initWithCoder: in this method
}

- (id)initWithFrame:(CGRect)aRect
{
    if ((self = [super initWithFrame:aRect])) {
        [self commonInit];
    }
    return self;
}

- (id)initWithCoder:(NSCoder*)coder
{
    if ((self = [super initWithCoder:coder])) {
        [self commonInit];
    }
    return self;
}

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

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

для тривиальных случаев дублирование не является плохой стратегией:

- (id)initOne
{
    self = [super init];
    if (nil != self) { monIntIvar = SomeDefaultValue; }
    return self;
}

- (id)initTwo
{
    self = [super init];
    if (nil != self) { monIntIvar = SomeDefaultValue; }
    return self;
}

для нетривиальных случаев я рекомендую статическую функцию инициализации, которая принимает общий вид:

// MONView.h

@interface MONView : UIView
{
    MONIvar * ivar;
}

@end

// MONView.m

static inline bool InitMONView(MONIvar** ivar) {
    *ivar = [MONIvar new];
    return nil != *ivar;
}

@implementation MONView

- (id)initWithFrame:(CGRect)frame
{
    self = [super initWithFrame:frame];
    if (nil != self) {
        if (!InitMONView(&ivar)) {
            [self release];
            return nil;
        }
    }
    return self;
}

- (id)initWithCoder:(NSCoder *)coder
{
    self = [super initWithCoder:coder];
    if (nil != self) {
        if (!InitMONView(&ivar)) {
            [self release];
            return nil;
        }
    }
    return self;
}

// …

@end

Objective-C++:

если вы используете objC++, то вы можете просто реализовать подходящие конструкторы по умолчанию для своих иваров C++ и опустить большую часть инициализации и скаффолдинга dealloc (при условии, что вы правильно включили флаги компилятора).

Обновить:

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

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

Рекомендация: вызывать переопределенные методы экземпляра для частично созданного объекта (например, во время инициализации и dealloc) небезопасно и его следует избегать. аксессоры особенно плохи. В других языках это ошибка программиста (например, UB). Просмотрите документацию objc на эту тему (ссылка: "Реализация инициализатора"). Я считаю это обязательным, но я все еще знаю людей, которые настаивают на том, что методы экземпляров и методы доступа лучше в частично сконструированных состояниях, потому что это "обычно работает для них".

Требование: Соблюдайте график наследования. инициализировать от базы вверх. уничтожить сверху вниз. всегда.

Рекомендация: сохраняйте инициализацию согласованной для всех. если ваша база возвращает что-то из init, вы должны предполагать, что все хорошо. не вводите хрупкий танец инициализации для ваших клиентов и подклассов для реализации (это может вернуться как ошибка). вам нужно знать, есть ли у вас действительный экземпляр. Кроме того, субклассеры (по праву) предполагают, что ваша база правильно инициализирована, когда вы возвращаете объект из назначенного инициализатора. Вы можете уменьшить вероятность этого, сделав ивары базового класса приватными. как только вы возвращаетесь из init, клиенты / подклассы предполагают, что объект, из которого они получены, может использоваться и правильно инициализироваться. По мере роста графов классов ситуация становится очень сложной, и ошибки начинают появляться.

Рекомендация: проверьте ошибки в init. также сохраняйте обработку ошибок и обнаружение последовательными. возврат nil - очевидное соглашение, чтобы определить, была ли ошибка во время инициализации. обнаружить это рано.

Хорошо, а как насчет метода общего экземпляра?

exmaple заимствовано и изменено из другого поста:

@implementation MONDragon

- (void)commonInit
{
    ivar = [MONIvar new];
}

- (id)initWithFrame:(CGRect)aRect
{
        if ((self = [super initWithFrame:aRect])) {
                [self commonInit];
        }
        return self;
}

- (id)initWithCoder:(NSCoder*)coder
{
        if ((self = [super initWithCoder:coder])) {
                [self commonInit];
        }
        return self;
}

// …

(кстати, нет обработки ошибок в этом примере)

Caleb: самая большая "опасность", которую я вижу в приведенном выше коде, заключается в том, что кто-то может создать подкласс рассматриваемого класса, переопределить -commonInit и потенциально инициализировать объект дважды.

в частности, подкласс -[MONDragon commonInit] будет вызываться дважды (утечка ресурсов, поскольку они будут созданы дважды), и инициализатор базы и обработка ошибок не будут выполняться.

Калеб: Если это реальный риск...

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

Калеб:… самый простой способ справиться с этим - это сохранить -commonInit в секрете и / или задокументировать его как нечто, что нельзя переопределить

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

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

если кто-то настаивает на использовании метода экземпляра, соглашение, которое вы резервируете, такие как -[MONDragon constructMONDragon] а также -[MONKomodo constructMONKomodo] может значительно уменьшить ошибку в большинстве случаев. инициализатор, вероятно, будет виден только для TU реализации класса, поэтому компилятор может пометить некоторые из наших потенциальных ошибок.

примечание стороны: общий конструктор объекта, такой как:

- (void)commonInit
{
    [super commonInit];
    // init this instance here
}

(что я также видел) еще хуже, потому что это ограничивает инициализацию, удаляет контекст (например, параметры), и вы все равно в конечном итоге получаете людей, смешивающих свой код инициализации между классами между назначенным инициализатором и -commonInit,

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

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

Если они совместно используют код, просто попросите их вызвать третий метод инициализации.

Например, initWithFrame может выглядеть примерно так:

- (id)initWithFrame:(CGRect)frame {
    if ((self = [super initWithFrame:frame])) {
        [self doMyInitStuff];
    }

    return self;
}

Обратите внимание, что если вы используете OS X (в отличие от iOS), кадр будет NSRect вместо CGRect,

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

- (id)initWithFrame:(CGRect)frame {
    if ((self = [super initWithFrame:frame])) {
        if (![self doMyInitStuff]) {
            [self release];
            self = nil;
        }
    }

    return self;
}

Это предполагает doMyInitStuff метод возвращает NO по ошибке.

Кроме того, если вы еще не посмотрели, есть немного документации по инициализации, которая может быть полезна для вас (хотя она не имеет прямого отношения к этому вопросу):

Рекомендации по кодированию для какао: советы и методы для разработчиков фреймворков

Другие вопросы по тегам