从 Swift 初始化说起

作者: 赵大老板 | 来源:发表于2016-11-16 11:40 被阅读4948次

    原文地址:http://huizhao.win/2016/11/13/swift-init/

    从 Objective-C 转到 Swift 后,可能首先就会发觉 Swift 的初始化方法变了,曾经 Objective-C 里面随意信手拈来的初始化代码可能不好使了,一起来学习一下吧。


    初始化方法调用顺序

    分别创建一个 Swift 类和 Objective-C 类,然后使用 Xcode 模板新建一个初始化方法后,我们可以得到如下代码:

    @implementation BlogInitOC
    
    - (instancetype)init
    {
        self = [super init];
        if (self) {
          
        }
        return self;
    }
    
    @end
    
    class BlogInit: NSObject {
    
        override init() { // 需要手动添加 override 关键字
            
        }
    
    }
    

    对比发现,Swift 的初始化代码需要加上 override 关键字,方法内部没有调用 super 的 init 方法,并且没有 return 语句。虽然如此,但此时编译是可以通过的。

    接着,分别给这两个类加上一个属性变量 param:

    @interface BlogInitOC ()
    @property (nonatomic, strong) NSString *param;
    @end
    
    @implementation BlogInitOC
    
    - (instancetype)init
    {
        self = [super init];
        if (self) {
            
        }
        return self;
    }
    
    @end
    
    class BlogInit: NSObject {
        
        let param: String
            
        override init() {
            
        }
    
    }
    

    情况变了,Objective-C 类一切正常,而 Swift 类提示了一条错误 Property 'self.param' not initialized at implicitly generated super.init call,意思很明确,param 参数没有在隐式生成 super.init 调用之前完成初始化。原来 Swift 中并不是不调用 super.init,而是为了方便开发者由编译器完成了这一步,但是要求开发者在初始化方法中调用 super.init 之前完成成员变量的初始化。

    修改后的代码如下:

    class BlogInit: NSObject {
        
        let param: String
          
        override init() {
        self.param = "zhaohui"
        // super.init() // 可不写,编译器隐式生成 
        }
    
    }
    

    对于需要修改父类中成员变量值的情况,我们需要在调用 super.init 之后再进行修改,代码如下:

    class Cat {
        var name: String
        
        init() {
            name = "cat"
        }
    }
    
    class Tiger: Cat {
        let power: Int
        
        override init() {
            power = 10
            super.init()
            name = "tiger"
        }
    }
    

    因此 Swift 中类的初始化顺序可以总结如下:

    1. 初始化自己的成员变量,必须
    2. 调用父类初始化方法,如无需第三步,则这一步也可省略
    3. 修改父类成员变量,可选

    这里补充说明两点:

    1. 使用 let 声明的常量是可以在初始化方法中进行赋值的,这是编译器所允许的,因为 Swift 中的 init 方法只会被调用一次,这与 Objective-C 不同;
    2. 即使成员变量是可选类型,如:let param: String?,仍然是需要进行初始化的。

    关键词

    看完上面这部分,好像 Swift 初始化也没什么,不过是语法上一些变化,不过当我们按照曾经 Objective-C 的习惯添加类间继承关系、自定义初始化方法等,问题又来了。

    先来看下面这个例子:

    class CustomView: UIView {
        let param: Int
            
        override init() { // error 1
            self.param = 1
            super.init() // error 2
        }
    } // error 3
    

    好奇怪,我们只是将父类从 NSObject 修改为 UIView,竟然收到3条错误:

    1. Initializer does not override a designated initializer from its superclass
    2. Must call a designated initializer of the superclass 'UIView'
    3. 'required' initializer 'init(coder:)' must be provided by subclass of 'UIView'

    稍等,再看一个例子:

    class CustomView: UIView {
        convenience init(param: Int, frame: CGRect) {
            super.init(frame: frame) // error
        }
        
        required init?(coder aDecoder: NSCoder) {
            fatalError("init(coder:) has not been implemented")
        }
    }
    

    这时,我们又得到了一条新的错误:

    Convenience initializer for 'CustomView' must delegate (with 'self.init') rather than chaining to a superclass initializer (with 'super.init')

    前面两个例子中,我们看到了关键字 designatedconveniencerequired,理解了这几个关键字也就能帮助我们理解整个初始化过程了。

    designated

    看到 designated,我们很容易联想到 Objective-C 中 NS_DESIGNATED_INITIALIZER,它们的含义比较接近,都是用来设置指定初始化器,关于 Objective-C 中的用法,请参阅《正确编写Designated Initializer的几个原则》,下面我们主要讨论 Swift 中的 designated

    在 Apple 的官方文档中讲到,Swift 定义了两种类初始化器类型,用来保证所有成员属性能够获得一个初始化值,即 designated initializersconvenience initializers。对于 designated initializers 的定义如下:

    Designated initializers are the primary initializers for a class. A designated initializer fully initializes all properties introduced by that class and calls an appropriate superclass initializer to continue the initialization process up the superclass chain.

    加粗部分是几处关键的描述:

    1. primary initializers:designated initializers 是一个类的主初始化器,理论上来说是一个类初始化的必经之路(注:不同的初始化路径可能调用不同的 designated initializers);
    2. fully initializes all properties:这点很明确,必须在 designated initializers 中完成所有成员属性的初始化;
    3. calls an appropriate superclass initializer:需要调用合适的父类初始化器完成初始化,不能随意调用。

    下面我们结合前面的 Sample-1 进行解释:

    class CustomView: UIView {
        let param: Int
            
        override init() { // error 1
            self.param = 1
            super.init() // error 2
        }
    } // error 3
    

    在 Swift 中,designated initializers 的写法和一般的初始化方法无异,Sample-1 中,我们试图去 override init,可以理解为我们就是在 override 一个 designated initializers,然后我们收到了错误 Initializer does not override a designated initializer from its superclass,可见我们并没有找到合适的 designated initializers,我们进入父类 UIView,可以看到下面两个初始化方法:

        public init(frame: CGRect)
        public init?(coder aDecoder: NSCoder)
    

    原来,这两个类才是父类的 designated initializers,那我们改改试试:

    class CustomView: UIView {
        let param: Int
            
        override init(frame: CGRect) { // error 1 fixed
            self.param = 1
            super.init() // error 2
        }
    } // error 3
    

    果然,error 1 没了,由此也可以看出,我们去 override 一个不是 designated initializers 的初始化器不满足定义中所说的 primary initializers,这就可能导致这个初始化器不被执行,成员变量没有初始化,这样创建的“半成品”实例可能存在一些不安全的情况。

    第二条 fully initializes all properties,这点我们并没有犯错,因为我们已经初始化了 CustomView 类中引入的 param 变量。

    第三条 calls an appropriate superclass initializer 很明显就对应了 error 2,我们 override init(frame: CGRect),那我们就必须调用对应的父类初始化方法,修改如下:

    class CustomView: UIView {
        let param: Int
            
        override init(frame: CGRect) { // error 1 fixed
            self.param = 1
            super.init(frame: frame) // error 2 fixed
        }
    } // error 3
    

    再来看 error 3:'required' initializer 'init(coder:)' must be provided by subclass of 'UIView',这条错误提示我们 init(coder:) 是一个 'required' initializer,子类必须提供,那什么是 required 呢?

    required

    对于 required,官方给出了一句说明:

    Write the required modifier before the definition of a class initializer to indicate that every subclass of the class must implement that initializer.

    意思很明白,通过添加 required 关键字强制子类对某个初始化方法进行重写。前面的 error 3 中,init(coder:) 正好对应了父类 UIView 中的第二个初始化方法,所以想要修复这个错误,就需要重写 init(coder:)

    其实,在 Xcode 中,双击这个错误就会帮我们插入这个方法,修复后代码如下:

    class CustomView: UIView {
        let param: Int
            
        override init(frame: CGRect) { // error 1 fixed
            self.param = 1
            super.init(frame: frame) // error 2 fixed
        }
        
        required init?(coder aDecoder: NSCoder) {
            fatalError("init(coder:) has not been implemented")
        }
    } // error 3 fixed
    

    这样,我们修复了 error 3,不过插入的这个方法很奇怪,方法体里直接写 fatalError("init(coder:) has not been implemented"),那岂不是走到这里就 fatal 了,这不是坑我们吗?!

    前文中我们讲了,designated initializers 是一个类的主初始化器,理论上来说是一个类初始化的必经之路(注:不同的初始化路径可能调用不同的 designated initializers),其实,这个 init(coder:)init(frame: frame) 就是不同的初始化路径,当我们使用 xib 方式初始化一个 view 时,就会走到 init(coder:)。此时,如果我们没有真正实现这个方法,就会出现 fatal crash,如下图所示:

    init(coder:) fatal

    所以到目前为止,我们仍然没有提供一套完整的、安全的初始化方法,需要继续补全 init(coder:) 方法,以覆盖全部可能的初始化流程:

    class CustomView: UIView {
        let param: Int
            
        override init(frame: CGRect) {
            self.param = 1
            super.init(frame: frame)
        }
        
        required init?(coder aDecoder: NSCoder) {
            self.param = 1
            super.init(coder: aDecoder)
        }
    }
    

    这样,我们就完成了一个 UIView 子类的初始化代码。

    convenience

    在 Apple 的官方文档对 convenience initializers 的定义如下:

    Convenience initializers are secondary, supporting initializers for a class. You can define a convenience initializer to call a designated initializer from the same class as the convenience initializer with some of the designated initializer’s parameters set to default values. You can also define a convenience initializer to create an instance of that class for a specific use case or input value type.

    convenience initializers 是对类初始化方法的补充,用于为类提供一些快捷的初始化方法,可以不创建这类方法,但如果创建了,就需要遵循原则:call a designated initializer from the same class,那么回到前文的 Sample-2:

    class CustomView: UIView {
        convenience init(param: Int, frame: CGRect) {
            super.init(frame: frame) // error
        }
        
        required init?(coder aDecoder: NSCoder) {
            fatalError("init(coder:) has not been implemented")
        }
    }
    

    这里我们得到的错误,正好匹配了上面的原则:

    Convenience initializer for 'CustomView' must delegate (with 'self.init') rather than chaining to a superclass initializer (with 'super.init')

    看来我们需要调用该类自己的 designated initializer,那么我们应该 override init(frame: CGRect),然后修改 convenience init(param: Int) 中的 super 为 self:

    class CustomView: UIView {
        convenience init(param: Int, frame: CGRect) {
            self.init(frame: frame) // error fixed
        }
            
        override init(frame: CGRect) {
            super.init(frame: frame)
        }
            
        required init?(coder aDecoder: NSCoder) {
            fatalError("init(coder:) has not been implemented")
        }
    }
    

    好啦,这下没错了!接着,我要用一个成员变量把 param 的值存起来:

    class CustomView: UIView {
        var param: Int
        
        convenience init(param: Int, frame: CGRect) {
            self.param = param // error
            self.init(frame: frame)
        }
            
        override init(frame: CGRect) {
            super.init(frame: frame) // error
        }
            
        required init?(coder aDecoder: NSCoder) {
            fatalError("init(coder:) has not been implemented")
        }
    }
    

    又出来两个错误:

    1. Use of 'self' in property access 'param' before self.init initializes self
    2. Property 'self.param' not initialized at super.init call

    第二个错误我们清楚,是需要在调用 super.init 之前初始化本类成员属性。第一个错误其实,这是 Swift 编译器提供的安全检查,文档原文如下:

    A convenience initializer must delegate to another initializer before assigning a value to any property (including properties defined by the same class). If it doesn’t, the new value the convenience initializer assigns will be overwritten by its own class’s designated initializer.

    原来 Swift 防止 convenience initializers 中赋值之后又被该类自己的 designated initializer 覆盖而做了检查,因此,正确的方式应该是调用该类的其他初始化方法之后再修改属性值,最终修改如下:

    class CustomView: UIView {
        var param: Int
        
        convenience init(param: Int, frame: CGRect) {
            self.init(frame: frame)
            self.param = param // error fixed
        }
            
        override init(frame: CGRect) {
            self.param = 0 // error fixed
            super.init(frame: frame)
        }
            
        required init?(coder aDecoder: NSCoder) {
            self.param = 0
            super.init(coder: aDecoder)
        }
    }
    

    小结

    对于 Swift 中的初始化方法,总结如下:

    1. 子类中初始化方法必须覆盖全部初始化路径,以保证对象完全初始化;
    2. 子类中 designated initializer 必须调用父类中对应的 designated initializer,以保证父类也能完成初始化;
    3. 子类中如果重写父类中 convenience initializer 所需要的全部 init 方法,就可以在子类中使用父类的 convenience initializer 了;
    4. 子类如果没有定义任何 designated initializer,则默认继承所有父类的 designated initializerconvenience initializer
    5. 子类中必须实现的 designated initializer,可以通过添加 required 关键字强制子类重写其实现,以保证依赖该方法的 convenience initializer 始终可以使用;
    6. convenience initializer 必须调用自身类中的其他初始化方法,并在最终必须调用一个 designated initializer
    7. 在构造器完成初始化之前, 不能调用任何实例方法,或读取任何实例属性的值,self 本身也不能被引用。

    看上去 Swift 中对初始化过程添加了很多“规矩”,开发上繁琐了不少,但是却更有利于帮助我们开发更规范、更安全的初始化方法,从而减少一些潜在的问题,所以掌握这些“规矩”是非常有用且值得的。


    可失败初始化器

    可失败初始化器(Failable Initializers),即可以返回 nil 的初始化方法,这在 Objective-C 的初始化过程中本来就支持,但这种支持反而导致逻辑上的模糊,什么时候返回 nil 其实我们并不明确,而 Swift 对这些情况进行了明确。

    官方文档对 Failable Initializers 的定义如下:

    A failable initializer creates an optional value of the type it initializes. You write return nil within a failable initializer to indicate a point at which initialization failure can be triggered.

    很容易理解,就是将初始化返回值变成 optional value(在 init 后面加上 ?),并在不满足初始化条件的地方 return nil,这样,我们通过调用处判断是否有值即可知道是否初始化成功。

    我们以官方例子进行解释:

    class Product {
        let name: String
        init?(name: String) {
            if name.isEmpty { return nil }
            self.name = name
        }
    }
     
    class CartItem: Product {
        let quantity: Int
        init?(name: String, quantity: Int) {
            if quantity < 1 { return nil }
            self.quantity = quantity
            super.init(name: name)
        }
    }
    

    CartItem 类的初始化方法先对传入参数 quantity 的值进行判断,小于 1 则为无效参数,然后 return nil(初始化失败),大于或等于 1 则继续调用父类 Product 的初始化方法,再次判断传入参数 name,为空则 return nil(初始化失败),否则继续初始化。

    这样,我们通过下面几种不同参数进行初始化,即可得到不同的初始化结果:

    if let twoSocks = CartItem(name: "sock", quantity: 2) {
        print("Item: \(twoSocks.name), quantity: \(twoSocks.quantity)")
    }
    // Prints "Item: sock, quantity: 2"
    
    if let zeroShirts = CartItem(name: "shirt", quantity: 0) {
        print("Item: \(zeroShirts.name), quantity: \(zeroShirts.quantity)")
    } else {
        print("Unable to initialize zero shirts")
    }
    // Prints "Unable to initialize zero shirts"
    
    if let oneUnnamed = CartItem(name: "", quantity: 1) {
        print("Item: \(oneUnnamed.name), quantity: \(oneUnnamed.quantity)")
    } else {
        print("Unable to initialize one unnamed product")
    }
    // Prints "Unable to initialize one unnamed product"
    

    总的来说,可失败初始化器的设定,是在保证安全性的基础上提供了逻辑上更清晰的初始化方式。Failable Initializers 所有的结果都将是 T? 类型,通过 Optional Binding 方式,我们就能知道初始化是否成功,并安全地使用它们了。

    注:本文所有描述均针对类类型初始化,对于结构体或枚举类型基本类似,还有一些其他特性大家可以参考官方文档进行学习。


    参考资料

    1. The Swift Programming Language: Initialization
    2. Swift 类构造器的使用
    3. 初始化方法顺序
    4. 正确编写Designated Initializer的几个原则
    5. DESIGNATED,CONVENIENCE 和 REQUIRED
    6. 初始化返回 NIL

    相关文章

      网友评论

      • 6d822d99ddb3:强 这篇文章好 学到了 好久没看到这么有收获的文章了 :smiley: :smiley:
      • 我本善良:思路清晰,谅解透彻。但是有个问题,便利构造器真正在实际开发中的作用是啥,感觉指定构造器就够用了
        Lin__Chuan:如果仅仅是override的话, 指定构造器就够了, 如果需要添加或修改新的构造器参数的话, 就必须用便利构造器了
      • 王玺__boy:很详细,能再发一篇继承重载的构造函数编写方法不
      • amisarex:碉堡了

      本文标题:从 Swift 初始化说起

      本文链接:https://www.haomeiwen.com/subject/cjqrpttx.html