Swift 2.2学习笔记之『Initializers』

Initializer,很多中文译文翻译为“构造器”,在这里也使用构造器来称呼它。

构造器是啥

构造器可以应用在class、struct和enum上,从表现形式来看,就是名称固定为init的instance method。一个type可以存在多个构造器,当然,参数必须各不相同。

class SomeClass {
    var a = 1
    var b = 2
    init() {

    }

    init(a: Int, b: Int) {
        self.a = a
        self.b = b
    }
}

构造器有啥用

不管有几个构造器,它们存在的目的只有一个:为了保证一个instance的所有properties都被初始化。

构造器怎么调用

init方法不是让你手动调用的,而是在创建一个instance时被自动调用:

class AA {
}
struct BB {
    var x: Int
    var y: Init
}
var a = AA()  //创建一个AA实例,自动调用AA.init()方法,然后由a对其强引用
var b = BB(x: 1, y: 2)  //创建一个BB实例,自动调用BB.init(x: Int, y: Int)方法,然后由b对其强引用

init方法的参数展开规则

init方法本身是一个函数。Swift的函数很有意思,它的默认形式如下:

func foo(external_param1_name internal_param1_name: Int, external_param2_name internal_param2_name: Int) {
  //do something with internal_param1_name and internal_param2_name
}
foo(external_param1_name:1, external_param2_name:2)  //调用

每个参数都有外部名和内部名,外部名给调用者使用,内部名给函数体使用。每个参数都要写外部名和内部名,实在过于繁琐,因此一般我们都使用简化版本,即每个参数只写一个名字:

func foo(param1: Int, param2: Int) {
  //do something with param1 and param2
}

对于这种形式,Swift会把第一个参数的外部名设为_(表示调用的时候可以省略外部名),内部名设为param1,第二个参数以及后续的外部名和内部名都设为param2,即实际上展开为:

func foo(_ param1: Int, param2 param2: Int)

调用如下:

foo(1, param2: 2)

而对于init方法,简写形式的展开规则将发生一点变化,所有参数都将拥有同样的外部名和内部名,也就是说,如果定义这样的构造器:

class A {
    init(a: Int, b: Int) {  // 展开为:init(a a: Int, b b: Int)
    }
}

创建A实例时,第一个参数的外部名将不能省略:

var a = A(a: 1, b: 2)  // √
var b = A(1, b: 2)  // ×

为何Swift要对普通funcinit区别对待呢?我写了另一篇文章来详述,有兴趣可以移步。

默认构造器

未自定义init方法时,将使用编译器为你提供的默认构造器。

struct默认提供两个构造器,一个是光秃秃的init(),如果你使用这个构造器,那各properties定义的时候都必须提供初始值;另一个版本是,各参数列表跟各properties一一对应(包括顺序),举例如下:

struct A {
    var aa: Int = 1
    var bb: Int
    var cc: Int = 3
}

对于上面定义的struct A,无法使用其光头版本的构造器:

var a = A()  //报错,因为bb没有初值

只能使用另一个版本:

var a = A(aa: 1, bb: 2, cc: 3)  // 参数列表aa、bb、cc跟properties一一对应

如果给bb加上初值:

struct A {
    var aa: Int = 1
    var bb: Int = 2
    var cc: Int = 3
}

那两个构造函数都能使用了:

var a = A()  //a.aa == 1
var b = A(aa: 4, bb: 5, cc: 6)  //a.aa == 4

class则不同,它只有光头init()这一个版本的默认构造器,因此,如下的定义直接无法通过编译:

class A {
    var aa: Int = 1
    var bb: Int
    var cc: Int = 3
}  //编译错误,因为bb无法初始化

只有把bb的默认值给补全了,才能编译通过:

class A {
    var aa: Int = 1
    var bb: Int = 2
    var cc: Int = 3
}
var a = A()  //works

总结:
对于struct,默认构造器只在未自定义init方法时才会有,一旦自己提供了init方法,不管以什么参数列表提供,不管提供几个,都不再享有默认构造器。
对于class而言,在未自定义designated构造器时,才享有默认designated构造器。什么是designated构造器,下文会详述。

class构造器的两种类型:designatedconvenience

首先需要明确,只有class的构造器,才有designatedconvenience之分。

init前加上convenience修饰符的init方法,称作convenience构造器,相反的,不带convenience修饰符的,都是designated。上文中举例提到的class构造器,都属于designated构造器。

一个convenience构造器,是用来调用自己的其他构造器(designated或designated均可),并最终调用自己的某个designated构造器;
而一个designated构造器,无法再调用自己的其他构造器,并且如果有SuperClass存在,必须调用SuperClass的某个designated构造器。
很显然,这样的概念源自于Objective-C,关于Objective-C中designatedconvenience的详解,可以参考这个链接:http://stackoverflow.com/a/26186421

以上规则可以推断出一条很有用的推论:子类无法直接调用SuperClass的convenience构造器

以下例子简单描述了这两种构造器:

class Parent {
    var a: Int = 1

    //拥有一个默认版本的designated init()
}

class Child : Parent {
    var b: Int

    //designated init
    init(b: Int) {
        self.b = b  //先初始化自己的property
        super.init()  //再调用parent的构造器,并且顺序不能颠倒,不然编译不通过
    }

    //convenience init
    convenience override init() {
        self.init(b: 2)  //最终需要调用自己的某个designated init
    }
}

上面的例子中,Child派生自Parent,它的designated init中,必须先把自己的properties全部初始化完毕,才能调用super.init。它的convenience init的实现,则是调用designated init,给self.b赋了一个确定的值。可以将convenience构造器理解为designated构造器的一种快捷方式,跟C++中的构造函数重载差不多的思想。

注意到convenience init有一个override修饰符,这是因为父类已经有了一个同样形式的默认的构造器init(),必须加上override来覆盖,不然编译无法通过。关于override的更多描述,下文会讲到。

一般不推荐在构造器中调用instance method,因为构造器的作用只是用来初始化instance,如果非要调用的话,需要在初始化过程完成之后(可以理解为所有properties都被设定了初始值)再调用。官方文档中这样描述:

“An initializer cannot call any instance methods, read the values of any instance properties, or refer to self as a value until after the first phase of initialization is complete.”

摘录来自: Apple Inc. “The Swift Programming Language (Swift 2.2)”。 iBooks. https://itunes.apple.com/us/book/swift-programming-language/id881256329?mt=11

现在回过头来看上一段末尾提到的:对于class而言,在未手动提供designated构造器时,才享有designated默认构造器。
看这个例子:

class A {
    var a: Int = 1

    //拥有一个默认版本的designated init()

    //定义一个convenienc构造器
    convenience init(a: Int) {
        self.init()  //调用其默认版本的designated init
        self.a = a  //在init流程完成后,可以操作property
    }
}

var a = A()
a.a  //1
var b = A(a: 2)
b.a  //2

可以看到,class A的convenience init并未阻止默认designated init的生成。

Overriding

子类定义的某个方法,它的函数名、参数列表跟父类的某个方法一致时,称作对父类对应方法的覆盖(Overriding)。

子类满足如下条件时,将直接继承父类的构造器:

  • 子类不手动提供任何designated构造器时,将继承父类的designated构造器和convenience构造器
  • 如果子类将父类的designated构造器全部覆盖(Override)一遍,那它将继承父类的convenience构造器

以上两条规则其实比较容易理解,不改变父类构造规则的情况下,对应的构造器自然会被继承;只覆盖一部分构造器,意味着打破了父类定下的构造规则,未被覆盖的那部分designated构造器,自然没理由继承下来,convenience构造器只是designated构造器的快捷方式,自然也不会被继承。举例如下:

class Parent {
    var a: Int

    //designated init()
    init(a: Int) {
        self.a = a
    }

    //定义一个convenience构造器
    convenience init() {
        self.init(a: 1)
    }
}

class Child : Parent {
    var b: Int = 2
}

var c = Child()  //调用了从Parent继承到的convenience构造器
var d = Child(a: 2)  //调用了从Parent继承到的designated构造器

上面的例子说服力不是很强,因为var c = Child()看上去似乎可以理解为调用的是Child的默认构造器,但事实上Child一旦从Parent继承了designated构造器,它已经没有默认构造器存在了,把上面例子稍微修改一下:

class Parent {
    var a: Int

    //designated init()
    init(a: Int) {
        self.a = a
    }

    //定义一个convenience构造器
    //没啥实际用途,只是为了说明问题
    convenience init(withA: Int) {
        self.init(a: withA)
    }
}

class Child : Parent {
    var b: Int = 2
}

var c = Child(withA: 2)  //调用了从Parent继承到的convenience构造器
var d = Child(a: 2)  //调用了从Parent继承到的designated构造器
var e = Child()  //编译错误,说明Child确实没有默认构造器存在

一旦子类自定义了designated构造器,则打破了上面的两条规则,举例如下:

class Parent {
    var a: Int

    //designated init()
    init(a: Int) {
        self.a = a
    }

    //定义一个convenience构造器
    convenience init() {
        self.init(a: 1)
    }
}

class Child : Parent {
    var b: Int = 2

    init() {  //自定义了一个designated构造器
        super.init(a: 1)  //虽然父类的designated构造器没有被继承下来,但好歹还是可以通过super调用的
    }
}

var d = Child()  //调用Child自定义的designated构造器
var e = Child(a: 2)  //编译错误,因为父类的designated构造器没有被继承下来

以上例子中,需要注意Child.init,前面没有加上override,是因为父类的convenience构造器对Child来说,直接就是不可见的(请参考上面提到的那条推论),所以不要加override,事实上加了反而报错。
这条规则可以简单记忆为:“覆盖”父类的convenience构造器,无需加override修饰符

可失败构造器

class、struct、enum可以在init方法名后加上?,来定义一个Failable Initializer。可失败构造器的意思是,它可以返回nil,来表示初始化失败。通过可失败构造器创建的实例,是一个Optional类型的实例。

class A {
    var a: Int

    init? (a: Int) {
        if a == 0 {
            return nil
        }
        self.a = a
    }
}

var b = A(a: 0)
var c = A(a: 1)

if let bb = b {
    print("b.a == (bb.a)")
}

if let cc = c {
    print("c.a == (cc.a)")
}

上面的例子中,将打印出c.a == 1

convenience构造器能不能是可失败的?显然可以:

class A {
    var a: Int

    init? (a: Int) {
        if a == 0 {
            return nil
        }
        self.a = a
    }

    convenience init? () {
        self.init(a: 1)
    }
}

var b = A()
b?.a  //1

上面的例子中,designated和convenience构造器都是可失败的,如果一个是可失败,一个是不可失败的呢?

class A {
    var a: Int

    init? (a: Int) {
        if a == 0 {
            return nil
        }
        self.a = a
    }

    convenience init() {
        self.init(a: 1)!  //因为designated构造器是可失败的,这里需要加上!来强制拆包
    }
}

var b = A()  //这里是ok的,但如果强制拆包失败,则会引发运行时异常
b.a

class的Required构造器

对class而言,在init方法前加上required修饰符,表示这个designated构造器是一个必选构造器,子类必须实现它,并且仍旧需要加required。注意,这里看上去像是覆盖,但不能加override修饰符:

class A {
    var a: Int
    required init() {
        self.a = 1
    }
}

class B: A {
    var b: Int

    required init() {  //需要写required,但不能写override,写了报错
        self.b = 2
        super.init()
    }
}

将以上例子小作修改:

class A {
    var a: Int
    required init() {
        self.a = 1
    }
}

class B: A {
    var b: Int = 2
}

不是说子类必须实现父类的required构造器吗,为何这里B可以不实现它?实际上B并非未实现,而是隐式继承了A的那个required designated构造器,还记得上面提到的自动继承的规则吗?这里B满足了自动继承,自然意味着实现了A的required designated构造器。

好了,大概写完了,累得一笔,回头再校对一下。构造器还是挺复杂的,Swift你特么有必要整这么烦吗?

发表评论

电子邮件地址不会被公开。 必填项已用*标注

此站点使用Akismet来减少垃圾评论。了解我们如何处理您的评论数据