神刀安全网

Swift Tutorial: Initialization In Depth, Part 1/2

Swift Tutorial: Initialization In Depth, Part 1/2

Some things are inherently awesome: rockets, missions to Mars, initialization in Swift. This tutorial combines all three for a combo-platter of awesomeness in which you’ll get to learn about the power of initialization!

Initialization in Swift is about what happens when you create a new instance of a named type:

let number = Float()

Initialization is the time to manage the inital values of stored properties for named types: classes, structures, and enumerations. Because of the safety features built into Swift, initialization can be tricky. There are a lot of rules, some of which are not obvious.

By following this two-part tutorial, you will learn the ins and outs to designing initializers for your Swift types. In Part 1, you’ll begin with the basics including structure initialization, and in Part 2 you’ll move on to learning about class initialization.

Before getting started, you should be familiar with the basics of initialization in Swift and be comfortable with concepts such as optional types, throwing and handling errors, and declaring default stored property values. Also, make sure you have Xcode 7.2 or later installed.

If you need a refresher on the basics, or if you are just starting to learn Swift, check out our bookSwift Apprentice or our many Swift intro tutorials .

Getting Started

Let’s set the scene: it’s your first day on your new job as a launch software engineer at NASA (go you!). You’ve been tasked with designing the data model that will drive the launch sequence for the first manned mission to Mars, Mars Unum. Of course, the first thing you do is convince the team to use Swift. Then …

Open Xcode and create a new playground named BlastOff . You can select any platform, since the code in this tutorial is platform-agnostic and depends only on Foundation.

Throughout the tutorial, remember this one golden rule: You cannot use an instance until it is fully initialized. “Use” of an instance includes accessing properties, setting properties and calling methods. Everything in Part 1 applies specifically to structures unless otherwise specified.

Banking on the Default Initializer

To start modeling the launch sequence, declare a new structure named RocketConfiguration in your playground:

struct RocketConfiguration {   }

Below the closing curly brace of the definition for RocketConfiguration , initialize a constant instance named athena9Heavy :

let athena9Heavy = RocketConfiguration()

This uses a default initializer to instantiate athena9Heavy . In the default initializer, the name of the type is followed by empty parentheses. You can use default initializers when your types either don’t have any stored properties, or all of the type’s stored properties have default values. This holds true for both structures and classes.

Add the following three stored properties inside the struct definition:

let name: String = "Athena 9 Heavy" let numberOfFirstStageCores: Int = 3 let numberOfSecondStageCores: Int = 1

Notice how the default initializer still works. The code continues to run because all the stored properties have default values. That means the default initializer doesn’t have very much work to do since you’ve provided defaults!

What about optional types? Add a variable stored property named numberOfStageReuseLandingLegs to the struct definition:

var numberOfStageReuseLandingLegs: Int?

In our NASA scenario, some of the rockets are reusable, while others are not. That’s why numberOfStageReuseLandingLegs is an optional Int . The default initializer continues to run fine because optional stored property variables are initialized to nil by default. However, that’s not the case with constants.

Change numberOfStageReuseLandingLegs from a variable to a constant:

let numberOfStageReuseLandingLegs: Int?

Notice how the playground reports a compiler error:

Swift Tutorial: Initialization In Depth, Part 1/2

You won’t run into this often, since constant optionals are rarely needed. To fix the compiler error, assign a default value of nil to numberOfStageReuseLandingLegs :

let numberOfStageReuseLandingLegs: Int? = nil

Hooray! The compiler is happy again, and initialization succeeds. With this setup, numberOfStageReuseLandingLegs will never have a non-nil value. You cannot change it after initialization, since it is declared as a constant.

Banking on the Memberwise Initializer

Rockets are usually made up of several stages , which is the next thing to model. Declare a new struct named RocketStageConfiguration at the bottom of the playground:

struct RocketStageConfiguration {   let propellantMass: Double   let liquidOxygenMass: Double   let nominalBurnTime: Int }

This time, you have three stored properties propellantMass , liquidOxygenMass and nominalBurnTime with no default values.

Create an instance of RocketStageConfiguration for the rocket’s first stage:

let stageOneConfiguration = RocketStageConfiguration(propellantMass: 119.1,   liquidOxygenMass: 276.0, nominalBurnTime: 180)

None of RocketStageConfiguration ‘s stored properties have default values. Also, there is no initializer implemented for RocketStageConfiguration . Why isn’t there a compiler error? Swift structures (and only structures) automatically generate a memberwise initializer . This means you get a ready-made initializer for all the stored properties that don’t have default values. This is super handy, but there are several gotchas.

Imagine you submit this snippet for code review and your developer team lead tells you all properties should be ordered alphabetically.

Update the RocketStageConfiguration to re-order the stored properties:

struct RocketStageConfiguration {   let liquidOxygenMass: Double   let nominalBurnTime: Int   let propellantMass: Double }

Swift Tutorial: Initialization In Depth, Part 1/2

What happened? The stageOneConfiguaration initializer call is no longer valid, because the automatic memberwise initializer argument list’s order mirrors that of the stored property list. Be careful, because when re-ordering structure properties, you might break instance initialization. Thankfully the compiler should catch the error, but it is definitely something to watch out for.

Undo the stored property re-order change to get the playground compiling and running again:

struct RocketStageConfiguration {   let propellantMass: Double   let liquidOxygenMass: Double   let nominalBurnTime: Int }

All your rockets burn for 180 seconds, so it’s not useful to pass the nominal burn time every time you instantiate a stage configuration. Set nominalBurnTime ‘s default property value to 180:

let nominalBurnTime: Int = 180

Now there’s another compiler error:

Swift Tutorial: Initialization In Depth, Part 1/2

Compilation fails because memberwise initializers only provide parameters for stored properties without default values. In this case, the memberwise initializer only takes in propellant mass and liquid oxygen mass, since there is already a default value for burn time.

Remove nominalBurnTime ‘s default value so that there is no compiler error.

let nominalBurnTime: Int

Next, add a custom initializer to the struct definition that provides a default value for burn time:

init(propellantMass: Double, liquidOxygenMass: Double) {   self.propellantMass = propellantMass   self.liquidOxygenMass = liquidOxygenMass   self.nominalBurnTime = 180 }

Notice that the same compiler error is back on stageOneConfiguration !

Swift Tutorial: Initialization In Depth, Part 1/2

Wait, shouldn’t this work? All you did was provide an alternative initializer, but the original stageOneConfiguration initialization should work because it’s using the automatic memberwise initializer. This is where it gets tricky: you only get a memberwise initializer if a structure does not define any initializers. As soon as you define an initializer, you lose the automatic memberwise initializer.

In other words, Swift will help you out to start. But as soon as you add your own initializer, it assumes you want it to get out of the way.

Remove the nominalBurnTime argument from stageOneConfiguration ‘s initialization:

let stageOneConfiguration = RocketStageConfiguration(propellantMass: 119.1,   liquidOxygenMass: 276.0)

All is good again! :]

But what if you still need the automatic memberwise initializer? You can certainly write the equivalent initializer, but that’s a lot of work. Instead, move the custom initializer into an extension before you instantiate an instance.

Your struct will now be in two parts: the main definition, and an extension with your two-parameter initializer:

struct RocketStageConfiguration {   let propellantMass: Double   let liquidOxygenMass: Double   let nominalBurnTime: Int }   extension RocketStageConfiguration {   init(propellantMass: Double, liquidOxygenMass: Double) {     self.propellantMass = propellantMass     self.liquidOxygenMass = liquidOxygenMass     self.nominalBurnTime = 180   } }

Notice how stageOneConfiguration continues to initialize successfully with two parameters. Now re-add the nominalBurnTime parameter to stageOneConfiguration ‘s initialization:

let stageOneConfiguration = RocketStageConfiguration(propellantMass: 119.1,   liquidOxygenMass: 276.0, nominalBurnTime: 180)

That works too! If the main struct definition doesn’t include any initializers, Swift will still automatically generate the default memberwise initializer. Then you can add your custom ones via extensions to get the best of both worlds.

Swift Tutorial: Initialization In Depth, Part 1/2

Implementing a Custom Initializer

Weather plays a key role in launching rockets, so you’ll need to address that in the data model. Declare a new struct named Weather as follows:

struct Weather {   let temperatureCelsius: Double   let windSpeedKilometersPerHour: Double }

The struct has stored properties for temperature in degrees Celsius and wind speed in kilometers per hour.

Implement a custom initializer for Weather that takes in temperature in degrees Fahrenheit and wind speed in miles per hour. Add this code below the stored properties:

init(temperatureFahrenheit: Double, windSpeedMilesPerHour: Double) {   self.temperatureCelsius = (temperatureFahrenheit - 32) / 1.8   self.windSpeedKilometersPerHour = windSpeedMilesPerHour * 1.609344 }

Defining a custom initializer is very similar to defining a method, because an initializer’s argument list behaves exactly the same as a method’s. For example, you can define a default argument value for any of the initializer parameters.

Change the definition of the initializer to:

init(temperatureFahrenheit: Double = 72, windSpeedMilesPerHour: Double = 5) { ...

Now if you call the initializer with no parameters, you’ll get some sensible defaults. At the end of your playground file, create an instance of Weather and check its values:

let currentWeather = Weather() currentWeather.temperatureCelsius currentWeather.windSpeedKilometersPerHour

Cool, right? The default initializer uses the default values provided by the custom initializer. The implementation of the custom initializer converts the values into metric system equivalents and stores the values. When you check the values of the stored properties in the playground sidebar, you’ll get the correct values in degrees Celsius (22.2222) and kilometers per hour (8.047).

An initializer must assign a value to every single stored property that does not have a default value, or else you’ll get a compiler error. Remember that optional variables automatically have a default value of nil .

Next, change currentWeather to use your custom initializer with new values:

let currentWeather = Weather(temperatureFahrenheit: 87, windSpeedMilesPerHour: 2)

As you can see, custom values work just as well in the initializer as default values. The playground sidebar should now show 30.556 degrees and 3.219 km/h.

That’s how you implement and call a custom initializer. Your weather struct is ready to contribute to your mission to launch humans to Mars. Good work!

Swift Tutorial: Initialization In Depth, Part 1/2

Mars: not just for Matt Damon

Avoiding Duplication with Initializer Delegation

It’s time to think about rocket guidance. Rockets need fancy guidance systems to keep them flying perfectly straight. Declare a new structure named GuidanceSensorStatus with the following code:

struct GuidanceSensorStatus {   var currentZAngularVelocityRadiansPerMinute: Double   let initialZAngularVelocityRadiansPerMinute: Double   var needsCorrection: Bool     init(zAngularVelocityDegreesPerMinute: Double, needsCorrection: Bool) {     let radiansPerMinute = zAngularVelocityDegreesPerMinute * 0.01745329251994     self.currentZAngularVelocityRadiansPerMinute = radiansPerMinute     self.initialZAngularVelocityRadiansPerMinute = radiansPerMinute     self.needsCorrection = needsCorrection   } }

This struct holds the rocket’s current and initial angular velocity for the z-axis (how much it’s spinning). The struct also keeps track of whether or not the rocket needs a correction to stay on its target trajectory.

The custom initializer holds important business logic: how to convert degrees per minute to radians per minute. The initializer also sets the initial value of the angular velocity to keep for reference.

You’re happily coding away when the guidance engineers show up. They tell you that a new version of the rocket will give you an Int for needsCorrection instead of a Bool . The engineers say a positive integer should be interpreted as true , while zero and negative should be interpreted as false . Your team is not ready to change the rest of the code yet, since this change is part of a future feature. So how can you accommodate the guidance engineers while still keeping your structure definition intact?

No sweat — add the following custom initializer below the first initializer:

init(zAngularVelocityDegreesPerMinute: Double, needsCorrection: Int) {   let radiansPerMinute = zAngularVelocityDegreesPerMinute * 0.01745329251994   self.currentZAngularVelocityRadiansPerMinute = radiansPerMinute   self.initialZAngularVelocityRadiansPerMinute = radiansPerMinute   self.needsCorrection = (needsCorrection > 0) }

This new initializer takes an Int instead of a Bool as the final parameter. However, the needsCorrection stored property is still a Bool , and you set correctly according to their rules.

After you write this code though, something inside tells you there must be a better way. There’s so much repetition of the rest of the initializer code! And if there’s a bug in the calculation of the degrees to radians conversion, you’ll have to fix it in multiple places — an avoidable mistake. This is where initializer delegation comes in handy.

Replace the initializer you just wrote with the following:

init(zAngularVelocityDegreesPerMinute: Double, needsCorrection: Int) {   self.init(zAngularVelocityDegreesPerMinute: zAngularVelocityDegreesPerMinute,    needsCorrection: (needsCorrection > 0)) }

This initializer is a delegating initializer and, exactly as it sounds, it delegates initialization to another initializer. To delegate, just call any other initializer on self .

Delegate initialization is useful when you want to provide an alternate initializer argument list but you don’t want to repeat logic that is in your custom initializer. Also, using delegating initializers helps reduce the amount of code you have to write.

To test the initializer, instantiate a variable named guidanceStatus :

let guidanceStatus = GuidanceSensorStatus(zAngularVelocityDegreesPerMinute: 2.2, needsCorrection: 0) guidanceStatus.currentZAngularVelocityRadiansPerMinute // 0.038 guidanceStatus.needsCorrection // false

The playground should compile and run, and the two values you checked for the guidanceStatus properties will be in the sidebar.

One more thing — you’ve been asked to provide another initializer that defaults needsCorrection to false. That should be as easy as creating a new delegating initializer and setting the needsCorrection property inside before delegating initialization. Try adding the following initializer to the struct, and note that it won’t compile.

init(zAngularVelocityDegreesPerMinute: Double) {   self.needsCorrection = false   self.init(zAngularVelocityDegreesPerMinute: zAngularVelocityDegreesPerMinute,     needsCorrection: self.needsCorrection) }

Swift Tutorial: Initialization In Depth, Part 1/2

Compilation fails because delegating initializers cannot actually initialize any properties. There’s a good reason for this: the initializer you are delegating to could very well override the value you’ve set, and that’s not safe. The only thing a delegating initializer can do is manipulate values that are passed into another initializer.

Knowing that, remove the new initializer and give the needsCorrection argument of the main initiaziler a default value of false :

init(zAngularVelocityDegreesPerMinute: Double, needsCorrection: Bool = false) {

Update guidanceStatus ‘s initialization by removing the needsCorrection argument:

let guidanceStatus = GuidanceSensorStatus(zAngularVelocityDegreesPerMinute: 2.2) guidanceStatus.currentZAngularVelocityRadiansPerMinute // 0.038 guidanceStatus.needsCorrection // false

 Way to go! Now you can put those DRY (Don’t Repeat Yourself) principles into practice.

Introducing Two-Phase Initialization

So far, the code in your initializers have been setting up your properties and calling other initializers. That’s the first phase of initialization, but there are actually two phases to initializing a Swift type.

Phase 1 starts at the beginning of initialization and ends once all stored properties have been assigned a value. The remaining initialization execution is phase 2. You cannot use the instance you are initializing during phase 1, but you can use the instance during phase 2. If you have a chain of delegating initializers, phase 1 spans the call stack up to the non-delegating initializer. Phase 2 spans the return trip from the call stack.

Swift Tutorial: Initialization In Depth, Part 1/2

Putting Two-Phase Initialization to Work

Now that you understand two-phase initialization, let’s apply it to our scenario. Each rocket engine has a combustion chamber where fuel is injected with oxidizer to create a controlled explosion that propels the rocket. Setting up these parameters is the phase 1 part to prepare for blastoff.

Implement the following CombustionChamberStatus struct to see Swift’s two-phase initialization in action. Make sure to show Xcode’s Debug area to see the output of the print statements.

struct CombustionChamberStatus {   var temperatureKelvin: Double   var pressureKiloPascals: Double     init(temperatureKelvin: Double, pressureKiloPascals: Double) {     print("Phase 1 init")     self.temperatureKelvin = temperatureKelvin     self.pressureKiloPascals = pressureKiloPascals     print("CombustionChamberStatus fully initialized")     print("Phase 2 init")   }     init(temperatureCelsius: Double, pressureAtmospheric: Double) {     print("Phase 1 delegating init")     let temperatureKelvin = temperatureCelsius + 273.15     let pressureKiloPascals = pressureAtmospheric * 101.325     self.init(temperatureKelvin: temperatureKelvin, pressureKiloPascals: pressureKiloPascals)     print("Phase 2 delegating init")   } }   CombustionChamberStatus(temperatureCelsius: 32, pressureAtmospheric: 0.96)

You should see the following output in the Debug Area:

Phase 1 delegating init Phase 1 init CombustionChamberStatus fully initialized Phase 2 init Phase 2 delegating init

As you can see, phase 1 begins with the call to the delegating initializer init(temperatureCelsius:pressureAtmospheric:) during which self cannot be used. Phase 1 ends right after self.pressureKiloPascals gets assigned a value in the non-delegating initializer. Each initializer plays a role during each phase.

Isn’t the compiler super crazy smart? It knows how to enforce all these rules. At first, those rules might seem like nuisances, but remember that they provide a ton of safety.

What if Things Go Wrong?

You’ve been told the launch sequence will be fully autonomous, and that the sequence will perform a ton of tests to make sure all systems are good to go for launch. If an invalid value is passed into an initializer, the launch system should be able to know and react.

There are two ways to handle initialization failures in Swift: using failable initializers, and throwing from an initializer. Initialization can fail for many reasons, including invalid input, a missing system resource such as a file, and possible network failures.

Using Failable Initializers

There are two differences between normal initializers and failable initializers. One is that failable initializers return optional values, and the other is that failable initializers can return nil to express an initialization failure. This can be very useful — let’s apply it to the rocket’s tanks in our data model.

Each rocket stage carries two large tanks; one holds fuel, while the other holds oxidizer. To keep track of each tank, implement a new struct named TankStatus as follows:

struct TankStatus {   var currentVolume: Double   var currentLiquidType: String?     init(currentVolume: Double, currentLiquidType: String?) {     self.currentVolume = currentVolume     self.currentLiquidType = currentLiquidType   } }   let tankStatus = TankStatus(currentVolume: 0.0, currentLiquidType: nil)

There’s nothing wrong with this code except that it doesn’t recognize failure. What happens if you pass in a negative volume? What if you pass in a positive volume value but no liquid type? These are all failure scenarios. How can you model these situatons using failable initializers?

Start by changing TankStatus ‘s initializer to a failable initializer by appending a ? to init :

init?(currentVolume: Double, currentLiquidType: String?) {

Option-click on tankStatus and notice how the initializer now returns an optional TankStatus .

Update tankStatus ‘s instantiation to match the following:

if let tankStatus = TankStatus(currentVolume: 0.0, currentLiquidType: nil) {   print("Nice, tank status created.") // Printed! } else {   print("Oh no, an initialization failure occured.") }

The instantiation logic checks for failure by evaluating whether the returned optional contains a value or not.

Of course, there’s something missing: the initializer isn’t actually checking for invalid values yet. Update the failable initializer to the following:

init?(currentVolume: Double, currentLiquidType: String?) {   if currentVolume < 0 {     return nil   }   if currentVolume > 0 && currentLiquidType == nil {     return nil   }   self.currentVolume = currentVolume   self.currentLiquidType = currentLiquidType }

As soon as an invalid input is detected, the failable initializer returns nil . You can return nil at any time within a structure’s failable initializer. This is not the case with a class’s failable initializer, as you’ll see in Part 2 of this tutorial.

To see instantiation failure, pass an invalid value into tankStatus ‘s instantiation:

if let tankStatus = TankStatus(currentVolume: -10.0, currentLiquidType: nil) {

Notice how the playground prints, “Oh no, an initialization failure occurred.” Because initialization failed, the failable initializer returned a nil value and the if let statement executed the else clause.

Throwing From an Initializer

Failable initializers are great when returning nil is an option. For more serious errors, the other way to handle failure is throwing from an initializer.

You have one last structure to implement: one to represent each astronaut. Start by writing the following code:

struct Astronaut {   let name: String   let age: Int     init(name: String, age: Int) {     self.name = name     self.age = age   } }

The manager tells you an astronaut should have a non-empty String for his or her name property and should have an age ranging from 18 to 70.

To represent possible errors, add the following error enumeration before the implementation of Astronaut :

enum InvalidAstronautDataError: ErrorType {   case EmptyName   case InvalidAge }

The enumeration cases here cover the possible problems you might into when initializing a new Astronaut instance.

Next, replace the the Astronaut initializer with the following implementation:

init(name: String, age: Int) throws {   if name.isEmpty {     throw InvalidAstronautDataError.EmptyName   }   if age < 18 || age > 70 {     throw InvalidAstronautDataError.InvalidAge   }   self.name = name   self.age = age }

Note that the initializer is now marked as throws to let callers know to expect errors.

If an invalid input value is detected — either an empty string for the name, or an age outside the acceptable range — the initializer will now throw the appropriate error.

Try this out by instantiating a new astronaut:

let johnny = try? Astronaut(name: "Johnny Cosmoseed", age: 42)

This is exactly how you handle any old throwing method or function. Throwing initializers behave just like throwing methods and functions. You can also propagate throwing initializer errors, and handle errors with a docatch statement. Nothing new here.

To see the initializer throw an error, change johnny ‘s age to 17:

let johnny = try? Astronaut(name: "Johnny Cosmoseed", age: 17) // nil

When you call a throwing initializer, you write the try keyword — or the try? or try! variations — to identify that it can throw an error. In this case, you use try? so the value returned in the error case is nil. Notice how the value of johnny is nil . Seventeen is too young for spaceflight, sadly. Better luck next year, Johnny!

Swift Tutorial: Initialization In Depth, Part 1/2

To Fail or to Throw?

Initializing using a throwing initializer and try? looks an awful lot like initializing with a failable initializer. So which should you use?

Consider using throwing throwing initializers. Failable initializers can only express a binary failure/success situation. By using throwing initializers you can not only indicate failure, but also indicate a reason by throwing specific errors. Another benefit is that calling code can propagate any errors thrown by an initializer.

Failable initializers are much simpler though, since you don’t need to define an error type and you can avoid all those extra try? keywords.

Why does Swift even have failable initializers? Because the first version of Swift did not include throwing functions, so the language needed a way to manage initialization failures.

Swift Tutorial: Initialization In Depth, Part 1/2

Progress is what gets us to Mars, so we can grow rocks.

Where To Go From Here?

Wow — you’re not only halfway through getting humans to Mars, you’re now a Swift structure initialization guru! You can download the final playground for Part 1here.

To learn all about Swift class initialization, carry on to Part 2 of this tutorial.

You can find more information about initialization in the initialization chapter of Apple’s The Swift Programming Language guide. If you have any questions or comments, please join the discussion in the forum below!

转载本站任何文章请注明:转载至神刀安全网,谢谢神刀安全网 » Swift Tutorial: Initialization In Depth, Part 1/2

分享到:更多 ()

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址
分享按钮