我有一个自定义视图,我希望能够in-code
在nib
.
编写initWithFrame
和initWithCoder
方法的正确方法是什么?它们都共享一个用于某些初始化的代码块。
我有一个自定义视图,我希望能够in-code
在nib
.
编写initWithFrame
和initWithCoder
方法的正确方法是什么?它们都共享一个用于某些初始化的代码块。
在这种情况下,正确的做法是创建另一个方法,其中包含两个-initWithFrame:
and共有的代码,-initWithCoder:
然后从两个-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;
}
请注意贾斯汀的回答中概述的问题,特别是任何子类都不得覆盖 -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
目标-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:如果这是一个真正的风险......</p>
任何一种效果都可能等同于不可靠的程序。使用传统的初始化很容易避免这个问题。
Caleb:……处理它的最简单方法是保持 -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
在错误时返回。
此外,如果您还没有看过,有一些关于初始化的文档可能对您有用(尽管它没有直接解决这个问题):