原文:https://www.raywenderlich.com/119922/swift-tutorial-initialization-part-1
有一些事情天生是非常棒的例如:火箭, 火星探测, Swift的初始化.本教程通过包含最全的内容让你学习到最棒的Swift初始化!
当你创建某一个名称的类型实例时,Swift的初始化在做些什么呢:
letnumber=Float()
初始化的过程是用来初始化所有的各个类型存储属性的初始值,例如类类型、结构体类型、枚举类型。 正是因为Swift是建立在类型安全的基础之上,初始化变得非常复杂和重要。初始化的过程中有很多的规则和注意地方,包括一些不明显的地方。
通过这两部分的教程, 你将学习到设计自己的Swift设计初始化方法的来龙去脉。 在第一部分中,你将开始学习包括结构体初始化等基本过程, 在第二部分中,你将继续学习有关类的初始化过程。
在进入正题之前, 你应该对Swift中的基本初始化方法有一个基本的了解。能够知道例如可选类型,抛出异常和异常捕获,声明存储属性值,同时,确保你已经安装了Xcode7.2版本或者更新的版本。
如果你需要补充基本知识, 或者你仅仅刚开始学习Swift,查看我们的Swift Apprentice书籍或者我们更多的Swift intro tutorials。
开始学习
一起开始学习: 你在NASA的第一天新工作是启动软件引擎(go you!). 你的任务就是设计数据模型来启动火星探测流程的第一步。当然, 第一件事情就是说服团队使用Swift…
打开Xcode 和创建一个名称为BlastOffplayground。你可以选择一个平台, 本教程的所有代码都与平台无关,仅仅依赖Foundation框架。
贯穿整个教程, 请记住一个黄金规则:你不可以在没有完全初始化完成之前使用实例调用,使用实例包括访问属性, 设置属性 和调用方法. 在第一部分的代码没有特别说明的话一切适用于结构体中。
默认的初始化
为了开始建立启动流程, 在自己的playground中声明一个叫RocketConfiguration的结构体:
structRocketConfiguration{}
在关闭括号后定义一个RocketConfiguration类型的实例,并初始化一个名称为athena9Heavy:的常量
letathena9Heavy=RocketConfiguration()
使用默认的初始化方法实例化athena9Heavy. 在这个默认的初始化方法中,类型的名称后边是空的圆括号。当你的属性中没有存储属性或者所有的存储属性都已经有了默认值时,你可以使用默认的是初始化方法。这对于结构体和类来说都是可以适用的。
在结构体中添加三个存储属性:
letname:String="Athena 9 Heavy"letnumberOfFirstStageCores:Int=3letnumberOfSecondStageCores:Int=1
可以注意到,默认的初始化可以正常工作。这段代码依然可以继续运行是因为所有的存储属性已经有默认值了。这就意味着如果你已经提供了属性默认值,那么久意味着默认的初始化不需要做很多的工作!
什么是可选类型呢?在结构体中定义一个可变的存储属性numberOfStageReuseLandingLegs:
varnumberOfStageReuseLandingLegs:Int?
在NASA程序中,有一些火箭是可以重复使用的,而一些是不可以重复使用的。 这就是numberOfStageReuseLandingLegss是可选类型的原因。默认的初始化可以运行是因为可选的存储属性是有默认值。但是,这个默认值和常量不是同的情况。
成员的初始化
火箭的组成通常有几个部分组成,接下来就是模型的事情了。 声明一个新的结构体名称为RocketStageConfiguration在playground的地底部:
structRocketStageConfiguration{letpropellantMass:DoubleletliquidOxygenMass:DoubleletnominalBurnTime:Int}
这个时候, 你已经有三个存储属性propellantMass,liquidOxygenMassandnominalBurnTime并且它们没有默认值。
创建火箭的RocketStageConfiguration第一阶段:
letstageOneConfiguration=RocketStageConfiguration(propellantMass:119.1, liquidOxygenMass:276.0, nominalBurnTime:180)
RocketStageConfiguration‘s 的存储属性都是没有默认值。 而且也没有RocketStageConfiguration初始化实现.。为什么没有编译器错误呢? Swift的结构体(仅仅结构体) 自动生成成员初始化构造器。这就意味着你得到了一个现成的初始值为没有默认值的存储属性。 这是非常方便的,但是会有一些陷阱。
想象一下,你的开发负责人会在你提交此代码时告诉你最好所有属性进行按照字母顺序排列。
更新下RocketStageConfiguration中所有存储属性按照字母顺序排列:
structRocketStageConfiguration{letliquidOxygenMass:DoubleletnominalBurnTime:IntletpropellantMass:Double}
会发生什么呢?stageOneConfiguaration的初始化构造器将不在有效, 因为自动生成的成员初始化的参数将会按照重新排序之后的属性列表方式。 小心,因为重新排序之后的结构体属性,你将会破坏实例的初始化构造器。感谢编译器能捕捉到这个错误, 但是这个是绝对需要注意点的地方。
为了能够重新让playground能够运行,撤销存储属性的重新排序:
structRocketStageConfiguration{letpropellantMass:DoubleletliquidOxygenMass:DoubleletnominalBurnTime:Int}
火箭的燃烧有180s, 因此他不是在每次通过实例化后配置燃烧时间的。设置nominalBurnTime‘s 的默认属性值为180:
letnominalBurnTime:Int=180
现在编译器存在另外的错误:
编译器错误是因为成员初始化构造器仅仅提供了没有默认值的存储属性。在这种情况下, 因为燃烧时间已经提供了默认值,所以成员逐一初始化构造器只需要提供推进剂质量和液态氧质量属性。
移除nominalBurnTime‘s 默认值也不会造成编译器错误。
letnominalBurnTime:Int
下一步, 添加一个初始化构造器去给燃烧时间提供默认值:
init(propellantMass:Double, liquidOxygenMass:Double){self.propellantMass=propellantMassself.liquidOxygenMass=liquidOxygenMassself.nominalBurnTime=180}
注意同样会产生stageOneConfigurationd的编译器错误!
等等,它不能工作嘛?所有所做的都只是提供了另一种初始化, 但是原始的stageOneConfiguration初始化构造器是可以工作的因为它使用了自动成员构造器。这个时候棘手的问题是:如果这个结构体没有定义初始化构造器,你将仅仅得到成员构造器。只要你定义了初始化构造器, 你将失去自动成员构造器.
换句话说, Swift 将帮助你创建。但是只要你定义了自己的初始化构造器, 它将不再为你提供默认的成员初始化构造器。
从stageOneConfiguration‘s的初始化构造器中移除这个nominalBurnTime参数:
letstageOneConfiguration=RocketStageConfiguration(propellantMass:119.1, liquidOxygenMass:276.0)
又是没有问题了! :]
但是如果你仍然需要自动成员初始化构造器呢? 你当然可以自己去实现这样的构造器,但是需要做挺多事情的。 相反,在你实例化一个实例之前,可以扩展一个初始化构造器。
你的结构体将有两部分: 主要的声明,和两个参数的初始化构造器扩展:
structRocketStageConfiguration{letpropellantMass:DoubleletliquidOxygenMass:DoubleletnominalBurnTime:Int}extension RocketStageConfiguration{init(propellantMass:Double, liquidOxygenMass:Double){self.propellantMass=propellantMassself.liquidOxygenMass=liquidOxygenMassself.nominalBurnTime=180}}
注意到stageOneConfiguration继续可以通过两个参数来初始化. 现在再添加一个nominalBurnTime参数到stageOneConfiguration‘s 初始化构造器中:
letstageOneConfiguration=RocketStageConfiguration(propellantMass:119.1, liquidOxygenMass:276.0, nominalBurnTime:180)
这个工作太棒啦!如果这个主要的结构体不包含哪些初始化构造器, Swift 将仍然可以自动生成成员初始化构造器.之后,你可以通过扩展来添加你自定义的初始化构造器。
实现自定义的初始化构造器
天气在发射火箭中扮演了关键角色, 因此你需要在模型中添加一个地址。声明一个结构体名称Weather如下所示:
structWeather{lettemperatureCelsius:DoubleletwindSpeedKilometersPerHour:Double}
这个结构体中包括存储属性摄氏度,每小时公里的温度风速。
实现天气的自定义初始化。添加存储属性如下代码:
init(temperatureFahrenheit:Double, windSpeedMilesPerHour:Double){self.temperatureCelsius=(temperatureFahrenheit-32)/1.8self.windSpeedKilometersPerHour=windSpeedMilesPerHour*1.609344}
自定义一个初始化构造器是和定义一个方法非常相似的, 因为一个初始化参数列表的行为是完全一样的方法。例如, 你可以给初初始化构造器中的参数设置默认值。
改变初始化构造器的定义:
init(temperatureFahrenheit:Double=72, windSpeedMilesPerHour:Double=5){...
现在如果你调用没有参数的初始化构造器, 你将会得到的属性具有默认值。在文件结束地方, 创建一个天气实例,并检查它的值:
letcurrentWeather=Weather()currentWeather.temperatureCelsiuscurrentWeather.windSpeedKilometersPerHour
很酷吧? 默认的初始化构造器使用了自定义初始化中提供的默认值。自定义初始化构造器实现将相同的值转化到等价并存储。当你在playground中查看存储属性的值时,你讲会得到的值是degrees Celsius (22.2222) and kilometers per hour (8.047)。
一个初始化构造器必须设定给灭一个存储属性,没有默认值,否则你将得到一个编译错误。记住,可选类型属性将会有一个默认的值nil.
接下来, 使用自己自定义的初始化构造器利用新值去修改currentWeatherd的值:
letcurrentWeather=Weather(temperatureFahrenheit:87, windSpeedMilesPerHour:2)
正如你看到的, 自定义的初始化值在使用中和默认的值效果是一样的。 The playground 侧边已经显示了 30.556 degrees and 3.219 km/h.
这就是如何实并调用自定义初始化构造器的过程。你的天气结构体非常有助于你的火星探测工作。好样的!
Mars: not just forMatt Damon
避免重复设定初始化
是时间去考虑一下火箭的制导。 火箭需要高效的制导系统来保证他们的飞行直线。声明一个新的名为GuidanceSensorStatus如下代码:
structGuidanceSensorStatus{varcurrentZAngularVelocityRadiansPerMinute:DoubleletinitialZAngularVelocityRadiansPerMinute:DoublevarneedsCorrection:Boolinit(zAngularVelocityDegreesPerMinute:Double, needsCorrection:Bool){letradiansPerMinute=zAngularVelocityDegreesPerMinute*0.01745329251994self.currentZAngularVelocityRadiansPerMinute=radiansPerMinuteself.initialZAngularVelocityRadiansPerMinute=radiansPerMinuteself.needsCorrection=needsCorrection}}
这个结构体适合初始化Z轴的火箭的初始化角速度。这个结构也保证了火箭的校正轨迹,使得继续留在目标轨迹。
自定义的初始化构造器拥有重要的逻辑: 如何将每分钟的度数转化为每分钟的弧度。初始化还设置角速度保持为参考的初始值。
他们告诉你一个新的版本的火箭需要将needsCorrection的Int类型替换为Bool. 这个工程师说的正整数理解为真, 而0和负数理解为假。然而你的团队还没做好准备去修改代码, 因为这种变化是未来功能的一部分。所有你怎么能适应工程师的不断变化二十中保持您的结构的定义完成?
无果 — 添加如下自定义初始化构造器到第一个初始化构造器下边:
init(zAngularVelocityDegreesPerMinute:Double, needsCorrection:Int){letradiansPerMinute=zAngularVelocityDegreesPerMinute*0.01745329251994self.currentZAngularVelocityRadiansPerMinute=radiansPerMinuteself.initialZAngularVelocityRadiansPerMinute=radiansPerMinuteself.needsCorrection=(needsCorrection >0)}
这个新的初始化构造器将最后一个参数Int替换为了Bool类型。 然而, 这个needsCorrection存储属性仍然是一个Bool类型 , 和 正确按照他们设置的规则。
在写了这个代码之后, 你会发现必须要有一个好办法。还有初始化代码的其余部分是非常的重复! 如果在一个弧度转换中出现计算错误,你将需要在多个地方修复这个错误。这就体现了initializer delegation构造器的用处了。
用下面的初始化构造器替换:
init(zAngularVelocityDegreesPerMinute:Double, needsCorrection:Int){self.init(zAngularVelocityDegreesPerMinute:zAngularVelocityDegreesPerMinute, needsCorrection:(needsCorrection >0))}
这就是delegating initializerand, 正是因为这样, 它代理的其他的初始化构造器.为了完成指定代理, 仅仅调用自己的其他初始化构造器。
当你想要提供备用的初始化构造器的参数列表,但你又不希望重复逻辑,这样时候自定义代理的初始化就非常有用了。同时, 采用委托初始化有助于减少您必须编写的代码数量。
为了测试初始化构造器, 实例化第一个实例名称为guidanceStatus:
letguidanceStatus=GuidanceSensorStatus(zAngularVelocityDegreesPerMinute:2.2, needsCorrection:0)guidanceStatus.currentZAngularVelocityRadiansPerMinute// 0.038guidanceStatus.needsCorrection// false
playground 可以编译和运行, 和guidanceStatus的两个值将在侧边栏中。
还有一件事儿 — 你被要求提供另外的默认needsCorrection为假的初始化构造器。这应该和创建新的代理初始化一样简单以及在代理初始化之前完成needsCorrection属性被设置。尝试添加下边的初始化方法到结构体中,它将不会编译通过。
init(zAngularVelocityDegreesPerMinute:Double){self.needsCorrection=falseself.init(zAngularVelocityDegreesPerMinute:zAngularVelocityDegreesPerMinute, needsCorrection:self.needsCorrection)}
编译器失败是因为代理初始化不能真正初始化任何属性。 有一个很好的理由是:你所委托的初始化将覆盖你已经设置的值这是不安全的。 一个委托初始化能做的唯一的事情就是操作了传递到另一个初始化值。
如此之后, 移除新的初始化构造方法并 给needsCorrection参数默认值为false:
init(zAngularVelocityDegreesPerMinute:Double, needsCorrection:Bool=false){
UpdateguidanceStatus‘s initialization by removing theneedsCorrectionargument:
letguidanceStatus=GuidanceSensorStatus(zAngularVelocityDegreesPerMinute:2.2)guidanceStatus.currentZAngularVelocityRadiansPerMinute// 0.038guidanceStatus.needsCorrection// false
介绍Two-Phase Initialization 的阶段过程
至今,你的初始化代码已经被设置为您的属性和调用其他的初始化方法。这是初始化的第一阶段, 但实际上分为两个阶段来初始化Swift。
阶段一 从初始化构造器开始到所有存储属性都已经被赋值。 剩下的初始化执行是在第二阶段。在第一阶段初始化完成之前不能使用实例, 但是你可以在第二阶段中使用实例。如果你有委托初始化链,阶段一跨越调用堆栈向上到非委派初始化。
Two-Phase Initialization 开始工作
现在你已经理解了初始化的两个阶段, 一起应用到项目中。每一个火箭发动机都有一个燃烧室,其中燃料被喷射和氧化剂来创建一个可受控的爆炸方式推动火箭。设置这些参数是第一阶段的一部分,为升空做好准备。
实现CombustionChamberStatus结构体的 Swift’s two-phase 初始化行为。确保看到Xcode测试环境下打印出来的状态。
structCombustionChamberStatus{vartemperatureKelvin:DoublevarpressureKiloPascals:Doubleinit(temperatureKelvin:Double, pressureKiloPascals:Double){print("Phase 1 init")self.temperatureKelvin=temperatureKelvinself.pressureKiloPascals=pressureKiloPascalsprint("CombustionChamberStatus fully initialized")print("Phase 2 init")}init(temperatureCelsius:Double, pressureAtmospheric:Double){print("Phase 1 delegating init")lettemperatureKelvin=temperatureCelsius+273.15letpressureKiloPascals=pressureAtmospheric*101.325self.init(temperatureKelvin:temperatureKelvin, pressureKiloPascals:pressureKiloPascals)print("Phase 2 delegating init")}}CombustionChamberStatus(temperatureCelsius:32, pressureAtmospheric:0.96)
你可以看到如下的打印输出调试区域:
Phase1delegatinginitPhase1initCombustionChamberStatus fully initializedPhase2initPhase2delegatinginit
正如你看到的,第一阶段调用代理初始化(temperatureCelsius:pressureAtmospheric:)d的过程self是不能被使用。 第一阶段过后self.pressureKiloPascals被赋值。 初始化在不同的阶段扮演了不同的作用。
是编译器更加聪明了吗? 它知道如何去执行这些规则。 起初这些规则看起来更像是干扰,但请记住,他们提供了非常强大的安全性。
如果事情出错呢?
你已经告诉了发射程序将完全自主进行, 而该程序也将执行大量的测试,以确保所有的系统都能很好的进行。如果初始化中出现无效的值, 这个启动程序是可以知道和反馈的。
在Swift中有两种办法来迅速初始化失败: 使用可失败构造器, 和初始化抛出。 初始化失败是有很多原因的,包括无效的输入, 缺少系统资源, 和可能来自网络故障。
使用可失败构造器
正常的初始化和可失败初始化是不同的。 可失败初始化返回nil表示初始化失败。这个是非常有用的—让我们把它应用到我们的火箭数据模型中。
每一个火箭都需要两个大型坦克; 一个拥有燃料, 另一个需要氧化剂。实现一个叫TankStatus的结构体,如下所示:
structTankStatus{varcurrentVolume:DoublevarcurrentLiquidType:String?init(currentVolume:Double, currentLiquidType:String?){self.currentVolume=currentVolumeself.currentLiquidType=currentLiquidType}}lettankStatus=TankStatus(currentVolume:0.0, currentLiquidType:nil)
这个没有什么错误,仅仅是不会展示是否初始化失败。 如果你传一个负数进去会发生什么呢?如果你传一个没有类型的正数进去又会怎样呢? 这些都是错误情形。 我们使用可失败初始化会怎么样呢?
改变TankStatus‘s 的初始化为 afailable initializer在init初始化中添加个?:
init?(currentVolume:Double, currentLiquidType:String?){
Option-click ontankStatusk可以注意到它的返回值类型是optionalTankStatus.
更新tankStatus‘s 初始化为如下:
iflettankStatus=TankStatus(currentVolume:0.0, currentLiquidType:nil){print("Nice, tank status created.")// Printed!}else{print("Oh no, an initialization failure occurred.")}
实例化逻辑检查是否失败是通过评测返回的可选是否包含一个值或者没有值。
当然, 还有一些其他的事情: 初始化实际上没做检查无效值。 更新可失败初始化为如下:
init?(currentVolume:Double, currentLiquidType:String?){ifcurrentVolume <0{returnnil}ifcurrentVolume >0&¤tLiquidType==nil{returnnil}self.currentVolume=currentVolumeself.currentLiquidType=currentLiquidType}
一旦检测到一个无效的输入, 可失败初始化会返回nil. 你可以在任何时间处理结构体的可失败初始化返回nil。
要想查看实例失败的情况, 向tankStatus‘s 中传递一个无效的值:
iflettankStatus=TankStatus(currentVolume:-10.0, currentLiquidType:nil){
注意playground的打印, “Oh no, an initialization failure occurred.” 因为初始化失败,可失败初始化返回一个nil。
从初始化中抛出
当返回的是一个可选的空时,可失败初始化是非常好的。 对于更严重的错误,用初始化其他的方法来处理故障 。
你必须实现一个结构体: 代笔着宇航员. 从下面的代码开始:
structAstronaut{letname:Stringletage:Intinit(name:String, age:Int){self.name=nameself.age=age}}
该经理告诉你一个宇航员应该有他或她的名称属性是非空字符串,并且有一个年龄范围从18到70。
为了表示可能发生的错误,在实现之前添加以下的错误枚举Astronaut:
enumInvalidAstronautDataError:ErrorType{caseEmptyNamecaseInvalidAge}
枚举类型中涵盖了可能发生的所有情况,你可以在初始化实例的时候遇到。
接下来, 用下边的实现替换Astronaut初始化:
init(name:String, age:Int)throws{ifname.isEmpty{throw InvalidAstronautDataError.EmptyName}ifage <18|| age >70{throw InvalidAstronautDataError.InvalidAge}self.name=nameself.age=age}
需要注意的是, 初始化被标记throws可以让调用者知道可以抛出错误。
如果检测到一个无效的输入值 — 无论是名称的空字符串, 或在可接受的范围之外 — 初始化会抛出相应的错误。
尝试实例化一个新的宇航员:
letjohnny=try? Astronaut(name:"Johnny Cosmoseed", age:42)
这就是你如何处理旧的抛出方法或功能。抛出一个初始化行为就像抛出一个方法或者功能。 你也可以传递出是初始化错误, 并用do–catch处理错误。
为了看到这个初始化产生错误, 改变johnny‘s 年龄为 17:
letjohnny=try? Astronaut(name:"Johnny Cosmoseed", age:17)// nil
当你调用抛出初始化, 你需要写try关键字 — 或者try?ortry!— 以确定它能够抛出一个错误。在这种情况下,你可以使用try?因此,在错误的情况下返回的值是零。 注意johnny的值是怎么为nil.十七对应航天员太年轻,可悲的是. 祝福Johnny明年有更好的运气!
对于失败构造器或抛出构造器呢?
使用抛出初始化构造器和try方式看起来比使用可失败构造器要麻烦很多。 因此你应该使用哪一种呢?
考虑使用抛出构造器,可失败构造器仅仅表达了二进制的成功和失败情形。通过使用可失败构造器你不仅可以表示失败, 而且可以获取到某一种错误情况下的原因。另一个好处是调用代码可以传播有初始化构造器抛出的任何错误。
可失败构造器更加的简单, 因为你不需要去顶一个错误的类型和使用try?关键字去尝试避免所有额外的情况。
为什么Swift会存在可失败构造器呢? 因为第一版本中Swift没有包括函数抛出方法, 所以需要一种方法来管理可失败构造器。
如果有说的不对的地方欢迎指出来,大家一起学习,进步!