深化了解代替单纯记忆

日常OC和Swift混编开发中,会遇到初始化办法运用紊乱的问题,我又仔细阅读了Object Initialization和Swit Language Programing Guide-Class Initializer,希望详细了解下initializer在Swift和OC中的差异,并经过一些异常情况加深了解,由此也更清楚最佳实践

本文并未罗列官方要求的初始化时要遵守的一切规矩,而是对这些规矩的必要性的思考,比如咱们学完官方要求的规矩后,为什么仍然很容易遗忘,原因是了解不深化,那怎么样才干深化呢之类的问题。如需求学习根底的规矩建议看下官方文档(英文原版),文末参阅中的文章中有说到的相应官方文档链接

中心思想上没差异

如小标题所言,我认为中心思想上没差异,两类初始化办法的意图和战略是一致的:

意图:确保类经过初始化后一切成员变量都完成赋值和额外的装备作业(怎么布局视图等),这相同也包含杂乱的类承继联系情况

战略:

  • 初始化分两个进程,OC和Swift都是这样
    • 第一步,为一切类的成员变量进行赋值,该进程履行路径是从子类到父类
    • 第二步,每个类进行除成员变量之外的其他装备作业如进一步调整成员变量的值、布局视图等,该进程路径是从父类到子类

为了确保以上战略,苹果官方无论是Swift仍是OC都对编写初始化办法做了一些规定,我简单地总结一下:

  • 一个类至少有一个Designated Initializer,其他的初始化办法叫做Convenience Iinitializer(Swift官方叫法)或Secondary Initializer(Objective C官方叫法)
  • Designated Initializer中必须履行父类的Designated Initializer,Convenience Initializer中则只能调用当前类中的Convenience Initializer或Designated Initializer

更详细的规矩在官方文档中都有阐明

对于上述规矩,经过官方的两个示例图能能够很直观的了解

Swift官方示例图

Class initializer in Objective C and Swift

Objective C示例图

Class initializer in Objective C and Swift

详细完成上的差异

在详细完成上,Swift Class Initialization和OC Class Initialization仍是有差异的:

初始化成员变量原理不同

尽管初始化战略第一步都是为成员变量赋初始值,但作业原理不同

先来看OC

OC官方的说法为:

The default set-to-zero initialization performed on an instance variable during allocation is often sufficient. Make sure that you retain or copy instance variables, as required for memory management.

用代码举例:

- (instancetype)initWithFrame:(CGRect)frame {
											// 1.1
	if (self = [super initWIthFrame:frame]) { 1.2
		_instanceIntProperty = 1; // 2
	}
	return self;
}

真正为成员变量赋值的进程其实是在1.1和1.2,很多人可能一直是认为在2吧,2其实是初始化战略中的第2步。1.1这一行啥也没有,其实意思便是体系帮咱们完成了成员变量赋值作业。只是它会将一切的成员变量赋值为默认值0 or nil等

那Swift为成员变量赋值则更易辨认

init(frame: CGRect) {
	_instanceIntProperty = 1 // 1.1
	super.init(frame: frame) // 1.2
	_instanceIntProperty = 2 // 2
}

毫无疑问,1.1便是赋值进程。默认情况下Swift Class的每个成员变量是没有默认值的,咱们能够为其指定默认值或许在初始化办法中为其赋值,那么这个赋值进程就发生在1中

而且,经过上面代码相比OC,能够更清楚地看出初始化战略的第1、2步,上述代码中的1.1和1.2便是战略中的第1步,2以及后续代码时第2步

Swift相比OC做了更多保护机制

Swift中参加很多保护战略,使开发者在自定义Class时更易写出符合规范的初始化办法,详细来说:

  • 经过编译器强制要求先履行战略1,再履行战略2。即上面的代码咱们不可能在不为_instanceIntProperty赋值的情况下而履行super.init(frame:)办法,不然编译犯错
  • 相同在编译阶段,强制束缚类同级和跨层级之间Convenience Initializer和Designated Initializer的调用次序,一旦违反规矩,编译报错

OC编写初始化办法易错点

相比于Swift,由于OC的动态特性以及编译器并不会强制束缚开发者依照规矩定义初始化办法,在实际开发中仍是比较容易犯错的。本节罗列几个实际工程中遇到的例子:

先贴一下官方针对在OC中自定义初始化办法时的一些规矩或许说提示,当看完本节中的案例后再回来看这些规矩,领会更明显

  • When you define a subclass, you must be able to identify the designated initializer of the superclass and invoke it in your subclass’s designated initializer through a message to super.
  • You must also make sure that inherited initializers are covered in some way.
  • When designing the initializers of your class, keep in mind that designated initializers are chained to each other through messages to super; whereas other initializers are chained to the designated initializer of their class through messages to self.
  • The designated initializer for each class is the initializer with the most coverage; it is the method that initializes the attribute added by the subclass. The designated initializer is also the init… method that invokes the designated initializer of the superclass in a message to super.
  • When creating a subclass, it’s always important to know the designated initializer of the superclass.
  • Secondary initializers (as in this example) are frequently overridden versions of inherited initializers

Designated Initializer中未调用父类Designated Initializer

@Interface ABCView: UIView
@end
@implementation ABCView
- (id)init {
    if (self = [super init]) {
        [self commonInit];
    }
    return self;
}
- (void)commonInit {
	/// balabala
}
@end
  • 上述代码存在的问题是没搞清楚ABCView父类的Designated Initializer是谁。UIView的Designated Initializer是initWithFrame:
  • 所以在完成自己的初始化办法时挑选了init办法
  • 这会导致,当运用[[ABCView alloc] initWithFrame:frame]初始化ABCView时,commonInit未履行

解决办法也很简单

- (instancetype)initWithFrame:frame {
    if (self = [super initWithFrame:frame]) {
        [self commonInit];
    }
    return self;
}
- (instancetype)init {
	return [self initWithFrame:CGRectZero]; 
}

此时无论咱们[ABCView new], [[ABCView alloc] init]仍是[[ABCView alloc] initWithFrame:frame],都不会犯错

诘问一个问题:必须要重写init办法吗?

当然不是,能够彻底不必重写init办法,因为UIView的init办法也是相似的完成,ABCView承继了该办法,所以效果是等价的

至于为什么规定Designated Initializer中必须履行父类Designated Initializer办法?我的另一篇文章中有说到–再谈Initializer in iOS

子类掩盖了父类的完成

@Interface BaseClass: NSObject
@end
@implementation BaseClass
- (instancetype)init {
    self = [super init];
    if (self) {
        [self privateMethod];
    }
    return self;
}
- (void)privateMethod {
    NSLog(@"privateMethod in BaseClass!");
}
@end
@Interface SubClass: BaseClass
@end
@implementation SubClass
- (void)privateMethod {
    NSLog(@"privateMethod in SubClass!");
}
@end

需求阐明的是两个类中的privateMethod办法都是其各自内部的私有办法

当履行[[SubClass alloc] init]时,咱们会发现子类的privateMethod履行了,而父类的没有履行

原因在于OC的音讯发送机制,内部在寻觅privateMethod完成时,会先去看子类中有无该完成,有就履行。所以OC中其实本质上没有什么私有办法

但这确实给咱们写初始化办法提了一个醒,在命名私有办法时可能需求考虑被掩盖的情况

Swift和OC的音讯发送机制不同,它没问该问题

杂乱Designated Initializer的情况下,合理运用初始化办法相关的宏

比如,咱们需求写一个高可复用的自定义视图,不同业务需求经过承继该视图的办法来运用它,初始化办法需求传入几个必需的参数,很显然这个参数最多的Initializer便是Designated Initialzer了,一起可能还需求供给几个快捷的Convenience Initializer

这时候运用该视图的开发人员运用时,看到有这么多初始化办法可能不知怎么用

OC给咱们供给了几个编译修饰符,来提高代码的易用性

@interface ABCView : UIView
- (instancetype)initWithFrame:(CGRect)frame NS_UNAVAILABLE;
- (instancetype)initWithCoder:(NSCoder *)coder NS_UNAVAILABLE;
- (instancetype)initWithA:(TypeA)a b:(TypeB)b c:(TypeC)c NS_DESIGNATED_INITIALIZER;
- (instancetype)initWithA:(TypeA)a;
@end

经过上面相似NS_DESIGNATED_INITIALIZER的宏,运用方会很容易了解谁是Designated Initialzer,避免承继时犯错;并且由于咱们内部不想重写initWithFrame:等办法,能够运用NS_UNAVAILABLE将相关办法从编译层面禁掉,运用方便底子无机会用错办法了

而且,由于Swift的强规矩,如果OC的类写不好,在混编情况下,Swift中运用不规范初始化的OC类时,往往遇到各种棘手的问题

最终

终极建议:抛弃OC,改用Swift,Swift把一切问题都主动提示出来或解决掉了,想啥呢,还在用OC?

当年学习OC时没敢挑选官方英文文档,现在才仔细阅读,发现真的是好资料,掩盖了各种异常情况,一起会将初始化的意图和中心思想讲出来,这在中文材料中可不一定能体现出来。所以学好英语,看一手资料!

参阅

  • Object Initialization
  • 再谈Initializer in iOS
  • Initializers in Swift Class