'self' used before all stored properties are initialized
Asked Answered
A

4

61

I'm working through a learn-swift playground and upgrading it to Swift 2.0 as I learn the language. The following code (which likely worked with prior versions of Swift) now generates two errors: "'self' used before all stored properties are initialized" and "Constant 'self.capitalCity' used before initialized"

class Country
{
    let name: String
    let capitalCity: City!

    init(name: String, capitalName: String)
    {
        self.name = name
        self.capitalCity = City(name: capitalName, country: self)
    }
}

class City
{
    let name: String
    unowned let country: Country

    init(name: String, country: Country)
    {
        self.name = name
        self.country = country
    }
}

reading an answer to a similar question I see that I can change let capitalCity: City! to var capitalCity: City! and the syntax error is resolved.

I realize that in this contrived example a country's capital city can change, so that would be fine, but what if there were a case where the value really was a constant...

Is there any way to resolve the syntax error while keeping capitalCity a constant?

Andie answered 26/12, 2015 at 19:33 Comment(0)
S
43

In this case I would suggest you to make the property a variable but hiding it (make it seem like a constant) through a computed property:

class Country {
    let name: String

    private var _capitalCity: City!
    var capitalCity: City {
        return _capitalCity
    }

    init(name: String, capitalName: String) {
        self.name = name
        self._capitalCity = City(name: capitalName, country: self)
    }
}
Simonize answered 26/12, 2015 at 19:52 Comment(4)
this is a great answer. It gets to the interface that I wanted, though the explanation by @matt was fabulousAndie
swift is a weird language for initialization.Cordierite
This did not work for me. It fixed the compile error, but when I pass self to the constructor of another object, and then that object calls a function on self, that function sees an entirely different (uninitialized) self.Decemvir
@Decemvir Only capitalCity is not initialized when calling the constructor of City. If you want to use it inside the constructor, then you have to use self inside the City constructor. If the problem is complicated then feel free to ask a question on StackOverflow.Simonize
A
32

Is there any way to resolve the syntax error while keeping capitalCity a constant?

Not the way you have things set up. The source of the problem is actually that in order to set capitalCity, you have to create a City whose country is self. That is the use of self to which the compiler is objecting:

self.capitalCity = City(name: capitalName, country: self)
                                                    ^^^^

Since you have configured City's country as a constant, you must supply this value when you initialize your City. Thus you have no way out; you must make capitalCity an Optional var so that it has some other initial value that is legal, namely nil. Your proposed solution actually works like this:

class Country
{
    let name: String
    var capitalCity: City! = nil // implicit or explicit

    init(name: String, capitalName: String)
    {
        self.name = name
        // end of initialization!
        // name is set (to name), and capitalCity is set (to nil)...
        // ... and so the compiler is satisfied;
        // now, we _change_ capitalCity from nil to an actual City,
        // and in doing that, we _are_ allowed to mention `self`
        self.capitalCity = City(name: capitalName, country: self)
    }
}
Avian answered 26/12, 2015 at 19:43 Comment(7)
what about memory leak?Dodeca
@Dodeca That is the problem that unowned solves.Avian
@matt: You could also declare capitalCity prefixing private(set) in order to prevent external changes of the variable.Jeopardous
@appzYourLife Sure, but that wouldn't change the fact that, given the structural relation between the two classes, capitalCity must be an Optional var in order to satisfy the compiler that the object is being correctly initialized — which is what the question was. "Is there any way to resolve the syntax error while keeping capitalCity a constant?" No.Avian
@matt: yes, I know it must be an Optional var. I was just proposing to change the property declaration in your solution to private(set) var capitalCity: City! = nil. Right now infact some code external to your definition of Country could set capitalCity = nil which is dangerous for an implicitly unwrapped optional property.Jeopardous
@appzYourLife Fine, then give that as an answer! I'll upvote it. But in fact the other answer (from Qbyte) has already shown how to prevent that. But my answer stands. The OP didn't ask how to prevent someone else from changing capitalCity; I answered the question the OP did ask.Avian
FYI. For cross reference in the Swift Programming Language, see Unowned References and Implicitly Unwrapped Optional Properties section: var capitalCity: City! is "an implicitly unwrapped optional property … that … has a default value of nil"Chrominance
J
26

Just do:

private(set) var capitalCity: City!

which gives you the read-only public interface you want.

I understand that you find var capitalCity: City! contrived. I'd say that the selected answer is truly contrived; it adds lines of code that have no purpose other than resolving a language-related issue. Lines like that are meaningless and meaning is what matters most in code.

Jamaaljamaica answered 21/12, 2019 at 8:39 Comment(2)
This should be the selected answerJorgejorgensen
This should be the selected answer +1Twinkle
A
0

Came across this recently while struggling with a similar problem and as of swift 5.5 (or possibly lower) there is another alternative that is interesting. If you convert Capital City to a lazy var you are actually able to use self in initialization.

class Country
{
    let name: String

    lazy var capitalCity: City = {
        City(name: capitalName, country: self)
    }()

    private let capitalName: String

    init(name: String, capitalName: String)
    {
        self.name = name
        self.capitalName = capitalName
    }
}

class City
{
    let name: String
    unowned let country: Country

    init(name: String, country: Country)
    {
        self.name = name
        self.country = country
    }
}

Cheers!

Auer answered 23/11, 2021 at 20:46 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.