user3690467
user3690467

Reputation: 3377

Non-optional property of optional struct being shown as optional

Town struct:

struct Town {
    var population: Int = 5_422
    var numberOfStoplights: Int = 4

    func printDescription() {
        print("The town has \(population) people and \(numberOfStoplights) stoplights")
    }

    mutating func changePopulation(by amount: Int) {
        population += amount
    }
}

Monster class:

class Monster {
    var town: Town?
    var name = "Monster"

    func terrorizeTown() {
        if town != nil {
            print("\(name) is terrorizing a town")
        } else {
            print("\(name) hasn't found a town to terrorize yet")
        }
    }
}

Zombie class:

class Zombie: Monster {
    var walksWithLimp = true

    final override func terrorizeTown() {

        if town?.population > 0 {
            town?.changePopulation(by: -10)
        }

        super.terrorizeTown()
    }
}

I'm getting a compiler error on town?.population saying 'value of optional Int not unwrapped...'. I haven't declared population to be an optional and if I change this to town?.population? it says population is a non-optional Int. Both these errors contradict each other, what's going on?

Upvotes: 0

Views: 427

Answers (4)

dfrib
dfrib

Reputation: 73176

Optional chaining, even when querying non-optional methods and properties, will yield a result that is an optional

Consider the following example:

struct Foo {
    let bar: Int // not: not an optional
    init(_ bar: Int) { self.bar = bar }
}

let foo: Foo? = Foo(42) // 'foo' is an optional

In case we are not interested particularly in foo, but rather specifically in its member bar, we may use optional chaining to query access to bar of foo, which will give us the value of bar wrapped in .some(...) in case foo is not nil, and nil otherwise. I.e., such a query (even to a non-optional property as `bar) will yield an optional:

let optBar = foo?.bar // this is now an optional (Optional<Int>)

To reflect the fact that optional chaining can be called on a nil value, the result of an optional chaining call is always an optional value, even if the property, method, or subscript you are querying returns a nonoptional value.

Before we proceed, we also note that in Swift 3 we may no longer compare optional operands by the Comparable operators, as stated in the following accepted and implemented Swift evolution proposal:

Hence, if you'd like to conditionally proceed into an if block given the following:

  • foo is not nil, and
  • if so, bar is larger than 0

Then you first need to, in some manner, achieve a concrete value of (the possibly existing) bar prior to comparing it to 0.

You can do this e.g. a combined optional binding and boolean conditional if statement:

if let nonOptionalBar = foo?.bar, nonOptionalBar > 0 {
    print("'foo' is not nil and its 'bar' member is larger than 0")
} // prints "'foo' is not nil and its 'bar' member is larger than 0"

If you don't plan to use the value bar, however (and only wants to decrease it's population), you could make use of the nil coalescing operator to construct a conditional if statement that will fail given that foo is nil or its member bar is not greater than 0:

if (foo?.bar ?? -1) > 0 {
    print("'foo' is not nil and its 'bar' member is larger than 0")
}

Here, (foo?.bar ?? -1) will result in -1 if foo is nil (which will subsequently result in false for -1 > 0), or result in the value of bar (as a non-optional concrete Int) in case foo is not nil.

We could also make use of the map method of Optional to conditionally (that foo is not nil) test the comparison, and if foo is nil, supply false as a default value using the nil coalescing operator on the return from the call to map.

if foo.map({ $0.bar > 0 }) ?? false {
    print("'foo' is not nil and its 'bar' member is larger than 0")
}

The three alternatives applied to your example:

if let population = town?.population, population > 0 {
    town?.changePopulation(by: -10)
}

if (town?.population ?? -1) > 0 {
    town?.changePopulation(by: -10)
}

if town.map({ $0.population > 0 }) ?? false {
    town?.changePopulation(by: -10)
}

Upvotes: 2

Sagar Chauhan
Sagar Chauhan

Reputation: 5823

You must initialise instance of you structure Town, otherwise it is always return nil. So I see your code, Town? is always return nil.

Upvotes: 0

taipingeric
taipingeric

Reputation: 715

Because town?.population is still Int?, and you can't compare Int? with Int unless you get the Int from Int?

So the code could be

if let town = town, town.population > 0 {
    town.changePopulation(by: -10)
}
super.terrorizeTown()

Upvotes: 0

rghome
rghome

Reputation: 8819

The expression returns an optional Int but you are still testing it using non-optional syntax. You need to use if let.

if let pop = town?.population {
    if pop > 0

etc.

The optional chain does not remove the optionality. It simply cascades it so that any nil in the chain causes a nil to pop out at the end. But the result is still optional.

Upvotes: 0

Related Questions