Objective C - Custom view and implementing init method?

谁都会走 提交于 2019-11-26 17:58:19

问题


I have a custom view that I want to be able to initialize both in-code and in nib.

What's the correct way to write both initWithFrame and initWithCoder methods? They both share a block of code that is used for some initialization.


回答1:


The right thing to do in that case is to create another method containing the code that's common to both -initWithFrame: and -initWithCoder:, and then call that method from both -initWithFrame: and -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;
}

Do heed the concerns outlined in Justin's answer, particularly that any subclasses must not override -commonInit. I used that name here for its illustrative value, but you'll probably want one that's more closely tied to your class and less likely to be accidentally overridden. If you're creating a purpose-built UIView subclass that's unlikely to be subclassed itself, using a common initialization method as above is perfectly fine. If you're writing a framework for others to use, or if you don't understand the issue but want to do the safest possible thing, use a static function instead.




回答2:


the solution is not as simple as it initially appears. there are some dangers in initialization - more on those further down. for these reasons, i generally take one of the two following approaches in objc programs:

for trivial cases, duplication is not a bad strategy:

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

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

for nontrivial cases, i recommend a static initialization function which takes the general form:

// 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++:

if you're using objc++, then you can simply implement suitable default constructors for your c++ ivars and omit the majority of the initialization and dealloc scaffolding (assuming you have enabled the compiler flags correctly).

Update:

I am going to explain why this is the safe way to initialize an object and outline some reasons why typical implementations of the other answers are dangerous.

The typical problem with common initializers which call instance methods during initialization is that they abuse the inheritance graph, typically introduce complexity and bugs.

Recommendation: calling overridden instance methods on a partially constructed object (e.g. during initialization and dealloc) is unsafe and to be avoided. accessors are particularly bad. In other languages, this is a programmer's error (e.g. UB). Look through the objc docs on the subject (ref: "Implementing an Initializer"). I consider this a must, but I still know people who insist instance methods and accessors are better in partially constructed states because it "usually works for them".

Requirement: Respect the inheritence graph. initialize from base up. destroy from top down. always.

Recommendation: keep initialization consistent for all. if your base returns something from init, you should assume all's well. don't introduce a fragile initialization dance for your clients and subclasses to implement (it's likely to come back as a bug). you need to know if you have a hold on a valid instance. also, subclassers will (rightfully) assume that your base is properly initialized when you return an object from a designated initializer. you can reduce the probability of this by making ivars of the base class private. once you return from init, clients/subclasses assume that the object they derive from is usable and initialized properly. as class graphs grow, the situation becomes very complex and bugs begin to creep out.

Recommendation: check for errors in init. also keep error handling and detection consistent. returning nil is the obvious convention to determine if there's been an error during initialization. detect it early.

Ok, but what about a shared instance method?

exmaple borrowed and altered from another post:

@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;
}

// …

(btw, no error handling in that example)

Caleb: the greatest "danger" I see in the code above is that someone might create a subclass of the class in question, override -commonInit, and potentially initialize the object twice.

specifically, the subclass -[MONDragon commonInit] would be called twice (leaking resources as they would be created twice) and base's initializer and error handling would not be performed.

Caleb: If that's a real risk…

either effect can equate to an unreliable program. the problem is easily avoided by using conventional initialization.

Caleb: …the easiest way to deal with it is to keep -commonInit private and/or document it as something not to override

since the runtime does not distinguish visibility when messaging, this approach is dangerous because any subclass could easily declare the same private initialization method (see below).

documenting a method as something you should not override exposes burdens on subclassers and introduces complexities and problems which can be avoided easily - by using other approaches. it's also error prone since the compiler won't flag it.

if one insists on using an instance method, a convention which you reserve such as -[MONDragon constructMONDragon] and -[MONKomodo constructMONKomodo] could significantly reduce the error in the majority of cases. the initializer is likely to be visible only to the TU of the class implementation, so the compiler can flag some of our potential mistakes.

side note: a common object constructor such as:

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

(which i have seen as well) is even worse because it restricts initialization, removes context (e.g. parameters), and you still end up with people mixing their initialization code across classes between designated initializer and -commonInit.

through all that, a bunch of time wasted debugging all of the above problems from general misunderstanding and silly mistakes/oversights, i've concluded that a static function is the easiest to understand and maintain when you need to implement common initialization for a class. classes should insulate their clents from dangers, a problem the 'common initializer via instance method' has repeatedly failed at.

it is not an option in the OP based on the method specified, but as a general note: you can typically consdolidate common initialization more easily using convenience constructors. this is particularly useful to minimize complexity when dealing with class clusters, classes which may return specializations, and implementations which may opt to select from multiple internal initializers.




回答3:


If they share code, just have them call a third initialization method.

For example, initWithFrame might look something like this:

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

    return self;
}

Note if you're on OS X (as opposed to iOS) the frame will be NSRect instead of CGRect.

If you need to do error checking, have your initialization method return an error status like so:

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

    return self;
}

This assumes the doMyInitStuff method returns NO on error.

Also, if you haven't already looked at, there's a bit of documentation on initialization that might be useful to you (though it doesn't directly address this question):

Coding Guidelines for Cocoa: Tips and Techniques for Framework Developers



来源:https://stackoverflow.com/questions/7226239/objective-c-custom-view-and-implementing-init-method

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!