This is the second part of a two-part series on Swift Closures. You can read the previous article here: Understanding Swift Closures - Part 1
In this article, we're going to learn about some interesting Swift features related to functions and more specifically, closures:
- Inout parameters
- Escaping closures
- Autoclosures
Let's get dive into it.
Inout parameters
It's not strictly related to closures but to functions in general. By default, the parameters passed to a function or a closure become constants no matter how you've declared them outside the function. That being said, you cannot modify or so to say, mutate the argument inside the function.
It doesn't matter if it's an integer, string or an array, Swift won't let you change the state of the parameter.
func addItem(_ arr: [Int]) {
arr.append(2) // Cannot use mutating member on immutable value: 'arr' is a 'let' constant
}
addItem([1])
And there's no difference in case of Structs:
struct MyStruct {
var n = 0
mutating func increase() {
n += 1
}
}
func increaseStruct(_ obj: MyStruct) {
obj.increase() // Cannot use mutating member on immutable value: 'obj' is a 'let' constant
}
var myObj = MyStruct()
increaseStruct(myObj)
NOTE: However, at first glance, structs are very similar to classes, you can update the state of a class instance inside a function. That's because classes are reference types, not value types.
Mutating an input parameter is not a good idea in my opinion. Modifying the input inside a function means the function has an effect on the surrounding environment. A function should be a well-focused, small block of code that doesn't know much about what's happening in the outside world. When a function has been permitted to modify other parts of the application, it's called a side effect.
Personally, I prefer pure functions and omit side effects as much as I can. It doesn't just make debugging easier but also easier to write unit tests for pure functions. But that's just me. If you don't agree, Swift allows you to update function parameters, just make sure you use the inout
keyword right before the type annotation:
func increase(_ number: inout Int) {
number += 1
}
When we call the increase
function with an integer, Swift wants us to make it crystal clear on both ends that the function is going to change the value. When we declare the function, and when we call it. In case of inout parameters, we have to mark the given argument with the ampersand symbol at the place of the invocation.
var n = 0
increase(&n) // -> 1
NOTE: you can't pass the integer directly to the function and mark it with the ampersand. It's the outer scope's responsibility to register a memory pointer by assigning the value to a variable.
Obviously, if you've declared the argument as a constant, Swift won't let you modify the value regardless of the inout keyword at the function declaration.
let n = 0
func increase(_ number: inout Int) {
number += 1
}
increase(&n) // Cannot pass immutable value as inout argument: 'n' is a 'let' constant
If you want, you can do the same with closures but you have to be explicit regarding the closure parameters:
var n = 0
let increase = { (number: inout Int) in
number += 1
}
increase(&n) // -> 1
If you define inout parameters but you don't actually mutate them inside the function, you're a very very bad person. Period.
Escaping closures
If you pass a closure to a function and you don't actually call it, guess what? Swift is smart enough to figure and won't let you. It's like "Hey man! I don't know you but if you think you can mess with me, you better wake up earlier." So at this point, you're about to do something weird. But "why would you pass a closure to a function if you don't want to call it?" Simple. I store it for later execution. In that case, it's an escaping closure and you have to mark it with the @escaping
keyword right before the type annotation.
Suppose we have an event manager class by which we can be informed if a certain event happens. In order to do that, we add our listeners and tell the event manager "Hey buddy, please let me know about it by calling this function to me!"
struct Event {
var name = "Click"
}
class EventManager {
var listeners = [(Event) -> Void]()
func addEventListener(_ listener: @escaping (Event) -> Void) {
listeners.append(listener)
}
func trigger() {
for listener in listeners {
listener(Event())
}
}
}
let em = EventManager()
em.addEventListener() { (e: Event) in
print("\(e.name) happened.")
}
em.trigger() // Click happened.
In the example above, we have an addEventListener
method inside the EventManager class. It takes a closure as the first parameter but it doesn't call it before the method returns. It stores the closure in the listeners property which is an array of closures that take an Event parameter and returns nothing. It's an array because it lets you add multiple listeners if you will. The click event can happen anytime. It can happen immediately, or five minutes later. But when it does, the trigger method is responsible for calling each listener inside the listeners array. In case of scenarios like this, we have to explicitly mark them as @escaping
closures.
Autoclosures
For convenience purposes, if you have a simple statement but you want it to be executed later, you can ask Swift to wrap your statement inside a closure. These are the so-called Autoclosures:
func whatever(wrapped: @autoclosure () -> Int) {
print(wrapped())
}
whatever(wrapped: (1 + 2))
In the example above, since wrapped
is marked as @autoclosure
, Swift takes the statement, which is adding 1 and 2, put it inside a closure, do the math and returns the result of the evaluation. Autoclosures don't accept any incoming parameters.
Basically, it lets you omit the extra braces when you call whatever
. And that's it. To be honest, I don't really see the benefits of autoclosures. Yeah sure, I don't have to write the braces but for me, it makes the code a lot harder to follow.
Am I missing something? If you have any experiences with autoclosures and you see the positive or negative parts I might have missed, let me know in the comments section.
Top comments (0)