Why is it valid to initialize a static varialbe by using another static variable?

I find the following code works, which I didn't expect.

struct Test1 {
    static var x: Int? = 1

    static var y: Int = {
        if let x = x {
            return x
        }
        return 0
    }()
}

print(Test1.y)

I think Swift is very strict on initialization. For example, the following two examples don't compile. The error message is "Cannot use instance member 'x' within property initializer; property initializers run before 'self' is available". I wonder why the same reason doesn't apply to the above example, where self is the type object and property is the static variable? Is it because static variable is lazy initialized? How does that make a difference?

// This doesn't compile
struct Test2 {
    var x: Int = 1
    var y: Int = x
}

// This doesn't compile either.
struct Test3 {
    var x: Int = 1
    var y: Int = {
        x
    }()
}

With the current behavior, it's easy to set up cyclic dependence. The example below compiles but crashes at runtime.

struct Test4 {
    // x depends on y
    static var x: Int? = {
        // These are random code. They are meaningless. The only purpose is to set up dependence on y.
        if y != 0 {
            return 100
        } else {
            return 200
        }
    }()

    // y depends on x
    static var y: Int = {
        if let x = x {
            return x
        }
        return 0
    }()
}

print(Test4.y) // Crash at runtime.
2 Likes

Maybe these are smaller examples.

struct A {
    static var x: Int { y }
    static var y: Int { x }
}

struct B {
    static let x: String = x
}

... And with opaque result type, even the compiler crashes.

struct A { 
    static var x: some Numeric { y } 
    static var y: some Numeric { x } 
} 
1 Like

Yes. Specifically, the compiler desugars static initialization of this kind into a function call guarded by a call to swift_once. In this instance, y calls the lazy initializer of x. That also explains how the cyclic dependency can happen: there is no obvious reason that the initializer of x cannot call y.

This is exactly the same as having infinite recursion, and forbidding it in the general case would require solving the same problems as forbidding infinite recursion.

There is more than one self, but not more than one static location. Swift cannot pass self to the initializers of the stored properties becuase self is not fully initialized at that time, but this does not apply to statics.

1 Like

Thanks. I get it. So, due to the way how lazy initialization works (it's through a function, as you explained), static variables are always considered initialized as long as user provides an initialization expression. So they never have the "not fully initialized" error.


A incorrect example

Update to myself: It's important that those functions are not evaluated immediately, otherwise it won't work. See this example:

// This doesn't compile because instance property initialization expression are evaluated immediately.
struct Test1 {
    var x: Int = { y }()
    var y: Int = { x }()
}

On the other hand, it's also due to the lazy initialization behavior that makes it impossible to catch the cyclic dependence issue with static variables.

Yes, even in instance properties, the code will compile if you defer initializing the properties using the lazy keyword:

struct Test1 {
    lazy var x: Int = { y }()
    lazy var y: Int = { x }()
}

(Of course, the code still crashes due to infinite recursion.)

Note that a major difference between the automatically lazy static properties (and global variables as well) is that they are guaranteed to be called only once when initialized, even if accessed from multiple threads.

Marking an instance property as lazy does not guarantee this.

1 Like

Thanks @James_Dempsey for your comments. I intended to use the example to demonstrate that assigning a function to instance property doesn't work. But that is an incorrect example, because the initializer is a func call (a expression evaluated immediately), not a function. Your example is the one I was looking for.

It's interesting to know that. I seldom use lazy. I think it's an important topic which I should get myself familiar with in future.

1 Like