Optional is the mechanism in Swift to indicate the possible absence of a value or a reference to an object. While it can take a bit of getting used to for those unfamiliar with this concept, appropriate use of optionals can make Swift code safer and more expressive. This article explains what optionals are, why Swift has them, and the ways in which optionals can be used in Swift to tackle otherwise difficult situations in an elegant and effective manner.
Related articles:
Contents
Why does Swift have optionals
How are optionals implemented in Swift
When to use optionals in Swift
1. Property not guaranteed to have a value
2. Function not guaranteed to return a value
3. Initializer not guaranteed to create an instance
4. Function parameter not required in every case
5. Conditional type casting
6. Simplified error handling
Conclusion
What is an optional in Swift
Optional
is a special Swift type, instances of which may or may not contain an instance of a given Swift type. An optional can be thought of as a box, which may be empty or may contain (wrap in Swift parlance) an instance of a given Swift type. It is noteworthy that while optionals can be used to wrap instances of any Swift type, including the Optional
type, when an instance of Optional
is created, the type that it can wrap must be specified. Once created, an Optional
instance can be in one of two states – it either wraps a single instance of the specified wrapped type or it is empty. An empty optional is equivalent to nil
, which is a special literal representing the absence of a value.
Optionals are such an important part of Swift that a special syntax has been baked into the language to make them easy to use. We can declare an optional wrapping any Swift type by simply postfixing the name of the type to be wrapped with a question mark (?
). This works not only for types in the Standard Library, such as Int
and String
, but also for user-defined types, including reference types (classes and closures) and values types (structs, enums and tuples).
As an example, we declare an optional with String
as the wrapped type, and give it a String
value to wrap.
var maybeString: String? = "My string"
We can use the type(of:)
function from the Standard Library to check the type of maybeString
and use an equality check to confirm that it is not equal to nil
, since it was initialized with a wrapped value.
print(type(of: maybeString)) // Optional<String>
print(maybeString == nil) // false
The assignment statement above seems to break the strict typing rules of Swift, since we are able to assign a String
value to a variable of type Optional<String>
. This is one of the many syntactic conveniences built into Swift to make optionals easier to use. When we assign an instance of the wrapped type to an optional variable, behind the scenes, an optional wrapping the given instance is created and assigned to the variable. The same automatic wrapping also works when calling functions, which means that if we have a function with an optional as a parameter, we can pass as an argument a non-optional instance of the type wrapped by the optional. It is important to note that the reverse is not true. If we try to assign an optional to a variable of the wrapped type or pass an optional to a function that expects an instance of the wrapped type, we will get an error.
In the spirit of syntactic convenience, Swift also allows us to create an optional without providing an instance of the wrapped type, as shown below.
var maybeInt: Int?
If we don’t give an optional variable an initial value when we declare it, an empty optional, which equates to nil
, is automatically created and assigned to the variable. This is the only case where Swift automatically initializes a variable. We can confirm this by doing a type and equality check on maybeInt
.
print(type(of: maybeInt)) // Optional<Int>
print(maybeInt == nil) // true
An empty optional can be assigned any instance of the wrapped type. We can see this in action by assigning a value to maybeInt
, after which it is no longer equal to nil
.
maybeInt = 5
print(maybeInt == nil) // false
Note that although optionals wrapping String
and Int
instances are actually of types Optional<String>
and Optional<Int>
respectively, as we have seen above, we can use the shorthand String?
and Int?
, not only to declare such optionals but also for all purposes thereafter.
Why does Swift have optionals
This is a question normally asked by programmers coming to Swift from languages that don’t have the concept of optionals. There are times when we need some way to signal in our code that a value or a reference to an object may or may not exist. At other times, we would like to be assured that when we expect to have a value or a reference to an object, it will be there. In languages that don’t have built-in support for optionals, where such guarantees are not built into the language, it can lead to undesirable patterns in code, such as unnecessary checking to ensure that an object reference points to an actual object or, when dealing with scalar types, using sentinel values, which are arbitrary values assumed to have a special meaning in a given context, such as a function meant to return the index of an array returning -1
when there is no valid index value to return.
Swift resolves these problems from the ground up. First, Swift does not have any scalar types. All types in Swift are what may loosely be called object types, which means they can be instantiated and messages can be sent to the instances thus created. This is true for all value and reference types in Swift. Second, Swift optionals formalize the distinction between situations where a value or a reference must be available and those where a value or a reference legitimately may or may not be present.
A non-optional variable in Swift must always have a value or a reference, as the case may be, and cannot be used without being initialized. Similarly, a Swift function whose return type is non-optional will not compile unless it returns a value or a reference, depending on whether the return type is a value or a reference type. This provides an iron-clad guarantee that if the type of a variable or the return type of a function has not been declared to be optional, it will always have a value or a reference. On the other hand, if the type of a variable or the return type of a function is declared as optional, any code that uses such a variable or function must use special syntax built into the language not only to check whether a value or a reference actually exists but also to use it safely if it does.
How are optionals implemented in Swift
We have already noted that an optional is a wrapper that can wrap an instance of a given Swift type. This is implemented using a generic enum as follows.
public enum Optional<Wrapped> {
case none
case some(Wrapped)
}
The enum has two cases, to represent the absence and presence of a wrapped instance respectively. When there is no wrapped instance, the value of the optional is .none
. When the optional has a wrapped instance, its value is .some
, with the wrapped instance as the associated value. Since Optional
is a generic type, the Wrapped
type parameter is used to determine the type of the wrapped instance at the time an Optional
instance is created.
This explains why optionals wrapping a String
and an Int
are of types Optional<String>
and Optional<Int>
respectively. Although we could declare an optional using this syntax, it is much more convenient and customary to use the String?
and Int?
shorthand type names. The nil
literal serves as a convenient shorthand for the case .none
.
An optional can be initialized with a nil
literal because the Optional
type conforms to the protocol ExpressibleByNilLiteral
, which has the following requirement.
protocol ExpressibleByNilLiteral {
init(nilLiteral: ())
}
The type of the parameter nilLiteral
is an empty tuple, which signifies the lack of a value. When this initializer is called, it creates a new Optional
instance with the value .none
. While this protocol is not specific to Optional
, no other Swift type conforms to this protocol and use of this protocol with other types is discouraged to avoid confusion with Optional
. This initializer should not be called directly. It is automatically called by the compiler when an optional is initialized using the nil
literal (or an optional is created without providing an instance of the wrapped type, which has the same effect).
When to use optionals in Swift
Optionality is a common feature of real-world domains. By formalizing the concept of optionality, Swift can help us create better domain models and write code that can deal explicitly with cases where a certain relationship or association may or may not be present for a particular instance, either intrinsically or at a point in time.
It is also common to encounter situations in our code when a certain outcome cannot be guaranteed. Functions may be unable to return a value in all cases, creation of new instances of certain types may fail under certain conditions, not all type casting operations may succeed, etc. Optionals provide a simple and elegant way to deal with such situations where the reason for the failure can be discerned from the context.
Let us explore this subject in greater detail and look at some of the common scenarios where optionals are used in Swift.
1. Property not guaranteed to have a value
It is not uncommon to come across properties that may have a value for some instances of the type but not for others. Similarly, there could be properties that may have a value at certain times but not at others. Optionals are the natural choice to formally express such constraints. To illustrate, let us define a protocol to model pets and a class to model people who may own these pets.
protocol Pet {
var name: String? { get set }
func makeSound()
}
class Person {
var name: String
var pet: Pet?
init(named name: String) {
self.name = name
}
}
The Pet
protocol declares a method which conforming types will implement to indicate the sound that each pet makes. It also declares a property for the name of the pet. The Person
class has two properties, one for the name of the person and the other for the pet the person may have.
Since not every person may have a pet, and someone who has a pet once is not guaranteed to always have one, the pet
property in Person
is an optional. Also, while the name
property in Person
is non-optional, the name
property in Pet
is declared as an optional. This is to reflect the requirement that, while every person must have a name, not every pet is required to have a name.
2. Function not guaranteed to return a value
There could be functions, methods or closures with declared return types that may not be able to return a value in all cases. Usually this indicates that the operation could not be performed as requested.
As an example, the Collection
protocol in the Standard Library defines a firstIndex(of:)
method, available when elements of the collection conform to Equatable
, which returns the first index where the specified value appears in the collection. It is possible, however, that the value does not appear in the collection. That is why the return type of the method is Int?
. If the value is found, its integer index is returned wrapped in an optional; if not, nil
is returned.
To demonstrate this, we define a simple generic function which looks for the first index of a given element in an array, where elements of the array conform to Equatable
, printing the first index if the element is found and printing a suitable message if it isn’t.
func printFirstIndex<Element: Equatable>(of element: Element, in array: [Element]) {
let maybeIndex = array.firstIndex(of: element)
if let index = maybeIndex {
print("Element \(element) was found at index \(index)")
} else {
print("Element \(element) not found")
}
}
Here is how we can use the above function with an array of integers.
let ints = [2, 3, 5]
printFirstIndex(of: 3, in: ints) // Element 3 was found at index 1
printFirstIndex(of: 7, in: ints) // Element 7 not found
3. Initializer not guaranteed to create an instance
There could be circumstances under which an initializer of a type may not be able to create a valid instance. This could be due to some or all arguments not being valid or some other required condition not being met. To cater to such situations, Swift lets us declare a failable initializer, which returns an optional wrapping the newly created instance. If instance creation fails, nil
is returned. A failable initializer is declared by putting a question mark (?
) before the opening parenthesis of the parameter list.
Looking at the Person
class defined above, the name
property of Person
is not an optional since every person must have a name. But we could still create a nameless person by passing an empty string as the argument.
let namelessPerson = Person(named: "")
To prevent this from happening, we can modify the Person
class to make the initializer failable.
class Person {
var name: String
var pet: Pet?
init?(named name: String) {
guard !name.isEmpty else { return nil }
self.name = name
}
}
Now a Person
instance with an empty string for a name cannot be initialized.
let notAPerson = Person(named: "")
print(type(of: notAPerson)) // Optional<Person>
print(notAPerson == nil) // true
4. Function parameter not required in every case
A function may not need all its parameters for every invocation. Optional parameters provide an elegant way to handle such cases. When using such a function, for each optional parameter, the caller must either pass a value of the type wrapped by the optional or use nil
. Alternatively, an optional parameter can be given a default of nil
, which gives the caller the option of calling the function with as many arguments as may make sense for a particular invocation. This also applies to initializers.
Continuing our running example, we define a Cat
class, which conforms to the Pet
protocol.
class Cat: Pet {
var name: String?
init(named name: String? = nil) {
self.name = name
}
func makeSound() {
print("Meow")
}
}
We have defined an initializer with a parameter of type String?
, with a default value of nil
. We can use this initializer to create Cat
instances with or without a name.
let unnamedCat = Cat()
let namedCat = Cat(named: "Bella")
5. Conditional type casting
When we cast from a subtype to a supertype, the cast is guaranteed to succeed. The same is true when casting an instance of a concrete type to the type of a protocol to which the concrete type conforms. For casts that are guaranteed to succeed, we use the as
operator.
var cat = Cat()
var somePet: Pet = cat as Pet
However, when we cast from a supertype to a subtype or from a protocol type to a concrete type, we cannot use the as
operator since such casts are not guaranteed to succeed. We demonstrate this using the somePet
and cat
variables declared above.
somePet = Cat()
cat = somePet as Cat
// Error: 'Pet' is not convertible to 'Cat'
Although the instance we are trying to cast is of class Cat
, we get a compiler error. This is because the variable somePet
is typed as Pet
and the compiler cannot be certain that the runtime type of the instance we are trying to cast will always be one for which the cast will succeed.
For casts that are not guaranteed to succeed, we can post-fix the as
operator with a question mark (?
). If the cast succeeds, we get the result of the cast wrapped in an optional.
var maybeCat = somePet as? Cat
print(type(of: maybeCat)) // Optional<Cat>
print(maybeCat == nil) // false
If a conditional cast fails, we get nil
. To demonstrate this, we define a Dog
class that also conforms to Pet
.
class Dog: Pet {
var name: String?
init(named name: String? = nil) {
self.name = name
}
func makeSound() {
print("Woof")
}
}
somePet = Dog()
maybeCat = somePet as? Cat
print(maybeCat == nil) // true
Since we have assigned an instance of Dog
to somePet
, the cast to Cat
fails and maybeCat
equals nil
.
6. Simplified error handling
Optionals provide a convenient means for handling simple errors where the reason for the error can be easily discerned from the context. For more complex error handling requirements, we would normally use the error handling mechanism built into Swift. The usual way to handle errors in Swift is to put any statement or function call that can throw an error in a try
expression. If an error is thrown, it must be handled in some way, either by catching it with a do-catch
statement or propagating it by marking the function as throwing.
However, there may be situations when a particular use case only requires knowing whether an error got thrown and not which error it was. This can come in handy when using frameworks and third-party libraries where we may not be interested in the details of some of the errors thrown but only whether an operation failed.
To do this, we can post-fix try
with a question mark (?
). A try?
expression does not need to appear in a do-catch
statement or a throwing function. If no error is thrown, we get the result obtained from evaluating the expression wrapped in an optional. Otherwise, we get nil
.
We demonstrate this by defining an error type with a single error case. We also define a throwing function, which takes a single boolean parameter to help us control whether an error gets thrown.
enum TestError: Error {
case someError
}
func possibleThrower(shouldThrow: Bool) throws -> String {
if shouldThrow {
throw TestError.someError
}
return "My string"
}
We can use the above function to see the result of a try?
expression, both when the throwing function actually throws an error and when it does not.
var stringResult = try? possibleThrower(shouldThrow: false)
print(type(of: stringResult)) // Optional<String>
print(stringResult == nil) // false
stringResult = try? possibleThrower(shouldThrow: true)
print(stringResult == nil) // true
When the function in the try?
expression does not throw an error, we get an optional wrapping the return value of the function. When an error is thrown, we get nil
to indicate failure.
This works for functions with a return type. But how about functions with no return type? Interestingly, in Swift, there is no such thing as a function with no return type. Functions with no return type implicitly have a return type of Void
and a special return value of ()
, which is an empty tuple. An optional can be used to wrap any Swift type, including Void
.
We can use this to extend our above example to a function without an explicit return type.
func anotherPossibleThrower(shouldThrow: Bool) throws {
if shouldThrow {
throw TestError.someError
}
}
var voidResult: Void? = try? anotherPossibleThrower(shouldThrow: false)
print(voidResult == nil) // false
voidResult = try? anotherPossibleThrower(shouldThrow: true)
print(voidResult == nil) // true
The function anotherPossibleThrower
has an implicit return type of Void
which becomes Void?
when we use it in a try?
expression. We can capture the implicit return value to check whether it is nil
, which means an error was thrown. Note that we have to explicitly declare the type of the variable voidResult
to avoid getting a warning from the compiler for attempting to capture a return value that we have not explicitly declared.
Conclusion
Optionals can impose a bit of a learning curve on programmers new to Swift and can feel frustrating at times. Once mastered though, optionals feel natural in the way they can make code safer and more expressive by providing a way to indicate the possible absence of a value or a reference to an object.
Thank you for reading! I always appreciate constructive comments and feedback. Please feel free to leave a comment in the comments section of this post or start a conversation on Twitter.
Leave a Reply