【问题标题】:Objective C - Custom view and implementing init method?Objective C - 自定义视图和实现 init 方法?
【发布时间】:2011-11-05 18:37:07
【问题描述】:

我有一个自定义视图,我希望能够同时初始化 in-codenib

编写initWithFrameinitWithCoder 方法的正确方法是什么? 它们都共享一个用于某些初始化的代码块。

【问题讨论】:

    标签: objective-c initwithframe initwithcoder


    【解决方案1】:

    在这种情况下,正确的做法是创建另一个方法,其中包含-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 子类,它本身不太可能被子类化,那么使用上述通用初始化方法就可以了。如果您正在编写一个供他人使用的框架,或者如果您不了解问题但想要做最安全的事情,请改用静态函数。

    【讨论】:

    • 您能否提供一个如何安全实施的示例?
    • @Justin,我在上面的代码中看到的最大“危险”是有人可能会创建相关类的子类,覆盖-commonInit,并可能初始化对象两次。如果这是一个真正的风险,处理它的最简单方法是将 -commonInit 保密和/或将其记录为不覆盖的内容。
    • 是的 - 这是这种方法的危险之一。正在更新我的答案...
    • @Caleb 用awakeFromNib 代替initWithCoder: 怎么样?我想我想避免处理 initWithCoder: 中的视图对象。
    • @MohammadAbdurraafay 是的,awakeFromNib 是如果您需要引用 nib 中的其他对象的方法,但这与初始化对象有点不同。在任何情况下,对于希望以编程方式初始化的 OP 来说,这都不是一个好的解决方案——awakeFromNib 如果没有从 nib 文件加载对象,显然不会被调用。
    【解决方案2】:

    解决方案并不像最初看起来那么简单。初始化有一些危险——更多的是那些更远的地方。由于这些原因,我通常在 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++ ivars 实现合适的默认构造函数,并省略大部分初始化和 dealloc 脚手架(假设您已正确启用编译器标志)。

    更新:

    我将解释为什么这是初始化对象的安全方法,并概述其他答案的典型实现危险的一些原因。

    在初始化期间调用实例方法的普通初始化程序的典型问题是它们滥用继承图,通常会引入复杂性和错误。

    建议:在部分构造的对象上调用重写的实例方法(例如在初始化和释放期间)是不安全的,应避免。访问器特别糟糕。在其他语言中,这是程序员的错误(例如 UB)。查看有关该主题的 objc 文档(参考:“实现初始化程序”)。我认为这是必须的,但我仍然知道坚持实例方法和访问器在部分构造状态下更好的人,因为它“通常对他们有用”。

    要求:尊重继承图。从基础开始初始化。自上而下破坏。总是。

    建议:保持初始化一致。如果你的基地从 init 返回一些东西,你应该假设一切都很好。不要为你的客户和子类引入一个脆弱的初始化舞蹈来实现(它很可能作为一个错误回来)。您需要知道您是否持有有效实例。此外,当您从指定的初始化程序返回一个对象时,子类将(正确地)假设您的基础已正确初始化。您可以通过将基类的 ivars 设为私有来降低这种可能性。从 init 返回后,客户端/子类假定它们派生的对象是可用的并且已正确初始化。随着类图的增长,情况变得非常复杂,错误开始蔓延。

    建议:检查 init 中的错误。还要保持错误处理和检测的一致性。返回 nil 是确定初始化期间是否存在错误的明显约定。尽早发现。

    好的,但是共享实例方法呢?

    从另一个帖子借用和更改的示例:

    @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] 将被调用两次(因为它们将被创建两次,所以会泄漏资源)并且不会执行 base 的初始化程序和错误处理。

    Caleb:如果这是一个真正的风险......

    任何一种效果都可能等同于不可靠的程序。使用传统的初始化很容易避免这个问题。

    Caleb:……处理它的最简单方法是保持 -commonInit 私有和/或将其记录为不覆盖的东西

    由于运行时在消息传递时不区分可见性,因此这种方法很危险,因为任何子类都可以轻松声明相同的私有初始化方法(见下文)。

    将方法记录为不应覆盖的内容会暴露子类的负担,并引入可以轻松避免的复杂性和问题 - 通过使用其他方法。它也容易出错,因为编译器不会标记它。

    如果坚持使用实例方法,您保留的约定如-[MONDragon constructMONDragon]-[MONKomodo constructMONKomodo] 在大多数情况下可以显着减少错误。初始化器可能只对类实现的 TU 可见,因此编译器可以标记我们的一些潜在错误。

    旁注:常见的对象构造函数如:

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

    (我也看到过)更糟糕,因为它限制了初始化,删除了上下文(例如参数),而且您最终仍然会在指定初始化程序和 -commonInit 之间的类之间混合他们的初始化代码。

    通过所有这些,由于一般的误解和愚蠢的错误/疏忽而浪费了大量时间来调试上述所有问题,我得出的结论是,当您需要实现通用初始化时,静态函数是最容易理解和维护的一类。类应该使他们的客户免受危险,这是“通过实例方法的通用初始化程序”一再失败的问题。

    它不是基于指定方法的 OP 中的选项,但作为一般说明:您通常可以使用便利构造函数更轻松地巩固公共初始化。这对于在处理类集群、可能返回特化的类以及可能选择从多个内部初始化器中进行选择的实现时最小化复杂性特别有用。

    【讨论】:

    • 当我说“私有”时,我的意思是“不暴露在界面中”。是的,我很清楚 Obj-C 将允许您向任何对象发送任何消息,并且没有什么是真正私有的。 Obj-C 有一种合作文化:当有人说“不要覆盖-commonInit”时,不要覆盖-commonInitdocumenting such things is not uncommon。我同意你在这里写的大部分内容,特别是比“commonInit”更好的名称会有所帮助,但考虑到“重复实施”的建议,你的担心似乎有些过头了,这同样“危险”。
    • @Caleb ok - 响应已更新。 “和/或”可以有多种解释方式。
    • 如果您的static InitMONView@implementation 块内实现,您可以只使用MONView * 作为参数。然后它可以使用 -> 访问 ivars。
    【解决方案3】:

    如果他们共享代码,只需让他们调用第三种初始化方法即可。

    例如,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

    另外,如果您还没有看过,这里有一些关于初始化的文档可能对您有用(尽管它没有直接解决这个问题):

    Coding Guidelines for Cocoa: Tips and Techniques for Framework Developers

    【讨论】:

    • @daxnitro 否决票被撤回(默认情况下转为赞成票,因为它是相反的)。您提出的方法可以安全地完成,但通常不会那样做。否决票,因为您的答案中没有提及/举例说明常见问题。
    • @Justin:你的回答中哪里提到了这些问题?
    • 是的,还有更多。我会扩大我的答案,因为评论框不是最好的地方。
    • @Justin 我能想到的唯一问题可能是错误检查他是否需要它。我会继续将其添加到答案中,但是还有什么我没有想到的吗?
    猜你喜欢
    • 2011-11-08
    • 2014-01-02
    • 2010-12-25
    • 2021-03-12
    • 1970-01-01
    • 1970-01-01
    • 2011-09-02
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多