Swift initializer

今天聊聊Swift中的构造器,无论你是Swfit新手还是混迹论坛多年的老鸟,相信你看完本文之后一定会有所收获。

我们将讨论以下四部分内容:

  • designated initializer
  • convenience initializer
  • required 关键字
  • protocol中的initializer

Designated Initializer

当我们创建一个class的时候,如果这个class拥有****未初始化的非Optional属性****,则必须提供一个构造器(称为designated initializer)。这句话有两个重点:
****1.未初始化;2.非Optional属性。****
比较一下下面几种写法:

//A
class Person {
    let name : String = ""
}
//B
class Person {
    let name : String
}
//C
class Person {
    var name : String?
}
//D
class Person {
    let name : String?
}

A,C都可以正常编译,B,D会报错(error: class 'Person' has no initializers)。因为Swift不允许非Optional变量在没有初始化的情况下使用或者访问,这样会造成潜在的风险,于是Swift编译器对构造器做了严格的限制:类必须提供一个designated initializer,并且初始化所有****未初始化的非Optional属性****


A,B两种写法中,let换成var结果是一样的。但是D比较特殊,在Swift中,常量在访问和使用之前必须初始化(哪怕是Optional)

var p : String?
print(p)
//正常编译
let p : String?
print(p)
//报错:error: constant 'p' used before being initialized

由此可见我们上面的描述并不准确,应当是****未初始化的非Optional属性或Optinal常量****。不过一般我们不会声明一个Optional常量,另外这么说也是在太过拗口,所以就简单称之为****未初始化的非Optional属性****吧(下文也会这样表述)。

同样,在子类初始化过程中,Swift也做了严格的限制

class Person {
    let name : String
    init(name : String){
        self.name = name
    }
}
class Engineer: Person {
    var title : String
    init(name: String, title: String) {
        self.title = title
        super.init(name: name)
    }
}

****首先****,Engineer必须调用super.init,如果你这样写:

    init(name: String, title: String) {
        self.title = title
        self.name = name
    }

不好意思,self.name是常量,不可修改。即便我们将let name : String改为var name : String,仍会报错,而且是两个:

  • error: super.init isn't called on all paths before returning from initializer
  • error: use of 'self' in property access 'name' before super.init initializes self

从这两个错误信息可以看出,在子类的构造器中,父类的成员变量必须通过父类的designated initializer进行初始化。也就是说,我们必须调用super.init(第一个error),而且在调用super.init之前不能使用和访问任何父类的成员变量(第二个error)。

****其次****,super.init必须在子类所有****未初始化的非Optional属性****初始化之后调用。


这跟我们以前所了解的不同,例如在JAVA的构造器中我们会先调用super.init。Swift之所以这样做,同样是为了安全性。
假设我们要求在初始化一个Person之后,立刻输出一些信息用来debug:

class Person {
    var name : String
    init(name : String){
        self.name = name
        printDescription()
    }
    
    func printDescription() -> () {
        print("name=\(name)")
    }
}
class Engineer: Person {
    var title : String
    init(name: String, title: String) {
        self.title = title
        super.init(name: name)
    }
    
    override func printDescription() -> () {
        print("name=\(name), title=\(title)")
    }
}

如果在Engineerinit中,我们首先调用super.init,那么我们就会在初始化title之前执行子类的printDescription方法,这是Swift所不允许的。


Convenience Initializer

一个类可以提供多个designated initializer,但是designated initializer不可以调用designated initializer。
如果需要提供一个依赖designated initializer的构造器,需要添加关键字convenience,称为convenience initializer。
在上面的例子中,假设我们从文件中读取人的信息,读取到的是****“名字;职位;联系方式”****格式的字符串。我们需要写一个构造器方法从这串文字中解析出相应的信息。

class Person {
    let name : String
    init(name : String){
        self.name = name
    }
    
    convenience init(convertFromLiteral value : String){
        var name = ""
        let components = value.componentsSeparatedByString(";")
        for component in components{
            if(name.characters.count == 0){
                name = component
                break
            }
        }
        self.init(name: name)
    }
}

因为新的构造器调用了self.init,因此需要添加关键字convenience,否则会报错:error: designated initializer for 'Person' cannot delegate (with 'self.init'); did you mean this to be a convenience initializer?

一个类可以拥有多个convenience initializer,而且convenience initializer可以调用其他convenience initializer。
convenience initializer有以下约束:

  • 必须调用当前类的designated initializer,调用父类的designated initializer是不行的。
  • 只有当前类可以使用(子类也可以使用,不过需要特殊处理。见下文)。

Required 关键字

如果父类中的构造器需要在子类中重写,可以使用required关键字。子类必须重写父类中标记为required的构造器方法(不管是designated initializer还是convenience initializer)。
上面的例子中,init(convertFromLiteral value : String)并不适合子类,所以我们加上required关键字,编译器会提示我们Engineer没有实现required initializer。

class Engineer: Person {
    var title : String
    
    init(name: String, title: String) {
        self.title = title
        super.init(name: name)
    }
    
    required convenience init(convertFromLiteral value : String){
        var name = ""
        var title = ""
        let components = value.componentsSeparatedByString(";")
        for component in components{
            if(name.characters.count == 0){
                name = component
            }else if(title.characters.count == 0){
                title = component
                break
            }
        }
        self.init(name: name, title: title)
    }
}

上文提到了convenience initializer只有当前类才能使用,Swift不允许子类通过父类构造器进行初始化是显而易见的。convenience initializer实际上更像一个方法,如果我们将某个convenience initializer所依赖的designated initializer标记为required,就相当于告诉了编译器我们的子类一定重写了这个designated initializer。也就是说,如果我们的子类通过这个convenience initializer进行初始化,最终会执行子类中重写的designated initializer。

说这么一大堆,用代码看的更清楚:

class Person {
    let name : String
    required init(name : String){
        self.name = name
    }
    
    convenience init(convertFromLiteral value : String){
        var name = ""
        let components = value.componentsSeparatedByString(";")
        for component in components{
            if(name.characters.count == 0){
                name = component
                break
            }
        }
        self.init(name: name)
    }
}

class Engineer: Person {
    var title : String
    
    required init(name: String) {
        self.title = "unknow"
        super.init(name: name)
    }
    
    init(name: String, title: String) {
        self.title = title
        super.init(name: name)
    }
}

let peter = Person.init(convertFromLiteral: "peter;")
print(peter.name)
let tom = Engineer.init(convertFromLiteral: "tom;")
print(tom.name + ", " + tom.title)

将父类中convenience initializer所依赖的designated initializer标记为required,我们就可以通过该convenience initializer初始化子类了。


Protocol中的Initializer

Swift提供了很多包涵构造器的protocol,如StringLiteralConvertibleDictionaryLiteralConvertibleArrayLiteralConvertible等。

如果我们想通过如下方式初始化,我们就需要用到StringLiteralConvertible

let peter : Person = "Peter"

代码如下:

class Person: StringLiteralConvertible{
    let name: String
    init(name value: String) {
        self.name = value
    }
    
    required convenience init(stringLiteral value: String) {
        self.init(name: value)
    }
    
    required convenience init(extendedGraphemeClusterLiteral value: String) {
        self.init(name: value)
    }
    
    required convenience init(unicodeScalarLiteral value: String) {
        self.init(name: value)
    }
}

let peter : Person = "Peter"

是不是很cool?

需要注意的是,Swfit很好的控制了构造器的作用域(默认情况下,convenience initializer是子类不可见的),但是protocol中的方法是子类可见的。所以protocol中的构造器方法都必须声明为required
如果实现protocol的是一个final类,那么则没必要再是required,因为它不会有子类。


等一下,如果Person是第三方库中的类呢?
你可能会说没问题啊,我们可以通过extension实现StringLiteralConvertible

class Person: StringLiteralConvertible{
    let name: String
    init(name value: String) {
        self.name = value
    }
    
}

extension Person: StringLiteralConvertible{
    required convenience init(stringLiteral value: String) {
        self.init(name: value)
    }
    
    required convenience init(extendedGraphemeClusterLiteral value: String) {
        self.init(name: value)
    }
    
    required convenience init(unicodeScalarLiteral value: String) {
        self.init(name: value)
    }
}

let peter : Person = "Peter"

真是这样吗?Sorry!!!

  • 首先,required initializer必须在类中声明。所以上面的required关键字要不得
  • 其次,protocol中的构造器方法必须声明为required,除非Personfinal的。

emmm....

What Else

structenum同样可以拥有构造器,不过上面提到的designated initializer,convenience initializer,required 关键字都是针对class的,不要搞混。
最后上段代码,供大家把玩

enum Sex{
    case Male
    case Female
    case Unknow
}
let malesName = ["peter", "tom"]
let femalesName = ["marry", "taylor"]
extension Sex: StringLiteralConvertible{
    init(stringLiteral value: String) {
        if(malesName.contains(value)){
            self = .Male
        }else if(femalesName.contains(value)){
            self = .Female
        }else{
            self = .Unknow
        }
        print(self)
    }
    init(extendedGraphemeClusterLiteral value: String) {
        self.init(stringLiteral: value)
    }
    init(unicodeScalarLiteral value: String) {
        self.init(stringLiteral: value)
    }
}

var sex : Sex = "tom"
sex = "marry"
sex = "gollum"
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,835评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,598评论 1 295
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,569评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,159评论 0 213
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,533评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,710评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,923评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,674评论 0 203
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,421评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,622评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,115评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,428评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,114评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,097评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,875评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,753评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,649评论 2 271

推荐阅读更多精彩内容

  • 原文地址:http://huizhao.win/2016/11/13/swift-init/ 从 Objectiv...
    赵大老板阅读 33,898评论 11 120
  • 简介 *自定义构造过程 *默认构造器 *值类型的构造器代理 *类的继承和构造过程 *可失败构造器 *必需构造器 *...
    FishSha阅读 238评论 0 0
  • 适当距离 产生惊喜 今天他好像对我特别好呀,不知道是这两天我不在伙食太差突然一下伙食好了感动的,还是太想我了,总之...
    fangyuanjili阅读 161评论 0 0
  • 日念亲人一处好,加持美好享幸福! 第11天(2017.8.9) 1.念老公好:上午继续去婆婆家帮忙,中午回来后下午...
    轩轩妈慧子阅读 211评论 0 0
  • 今天有人来倾诉,不久前刚做的一个决定,发现并不适合自己,或者说跟当初设想的有很多不同。此时,恰好有另外一个机会,在...
    理想家园阅读 87评论 0 4