How does the optional chaining work here

I do not seem to understand how optional chaining works. The first print works as I expect. The second works, but how and why? The third one fails, but I thought I was accessing .count on Optional("") because that is what I get when I do print((s as? SomethingElse)?.some_string)

class Something {}

class SomethingElse: Something {var some_string: String = ""}

let s: Something = SomethingElse()

// explicit parenthesis - works
print(((s as? SomethingElse)?.some_string)?.count as Any)
// works but how?
print((s as? SomethingElse)?.some_string.count as Any)

// does not work
// error: cannot use optional chaining on non-optional value of type 'String'
// print((s as? SomethingElse)?.some_string?.count as Any)

The optional chaining operator covers the entire postfix-operator chain to its right: member accesses, calls, and subscripts. It does not pass out of parentheses. If the optional is nil, all of those operators and their sub-expressions are skipped; otherwise, it behaves exactly as if the value were non-optional, and then the result is coerced to an optional type. So within (s as? SomethingElse)?.some_string.count, (s as? SomethingElse)?.some_string has non-optional String type.

When an optional chaining operator is used as the left-hand side of an assignment operator (such as = or +=), the chain also covers the right-hand side of the assignment operator. For example, in x?.property += foo(), foo is not called if x is nil.

8 Likes

Thank you for the reply @John_McCall , just a clarification. You mention

So within (s as? SomethingElse)?.some_string.count , (s as? SomethingElse)?.some_string has non-optional String type.

When I do print((s as? SomethingElse)?.some_string) I get Optional(""). Is it right in understanding that once the "chaining ends" whatever is the return value gets wrapped to an optional (unless the attribute / method is already optional, in which case it remains to be optional)? Within the chain, the original type (defined along with var / let) of the attribute remains the same (no conversion to optional if its non optional) as it is as long as there are more things to chain or unless the chaining ends?

1 Like

That's exactly right. Within the chain, everything happens exactly as if all the values tested by ? in the chain were non-optional. If any of them is actually nil, control jumps immediately to the end of the chain, bypassing any other steps, and the result of the expression is nil. At the end of the chain, the result is made optional if it isn't already.

For a more complex example, consider an expression like this with many different ? tests inside it:

let result = a?.b(x).c?.d(y).e?.f(z)

The behavior of this expression is essentially this:

typealias ChainResult = /* the return type of f, made optional */

func chain() -> ChainResult {
  guard let tmp1 = a else { return nil }
  guard let tmp2 = tmp1.b(x).c else { return nil }
  guard let tmp3 = tmp2.d(y).e else { return nil }
  return tmp3.f(z)
}

let result = chain()
4 Likes