Rss

A Perfectly Lazy Solution

Something nifty with Swift came up the other day, not a big deal, but worth blogging before I forget it again.

Basically it involves the interaction of three things:

  • lazy variables – Variables that are not created until they’re first accessed, typically because their initialization is an expense you’d like to put off (or not perform at all, if the variable is never used).
  • Instantiating variables with closures – When you need to perform multiple statements to set up a variable, you can do something like:
    
    let foo = { ... } ()
    

    which means you’re creating a closure, executing it, and assigning its return value to foo.

  • Delegates. Hoo boy, do we love our delegates…


The thing with delegates is that they’re usually optional. Most of the time, they represent some extra stuff a class may do. Semantically, the idea is “I only do so much, but in certain situations (defined by methods in a protocol), I will delegate responsibility to another object.” So it’s usually OK for a delegate to be nil and then when those events happen, the class just won’t do anything extra, because there’s no delegate assigned to handle the action. Everyone with me, so far?

But in a few cases here and there, a delegate cannot be nil. These are cases where the object makes no sense without some other object to call out to. In the iOS SDK, there are a few examples of this: URLSession, CBCentralManager and the unit-testing helper XCTWaiter. I’d also argue that we can include Timer, except that instead of a delegate, it has a target.

In all these cases, you have to provide the object with its delegate (or target) at initialization time. And that leads to a terrible head-scratcher. What do you do if you want to use one of these objects as a property, but also use self as the delegate?

Swift won’t let you easily do this. Let me show you why. We’ll start with a class and its delegate protocol.


/// A class which sends callbacks to a delegate
class Delegator {
    var delegate: Delegatee
    var someString: String?

    init (delegate: Delegatee) {
        self.delegate = delegate
    }
}

/// A protocol for the delegate called by a Delegator
protocol Delegatee: class {
    func getDelegated()
}

Now imagine you have a class where you want to have a Delegator as a property. You can’t do this:


var delegator: Delegator = Delegator(delegate: self)

Because that assumes that self exists prior to init(), which is obviously impossible. It’s a chicken-and-egg problem without an obvious good solution.

There’s an obvious not-very-good solution: make delegator an implicitly-unwrapped optional. This means you define it as type Delegator! and while it’s an optional, you don’t have to unwrap it all the time. You’re basically promising to the compiler that you know what you’re doing and will be sure to not access it before its value is set, because that would cause a crash.

Here’s what that version looks like:


/// A class that uses an implicitly-unwrapped optional for
/// its Delegator property, which it must populate in init()
/// after the call to super.init()
class MyClass: NSObject, Delegatee {
    var delegator: Delegator!
    
    override init() {
        super.init()
        delegator = Delegator(delegate: self)
        delegator.someString = "foo"
    }
    func getDelegated() {
        print ("got delegated")
    }
}

// creating an instance
let test1 = MyClass()
test1.delegator.someString

So this works… OK. But IUOs are still gross. And they’re still perfectly able to blow up in your face. Imagine if another developer adds an additional convenience initializer, and forgets to assign delegator:


    init(someParam: Int) {
        super.init()
        // * do something with someParam
        // * completely forget to initialize delegator property
    }

Now you have a potential crash if you ever use that initializer and try to touch delegator


let testDie = MyClass(someParam: 1)
testDie.delegator.someString // aieee! we die!

So we seem to be stuck with the IUO, or a full-on optional if we want to be truly mindful of how our class works.

But there’s another option, one that breaks the chicken-and-egg paradox. And it’s the lazy option.

If you go back to the naïve option, and just add the keyword lazy, like this:


lazy var delegator: Delegator = Delegator(delegate: self)

Suddenly, the compiler error goes away and everything works.

Think about why that is. The point of the lazy variable is that it won’t be assigned until its needed. Which in turn means that it can’t be accessed until self exists, because otherwise there’s no way to access it. Therefore, it’s always OK to reference self when assigning a lazy variable.

Thinking man meme: can't reference a property, if there's nothing to reference it from

This gets even better when we remember there’s a property within Delegator that we’d like to assign. Before, we did that in init(), but with this approach, we can use the technique from point 2 of our original list and run a closure to initialize a Delegator, configure it, and return it. Here’s the whole class listing:


/// A class that uses a lazy var for its Delegator, and can
/// thus access self from inside the closure that it runs to
/// initialize the var when first accessed
class MyLazyClass: NSObject, Delegatee {
    lazy var delegator: Delegator = {
        let tempDelegator = Delegator(delegate: self)
        tempDelegator.someString = "bar"
        return tempDelegator
    }()
    
    func getDelegated() {
        print ("got delegated")
    }
}

The other great thing about this is that since the closure can be shown to always return a value, we can make delegator a non-optional. No !, no ?, no nothing. You can call the delegator object whenever you need it with confidence that it’ll exist and not crash your app.

Comments (3)

  1. Chris Adamson

    Follow-up thoughts from Heath Borders and Michael Tsai on the thread-unsafety of lazy variables: https://twitter.com/heathborders/status/903337385244622849

  2. Riley

    > Imagine if another developer adds an additional convenience initializer, and forgets to assign delegator:

    Small nit, I believe this is actually an additional _designated_ initializer, since it delegates up and not across (and doesn’t have the `convenience` modifier, to boot).

Leave a Reply

Your email address will not be published. Required fields are marked *