Swift 构造器探究

什么时候要用构造器?

对于类(Class)

其实在其他语言中,比如说Java对于属性的初始化没有严格的要求。甚至在Model层只有对应属性的get,set访问器。而在Swift中无论是对于结构体(Structure)还是类(Class),如果其中存在存储属性(stored property),那么必须在合适的地方给它赋初始值,也就是初始化。不能让它们成为不确定的状态,即没有初始化。关于初始化,Swift提供了两种方式。一种是属性定义的时候初始化,也就是赋默认值。 第二种是在构造器中初始化。这里做个小结:存储属性必须初始化,初始化的方式且只能在以上两种方式选择,并且至少包含其中一种。

而对于第一种,在Swift中又有两种初始化方式。第一种,是给予明确的值。比如说var name = "Bob"Swift的数据类型推断机制会自动推断出name是一个String类型变量,初始值为Bob。第二种,是Optional类型初始化,通常用在这个属性在程序运行过程中可能存在值也可能不存在值的时候。比如说var avatar: UIImage?一个人可能没有头像。假设avatarPerson类的一个存储属性,程序在运行过程,如果Person类创建一个实例let myPerson = Person(),如果没有给myPerson.avatar赋值,那么myPersonavatar属性自动初始化为nil。如果myPerson.avatar = UIImage(named: "prettyGirl"),那么avatar属性初始化就是UIImage(named: "prettyGirl")

而对于第二种初始化方式,更加具体的说,是如果没有是实现第一种初始化方式的时候,必须实现的。也就是说如果你定义一个属性var avatar: UIImage既没有给予明确的初始值,也没有让它成为Optional类型,那么必须实现在构造器中的初始化。这就是什么时候要用构造器的重点了。
然而Swift的构造器又有两种,一种是designated构造器,一种是convenience构造器。所有designated构造器都必须初始化那些没有满足第一种情况的存储属性。注意这里是类中所有的designated构造器都必须要做这件事。具体怎么做请看后文Designated构造器

对于结构体(Structure)

Swift中结构体和类的构造器其实差不多。除了结构体中没有析构器(Deinitializer),不能够继承(inherit)以及结构体有memberwise构造器外大体上是一致的。所以你有时候看到一个结构体struct Point有两个存储属性var x: Double,var y: Double ,却没有任何构造器,但是他们既不符合类中讨论的第一种初始化方法(即赋默认值)。那么它们违背了语法规则吗?其实不是的,如果结构体没有自定义的构造器,Swift隐式创建了一个init(x:y:)的构造器其内容就相当于self.x = x,self.y = y。如果你手动给结构体加个空内容的构造器init(){ },编译器就报Return from initializer without initializing all stored properties的错误。也就是说,如果你创建了你自己定义的构造器,Swift就默默地帮你把memberwise构造器去掉了,而你自己定义的构造器又没有对存储属性初始化,那么这违背了语法规则。但是如果你想同时拥有这两个构造器(memberwise构造器和自定义构造器),你可以把自定义的构造写到Extension Point{ // custom initializer }里面进去。

构造器的继承

designated构造器

designated构造器在Swift中很常见,顾名思义这个构造器就是你类中所有构造器的“原型”。在这个构造器中只调用父类的designated构造器或者不调用其他任何构造器称为designated构造器。每个类都必须至少有一个designated构造器,但是你会看到有些情况看不见类中声明designated构造器,那是因为它是一个子类,如果不写任何designated构造器,将会自动继承父类所有designated构造器。我们将在下面的自动构造器继承中详细讲到。

init(parameters){
        // statments
    }

convenience构造器

convenience构造器是第二种构造器。它主要是横向代理,就是说在convenience构造器中一定存在也只能存在该类的一个构造器通常用self.init(parameters)调用该类的一个构造器。当然convenience构造器不是必要的。

convenience init(parameters){
        // 调用该类中的一个构造器
        self.init(parameters)

        // customize properties
    }

类的构造器代理规则

  • 规则1:子类中的designated构造器必须调用最近一级父类的desigated构造器
  • 规则2:convenience构造器必须在同一个类中调用其他一个构造器
  • 规则3:convenience构造器通过调用链(代理链)调用一个designated构造器

总结下也就说

  • desingated构造器必须一直向上代理(即调用最近一级父类的designated构造器)
  • convenience构造器必须横向代理,且代理终点为一个desingated构造器

下面这幅图(引用自苹果官方文档原图)就表明了这两点

SubClass的convenience构造器调用了第二个designated构造器(符合规则2),第二个designated的调用了SuperClass的designated构造器(符合规则1),这表明了convenience构造器最终调用的是designatedg构造器(符合规则3)。同理其他的构造器调用亦是如此

下面再来一幅图(引用自苹果官方文档原图)

Two-Phase初始化

第一阶段类中每个存储属性必须有初始值,一旦每个储值属性的初始状态被确定了,第二阶段就开始了。第二阶段就是在新的实例可用之前对初始值的修改阶段。利用两阶段初始化可以让初始化安全,防止属性值在初始化完成之前被访问,以及属性值被另外的构造器设置为不同的值。这和OC差不多,唯一区别就是OC在第一阶段初始的默认值只能是0或者是nil

初始化有安全检查机制

  • 安全检查1:一个designated构造器在向上代理之前必须初始化该类中定义的所有存储属性
  • 安全检查2:一个designated构造器必须先向上代理调用一个父类的构造器,在修改父类的属性值之前。如果不这样,那么你修改的属性值,会被父类的构造器初始化属性的时候覆盖掉。
  • 安全检查3:一个convenience构造器在对本类任何属性操作之前(包括父类的属性以及本类定义的属性)必须调用另外一个本类中的构造器。如果不是这样,那么修改完的属性很可能就被本类中的构造器初始化属性的时候覆盖了。
  • 安全检查4:一个构造器不能调用实例方法,读取任何实例属性的值,或者用作为一个值指向self知道第一阶段初始化结束

初始化两个阶段

第一阶段:

  • 一个designated或者convenience构造器在类中被调用
  • 类的新实例向系统申请内存空间,但是内存还没有被初始化
  • 该类的一个designated构造器确认所有在该类中定义的存储属性已经被初始化。这些属性的内存被初始化。
  • designated的构造器告知父类构造器对父类自己的属性执行相同的操作
  • 一直代理到类继承链的最高级
  • 一旦到达类继承链的最高级,并且链上的最终类确认了它所有的存储属性已经初始化完毕,则实例内存被完全初始化,然后第一阶段到此就完成了。

下面这幅图是第一阶段

第二阶段:

  • 从继承链的最顶端开始每个designated的构造器可以修改实例中的属性值,此时构造器也可以访问self指针了,可以调用实例方法等等。
  • 最后在继承链上的任何convenience构造器可以用self指针来修改实例

下面这幅图是第二阶段

构造器的继承和重写

Swift和绝大多数面向对象的语言不一样,它不会默认继承所有父类的构造器。也就是说你用类初始化实例的时候,只能访问到该子类构造器(除了自动构造器继承的情况)。这是因为防止用父类的构造方法来创建一个比父类多出一些属性的子类。然而如果调用父类构造方法,就不能初始化子类中的存储属性了,这样就违背了Swift的安全机制。

注意:父类中的构造器在某些特殊情况也会被自动继承,详细内容在后面的自动构造器继承会讲到

如果你在子类中写了一个构造器,并且它与父类某个designated构造器的名字相同,那么你必须用override关键字修饰init构造器。这里还得注意,就算你父类没有任何显式的构造器(但是Swift会默认给你一个init(){ // 空 }构造器),子类在重写init() { // customize }的时候也是要加修饰词override的,就是这样override init() { // customize }

注意:就算你在子类中写的是convenience类型的构造器,如果名字和父类的某个designated构造器,也必须加关键字override修饰。

还有一种情况就是如果你子类中一个构造器的名字和父类的某个convenience构造器相同,又因为父类中的convenience构造器是没办法被子类直接调用的(这个在上面类的构造器代理规则中讲到过)。因此,严格地说,子类的convenience构造器不提供对父类convenience的重写,也就不需要用override修饰了。这个构造器就相当于新的自定义构造器,不和父类产生任何联系。

好吧讲了这么多理论估计你们都晕乎乎的,来讲点实际的例子把。

class Vehicle {
        var numberOfWheels = 0
        var description: String {
            return "\(numberOfWheels)wheel(s)"
        }
    }

这是一个基类。numberOfWheels有了默认的初始值,也算是初始化的一种方式。description是一个计算属性,计算属性不需要初始化。虽然看到这个类没有任何的构造器,但是前面提到过默认构造器,就是一个空的构造器init() { //空 } 下面我们来创建一个实例

let vehicle = Vehicle()
    println("Vehicle: \(vehicle.decription)")

这个时候控制台打印Vehicle: 0 wheel(s)

下面我们来创建一个子类Bicycle

class Bicycle: Vehicle{
        override init(){
            super.init()
            numberOfWheels = 2
        }
    }

由于Bicycle类继承Vehicle类,定义了一个init()designated构造器,这个构造器名字和Vehicle类中的默认构造器相同,因此需要重写override。重写中,首先调用父类的super.init(),然后在修改父类属性的值。这符合Two-Phase初始化规则,在修改父类属性之前,先调用父类的designated的构造器,以保证父类的所有属性被初始化,并且防止修改的值被构造器的初始化过程给覆盖了。

下面我们来创建Bicycle子类的实例

let bicycle = Bicycle()
    println("Bicycle: \(bicycle.description)")

控制台打印:Bicycle: 2 wheel(s)
分析:由于子类修改了父类中的numberOfWhiles属性,因此打印结果是2

注意:子类可以在合适的位置(具体是什么位置看Two-Phase初始化中介绍)对父类的属性在初始化的时候修改,但是如果属性是常量let声明的,这是不允许的,因为let属性常量一旦被初始化赋值之后,就再也不能改了。

自动构造器继承

上面很多地方我们提到了自动构造器继承,那么这东西到底是什么呢?一般情况下,子类如果定义了自己的designated的构造器,父类的构造器是不会被继承的。但是,如果子类没有定义任何自己的构造器,那么子类就会继承父类的构造器。
假设你给你子类中任何新的属性赋予了默认值,那么一下两条规则将会被适用:

  • 规则1:如果你的子类没有定义任何designated构造器,那么它会自动继承父类的所有designated构造器。
  • 规则2:如果你子类提供实现了所有父类的designated的构造器(无论是通过规则1实现的,还是提供自定义手动实现的)都会继承父类所有的convenience构造器。

即使你在子类中添加了其他convenience构造器,这些规则也是适用的。

注意上面所说的提供自定义手动实现,也可以包括子类中用convenience构造器覆盖父类的designated构造器。比如说父类有个init(original:)构造器,子类用override convenience init(original:)也是可以使之满足规则2的

这个是有点抽象,我们来举个例子把
先定义一个基类Food

class Food {
        var name: String
        init(name: String) {
            self.name = name
        }
        convenience init() {
            self.name(name: "[Unnamed]")
        }
    }

这里init(name:)构造器是Food类的designated构造器,init()构造器是Food类的convenience构造器,里面调用了该类的init(name:)designated构造器

现在利用designated构造器创建一个实例

let namedMeat = Food(name: "Bacon")

现在创建了一个实例叫namedMeat,它的name属性值为"Bacon"

现在利用convenience构造器创建一个实例

let mysteryMeat = Food()

现在创建了一个实例叫mysteryMeat,它的name属性值为"[Unnamed]"

现在我们创建一个RecipeIngredient类,继承Food类

 class RecipseIngredient: Foof { var quantity: Int init(name: String,quantity: Int){ self.quantity = quantity super.init(name: name) // customize superclass properties if needed } override convenience init(name: String) { self.init(name: name,quantity: 1) } }

这里我们定义了RecipeIngredient类,它继承了Food类。其中init(name:quantity)是它的designated构造器。init(name:)是convenience构造器,并且是重写了父类(Food类)的init(name:)designated构造器

构造器继承关系图

接下来我会结合Two-Phase初始化机制来讲解这个RecipeIngredient类的初始化过程。

首先明确的是,RecipeIngredient类只有一个designated构造器叫做init(name: String,quantity: Int)。当这个类初始化实例的时候后,如果使用这个构造器。首先,要满足类的代理规则1,在designated构造器中必须调用最近父类的designated的构造器。但是要先满足安全检查第一个步骤,在调用父类designated构造器之前,必须先初始化改类中定义的所有存储属性。所以对quantity属性进行初始化self.qunatity = qunatity。初始化完成之后,调用父类(Food类)的super.init(name:)构造器进行对父类属性的初始化,由于该例子中Food类已经是构造(代理)链的最上级了,所以在这里停止向上代理。等Food类属性初始化完成之后,返回到RecipeIngredient类中(就是执行完了super.init(name:)),如果你想再对父类属性进行修改,那么可以在这个语句后面自定义修改。这符合安全检查机制3.

现在我们来用不同的构造器创建几个实例

let oneMysteryItem = RecipeIngredient()
    let oneBacon = RecipeIngredient(name: "Bacon")
    let sixEggs = RecipeIngredient(name: "Eggs",quantity: 6)

现在实例oneMysteryItem调用的是init()构造器。但是我们又没有在RecipeIngredient类中看到定义,而且有了designated构造器之后Swift不会给你默认创建init()构造器了。其实init()来自父类Food。因为子类实现了父类所有的designated构造器(即init(name: String)构造器),因此根据自动构造器继承的第二条规则,子类将继承父类的所有convenience构造器,也就是init()构造器。这个init()和内容和父类基本一致,只不过内部的self.init(name:String)调用的是RecipeIngredient类的,而不是父类的。也就是说现在oneMysteryItemquantity属性值为1,name属性值为“[Unnamed]”。而实例oneBacon调用的是RecipeIngredient的init(name: String)convenience构造器。所以它的name属性的值为"Bacon"quantity属性的值为1。sixEggs实例调用的是RecipeIngredient的init(name: String,quantity: Int)designated构造器,因此name属性的值为"Eggs"quantity属性的值为6

再来一个更加特殊的类ShopListItem

class ShoppingListItem: RecipeIngredient {
        var purchased = false
        var description: String{
            var output = "\(quantity)x \(name)"
            output += purchased ? "√" : "×"
            return output
        }
    }

你可以看到这个类没有任何构造器对purchased的初始化,而是给予了一个默认值false因为一件物品默认是没有购买的。
由于它没有提供任何构造器,于是符合自动构造器继承的第一条规则,它将自动继承父类RecipeIngredient的所有designated的构造器,又因为如此,也就说子类是吸纳了父类的所有desigated的构造器,自动构造器继承的第二条规则也将适用,也就是自动继承父类的所有convenience构造器。下面就是整个继承图。

现在我们创建个实例数组

var breakfastList = [
        ShoppingListItem(),ShoppingListItem(name: "Bacon"),ShoppingListItem(name: "Eggs",quantity: 6)
    ]
    breakfastList[0].name = "Orange juice"
    breakfastList[0].purchased = true
    for item in breakfastList {
        println(item.decription)
    }

控制台输出结果

1 x Orange juice √

1 x Bacon ×

6 x Eggs ×

这里第一个item是ShoppingListItem,它调用的是init()构造器是从Food一直继承下来的,然而在RecipeIngredient层,将它初始化的内容改为了self.init(name: name,quantity: 1)。最终到了ShoppingListItem这个类中,初始化的内容依然是self.init(name: name,quantity: 1)这个,只不过这个self不指向RecipeIngredient,而是指向ShoppingListItem。因此初始化完毕之后第一个item的name属性的值为“Unnamed”quantity属性的值为1purchased的属性的值为了false。但是在后来breakfastList[0]把属性name值改为了Orange juice,把purchased的属性的值改为了true。所以最后的decription1 x Orange juice √ 而其他初始化内容应该不难理解,我这里也不再重复叙述了。

Swift 构造器探究的更多相关文章

  1. three.js模拟实现太阳系行星体系功能

    这篇文章主要介绍了three.js模拟实现太阳系行星体系功能,本文通过实例代码给大家介绍的非常详细,具有一定的参考借鉴价值,需要的朋友可以参考下

  2. HTML5 WebSocket实现点对点聊天的示例代码

    这篇文章主要介绍了HTML5 WebSocket实现点对点聊天的示例代码的相关资料,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧

  3. HTML5页面无缝闪开的问题及解决方案

    这篇文章主要介绍了HTML5页面无缝闪开方案,本文通过实例代码给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下

  4. ios – 在Swift的UIView中找到UILabel

    我正在尝试在我的UIViewControllers的超级视图中找到我的UILabels.这是我的代码:这是在Objective-C中推荐的方式,但是在Swift中我只得到UIViews和CALayer.我肯定在提供给这个方法的视图中有UILabel.我错过了什么?我的UIViewController中的调用:解决方法使用函数式编程概念可以更轻松地实现这一目标.

  5. ios – 在Swift中将输入字段字符串转换为Int

    所以我非常擅长制作APP广告Swift,我试图在文本字段中做一些非常简单的输入,取值,然后将它们用作Int进行某些计算.但是’vardistance’有些东西不正确它是导致错误的最后一行代码.它说致命错误:无法解开Optional.None解决方法在你的例子中,距离是一个Int?否则称为可选的Int..toInt()返回Int?因为从String到Int的转换可能失败.请参阅以下示例:

  6. 如何在iOS中检测文本(字符串)语言?

    例如,给定以下字符串:我想检测每个声明的字符串中使用的语言.让我们假设已实现函数的签名是:如果没有检测到语言,则返回可选字符串.因此,适当的结果将是:有一个简单的方法来实现它吗?

  7. xamarin – 崩溃在AccountStore.Create().保存(e.Account,“);

    在Xamarin.Forms示例TodoAwsAuth中https://developer.xamarin.com/guides/xamarin-forms/web-services/authentication/oauth/成功登录后,在aOnAuthenticationCompleted事件中,应用程序在尝试保存到Xamarin.Auth时崩溃错误说不能对钥匙串说期待着寻求帮助.解决方法看看你

  8. ios – 将视频分享到Facebook

    我正在编写一个简单的测试应用程序,用于将视频从iOS上传到Facebook.由于FacebookSDK的所有文档都在Objective-C中,因此我发现很难在线找到有关如何使用Swift执行此操作的示例/教程.到目前为止我有这个在我的UI上放置一个共享按钮,但它看起来已禁用,从我读到的这是因为没有内容设置,但我看不出这是怎么可能的.我的getVideoURL()函数返回一个NSURL,它肯定包含视

  9. xcode – 错误“线程1:断点2.1”

    我正在研究RESTAPI管理器.这是一个错误,我无法解决它.我得到的错误在下面突出显示.当我打电话给这个班级获取资源时:我评论的线打印:Thread1:breakpoint2.1我需要修复错误的建议.任何建议都非常感谢解决方法您可能在不注意的情况下意外设置了断点.单击并拖动代表断路器外部断点的蓝色刻度线以将其擦除.

  10. ios – 更改导航栏标题swift中的字符间距

    类型的值有人可以帮我这个或建议一种不同的方式来改变swift中导航栏标题中的字符间距吗?解决方法您无法直接设置属性字符串.你可以通过替换titleView来做一个技巧

随机推荐

  1. Swift UITextField,UITextView,UISegmentedControl,UISwitch

    下面我们通过一个demo来简单的实现下这些控件的功能.首先,我们拖将这几个控件拖到storyboard,并关联上相应的属性和动作.如图:关联上属性和动作后,看看实现的代码:

  2. swift UISlider,UIStepper

    我们用两个label来显示slider和stepper的值.再用张图片来显示改变stepper值的效果.首先,这三个控件需要全局变量声明如下然后,我们对所有的控件做个简单的布局:最后,当slider的值改变时,我们用一个label来显示值的变化,同样,用另一个label来显示stepper值的变化,并改变图片的大小:实现效果如下:

  3. preferredFontForTextStyle字体设置之更改

    即:

  4. Swift没有异常处理,遇到功能性错误怎么办?

    本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容,请发送邮件至dio@foxmail.com举报,一经查实,本站将立刻删除。

  5. 字典实战和UIKit初探

    ios中数组和字典的应用Applicationschedule类别子项类别名称优先级数据包contactsentertainment接触UIKit学习用Swift调用CocoaTouchimportUIKitletcolors=[]varbackView=UIView(frame:CGRectMake(0.0,0.0,320.0,CGFloat(colors.count*50)))backView

  6. swift语言IOS8开发战记21 Core Data2

    上一话中我们简单地介绍了一些coredata的基本知识,这一话我们通过编程来实现coredata的使用。还记得我们在coredata中定义的那个Model么,上面这段代码会加载这个Model。定义完方法之后,我们对coredata的准备都已经完成了。最后强调一点,coredata并不是数据库,它只是一个框架,协助我们进行数据库操作,它并不关心我们把数据存到哪里。

  7. swift语言IOS8开发战记22 Core Data3

    上一话我们定义了与coredata有关的变量和方法,做足了准备工作,这一话我们来试试能不能成功。首先打开上一话中生成的Info类,在其中引用头文件的地方添加一个@objc,不然后面会报错,我也不知道为什么。

  8. swift实战小程序1天气预报

    在有一定swift基础的情况下,让我们来做一些小程序练练手,今天来试试做一个简单地天气预报。然后在btnpressed方法中依旧增加loadWeather方法.在loadWeather方法中加上信息的显示语句:运行一下看看效果,如图:虽然显示出来了,但是我们的text是可编辑状态的,在storyboard中勾选Editable,再次运行:大功告成,而且现在每次单击按钮,就会重新请求天气情况,大家也来试试吧。

  9. 【iOS学习01】swift ? and !  的学习

    如果不初始化就会报错。

  10. swift语言IOS8开发战记23 Core Data4

    接着我们需要把我们的Rest类变成一个被coredata管理的类,点开Rest类,作如下修改:关键字@NSManaged的作用是与实体中对应的属性通信,BinaryData对应的类型是NSData,CoreData没有布尔属性,只能用0和1来区分。进行如下操作,输入类名:建立好之后因为我们之前写的代码有些地方并不适用于coredata,所以编译器会报错,现在来一一解决。

返回
顶部