53

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?

Community
  • 1
  • 1
Ultrasaurus
  • 3,031
  • 2
  • 33
  • 52

4 Answers4

37

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)
    }
}
Qbyte
  • 12,753
  • 4
  • 41
  • 57
  • 1
    this is a great answer. It gets to the interface that I wanted, though the explanation by @matt was fabulous – Ultrasaurus Dec 26 '15 at 20:12
  • 4
    swift is a weird language for initialization. – WestCoastProjects May 25 '20 at 23:02
  • 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. – svenyonson Nov 14 '20 at 19:50
  • @svenyonson 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. – Qbyte Nov 14 '20 at 20:41
30

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)
    }
}
matt
  • 515,959
  • 87
  • 875
  • 1,141
  • what about memory leak? – user3441734 Dec 26 '15 at 19:46
  • @user3441734 That is the problem that `unowned` solves. – matt Dec 26 '15 at 19:48
  • @matt: You could also declare `capitalCity` prefixing `private(set)` in order to prevent external changes of the variable. – Luca Angeletti Dec 26 '15 at 21:19
  • @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. – matt Dec 26 '15 at 23:35
  • @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. – Luca Angeletti Dec 27 '15 at 00:08
  • @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. – matt Dec 27 '15 at 00:26
  • 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"](https://developer.apple.com/library/content/documentation/Swift/Conceptual/Swift_Programming_Language/AutomaticReferenceCounting.html) – marc-medley Feb 25 '18 at 03:00
25

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.

meaning-matters
  • 21,929
  • 10
  • 82
  • 142
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!

Andrew Zimmer
  • 3,183
  • 1
  • 19
  • 18