You are currently viewing Making Sense of Swift ? and ! Operators in Optional, Downcasting and Initialization

Making Sense of Swift ? and ! Operators in Optional, Downcasting and Initialization

One of the challenges, when picking up Swift, is to understand how optional work. New learners, usually get confused whether they should use ? or ! when dealing with optional.

In this article, let’s dive deep into the concept behind optional, downcasting and initialization, by exploring the usage of both ? and ! operators.

If you are a beginner in Swift, this article should give you a good overview on how and when to use ? and !.

For experienced Swift developer, you can use this as a quick reference whenever you have doubt while dealing with both ? and ! operators.

With all that being said, let’s get started! 🚀


Declaration

var myVariable: MyType
  • Declare myVariable as non-optional type — MyType.
  • myVariable will not be nil for its entire lifetime.

var myVariable: MyType?
  • Declare myVariable as optional MyType.
  • myVariable can be nil at some point of its lifetime.
  • Value of myVariable can only be accessed by using optional binding or forced unwrapping(more on optional binding and forced unwrapping later)

var myVariable: MyType!
  • Declare myVariable as implicitly unwrapped optional MyType.
  • myVariable can be nil at some point of its lifetime.
  • Value of myVariable can be accessed with or without using optional binding.
  • If myVariable is nil and it is being access without using optional binding, runtime error will trigger.

Important Notes

  • Always declare variables as non-optional type.
  • Set variables as optional type only when you are not able to initialize the variables during its class or struct initialization, or the variables might become nil at some point of its lifetime after being initialized.
  • Avoid using implicitly unwrapped optional type as it might cause a runtime error. Set variables as implicitly unwrapped optional type only when you are not able to initialize the variables during its class or struct initialization, but you are confident that the variables will not be nil for its entire lifetime after being initialized. The best example of implicitly unwrapped optional type are the IBOutlets you created for your view controller.

Unwrapping

In this section, we will look into the basic concept and ways we can use to access value of optional type, mainly optional binding and forced unwrapping.

Optional Binding

  • Code will fail gracefully when try to access optional variable with nil value, no runtime error will be triggered.
  • Code snippet below demonstrate how optional binding works.
let str: String? = "test"

// Optional binding using 'if'
if let value = str {
    print(value)
}


// Optional binding using guard
guard let value = str else {
    return
}
print(value)


// Optional chaining
if let value = str?.data(using: .utf8)?.first {
    print(value)
}
  • Note that optional binding can also be used on implicitly unwrapped optional type. However this is quite uncommon and rather redundant.

Forced Unwrapping

  • Use the ! operator to forcefully access value of optional type.
  • Will trigger a runtime error when the optional variable is nil.
  • Following is how to use the ! operator to force unwrap an optional variable.
let myString: String? = “test”
let value = myString! // Forced unwrapping

Important Notes

  • Optional binding is a much more elegant way to unwrap optional variable compared to forced unwrapping, always use optional binding to unwrap optional value.
  • Forced unwrapping is dangerous to use, avoid using it if possible.

Downcasting

Both ? and ! operator are very important when dealing with downcasting in Swift. However, developer sometimes might get confused whether he or she should use asas! or as? when performing downcasting.

as
  • as keyword is used to converting one type to another when compiler is guaranteeing the success of the cast.
  • The common use cases will be casting from String to NSStringNSDate to Date or casting an object back to its parent class type.

as?
  • Use as? keyword when the dwoncasting operation might fail. We call this conditional cast.
  • Conditional cast will produce an optional form of the desired type.
  • Return nil when the cast operation failed.
  • Since the cast result is an optional type, we can use optional binding to access the value of the cast result.

as!
  • as! is the forced cast keyword.
  • Forced cast will produce a non-optional form of the desire type.
  • When cast operation failed, runtime error will trigger.
  • Since the cast result is a non-optional type, we can access the cast result value without using optional binding.

If you find that it is a bit hard to understand the explanation above. Let’s have a look at the following code snippet that demonstrate how to perform downcasting using each as keyword and what are the outcome for each case, it should clear things up for you.

class Bird {
    var name: String
    init(name: String) {
        self.name = name
    }
}

class Chicken: Bird {
    let canFly = false
}

class Eagle: Bird {
    let canFly = true
    func fly() {
        print("Eagle is flying.")
    }
}

let chicken = Chicken(name: "Chicky")

// Casting from subclass to parent class by using 'as'
let bird1 = chicken as Bird     // Cast successful: bird1 type is Bird
let bird2 = chicken as? Bird    // Warning: Forced cast from 'Chicken' to 'Bird' always succeeds
let bird3 = chicken as! Bird    // Warning: Forced cast from 'Chicken' to 'Bird' always succeeds

// Add chicken into array of type [Bird]
let birds: [Bird] = [chicken]

let chicken1 = birds[0] as Chicken  // Compile error: 'Bird' is not convertible to 'Chicken'
let chicken2 = birds[0] as? Chicken // Cast successful: chicken2 type is Chicken?
let chicken3 = birds[0] as! Chicken // Cast successful: chicken3 type is Chicken
let eagle1 = birds[0] as Eagle  // Compile error: 'Bird' is not convertible to 'Eagle'
let eagle2 = birds[0] as? Eagle // Cast successful: eagle2 is nil
let eagle3 = birds[0] as! Eagle // Runtime error: Could not cast value of type 'Chicken' to 'Eagle'

// Using optional binding to access conditional cast result
if let myChicken = birds[0] as? Chicken {
    print(myChicken.name)
}

Downcasting To Optional Types

Based on what has been discussed above, we can explore more by trying to downcast to optional types.

// Create another bird array that accept nil value
let otherBirds: [Bird?] = [chicken, nil]

// Downcasting chicken to optional type
let chicken4 = otherBirds[0] as? Chicken? // Cast successful: chicken4 type is Chicken??
let chicken5 = otherBirds[0] as! Chicken? // Cast successful: chicken5 type is Chicken?
let chicken6 = otherBirds[0] as? Chicken! // Compile error: Using '!' is not allowed here
let chicken7 = otherBirds[0] as! Chicken! // Compile error: Using '!' is not allowed here
        
// Downcasting nil
let chicken8 = otherBirds[1] as? Chicken // Cast successful: chicken6 is nil
let chicken9 = otherBirds[1] as! Chicken // Runtime error: Unexpectedly found nil while unwrapping an Optional value

Do not get confused by the ? and ! sign at the end of each casting, if you look closely, they all are having the same behaviour as previous example even thought the desired casting type is an optional type.

Do note that downcasting to implicitly unwrapped optional type is not allowed since Swift 5.0. This really makes sense because downcasting to implicitly unwrapped optional type is theoretically the same as downcasting to optional type, they both yield the same outcome.

Important Notes

  • Always use optional binding to check for conditional cast result.
  • Avoid using forced binding that might trigger a runtime error.

Initialization

Next up we will look into failable initializer. When there are possibilities that an object initialization might fail, we will need a failable initializer.

A failable initializer will return nil if it failed to initialize. We mark an initializer as failable by using the ? operator.

class Boy {
    
    // Failable initialization
    init?(age: Int) {
        // Age cannot be less than 0
        if age < 0 {
            return nil
        }
    }
}

let boy1 = Boy(age: 10) // Boy object initialized
let boy2 = Boy(age: -1) // Boy object fail to initialize, boy2 is nil

Further Reading

Here are some other Swift programming language related articles recommended to you :


I hope this article helps you get a clearer picture on how and when to use both ? and ! operators. If you have any comments or questions, just drop it in the comment section below.

Feel free to share this article on your favourite social media using the social media buttons below.

Follow me on Twitter if you want to read more articles like this in future.

Thanks for reading. 🧑🏻‍💻


👋🏻 Hey!

While you’re still here, why not check out some of my favorite Mac tools on Setapp? They will definitely help improve your day-to-day productivity. Additionally, doing so will also help support my work.

  • Bartender: Superpower your menu bar and take full control over your menu bar items.
  • CleanShot X: The best screen capture app I’ve ever used.
  • PixelSnap: Measure on-screen elements with ease and precision.
  • iStat Menus: Track CPU, GPU, sensors, and more, all in one convenient tool.