Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

swift lazy var with throw init behavior

I am not sure if it is a bug or it is really how things should work?

class A {
    init() throws { }
}

class B {
    lazy var instance = A()
}

this code compiles without mistakes using XCode 9 and latest Swift version, and works perfect unless Class A init() really throws, then lazy var is null pointer. But shouldn't be this code somehow not be compiled?

like image 707
JuicyFruit Avatar asked Jun 04 '18 07:06

JuicyFruit


People also ask

How does lazy var work in Swift?

Swift has a mechanism built right into the language that enables just-in-time calculation of expensive work, and it is called a lazy variable. These variables are created using a function you specify only when that variable is first requested.

How do you call lazy VAR in Swift?

You indicate a lazy stored property by writing the lazy modifier before its declaration. You must always declare a lazy property as a variable (with the var keyword), because its initial value might not be retrieved until after instance initialization completes.

When should I use lazy var Swift?

Lazy variables allow you to delay the initialisation of stored properties. This can be useful to only perform expensive work when it's actually needed. The different between lazy- and computed properties is important in cases you need to have calculations based on the current state of values.

Do you have to initialize variables in Swift?

Variables and constants do not require initialization when declared. However, the variable or constant requires type annotation so that when the compiler reads the code line-by-line, it can determine the data type at the time of the build. A Use of undeclared type error will be thrown otherwise.


2 Answers

This is indeed a bug (SR-7862) – you cannot throw errors out of a property initialiser context (and even if you could, you would be required to prefix the call with try), therefore the compiler should produce an error.

I have opened a pull request to fix this (#17022).

Edit: The patch has now been cherry-picked to the 4.2 branch, so it'll be fixed for the release of Swift 4.2 with Xcode 10 (and until the release you can try a 4.2 snapshot).

like image 119
Hamish Avatar answered Jan 04 '23 05:01

Hamish


As an answer to your question:

But shouldn't be this code somehow not be compiled?

Well, at some point your code snippet worked without any issue (because -as you mentioned- the class A init doesn't actually throws), so it could be compiled without any problem. To make it more clear, consider it as a similar case to the following one:

let myString: String? = nil
print(myString!) // crashes!

it will get compiled just fine! although we all know that it crashes when evaluating myString!, i,e we do know it causes a run-time crash, but that doesn't mean that the compiler should prevent it because it could be valid at some point (for instance if we declare it as let myString: String? = "Hello"); Similarly to your case, it could be valid at some point -as mentioned above-.

Usually, for such cases we -as developers- are the responsible to handle it based on what's the desired behavior(s).


Referring to this case, we might need to ask:

"How can we implement the instance lazy variable to catch an error (with a do-catch block)?"

Actually, this code won't compile:

class B {
    lazy var instance:A = {
        do {
            let myA = try A()
            return myA
        } catch {
            print(error)
        }
    }()
}

complaining that:

Missing return in a closure expected to return 'A'

because obviously reaching the catch block means that there is nothing to be returned. Also, as you mentioned even if you implemented it as

lazy var instance = A()

you will not get a compile-time error, however trying to use it with an actual throwing should leads to run time error:

let myB = B()
print(myB.instance) // crash!

What I would suggest for resolving this issue is to declare instance as lazy optional variable:

class B {
    lazy var instance:A? = {
        do {
            let myA = try A()
            return myA
        } catch {
            print(error)
        }

        return nil
    }()
}

At this point, if we assume that A initializer always throws, trying to access it:

let myB = B()
print(myB.instance)

should log:

caught error

nil

without causing any crash. Otherwise, it should works fine, for instance:

let myB = B()
myB.instance?.doSomething() // works fine
like image 35
Ahmad F Avatar answered Jan 04 '23 07:01

Ahmad F