Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why does using dynamicType on a force unwrapped nil optional value type work?

Tags:

swift

I was really confused to find that the following code simply refuses to crash with the typical “Unexpectedly found nil while unwrapping an Optional value” exception that you’d expect from force unwrapping bar.

struct Foo {
    var bar: Bar?
}
struct Bar {}

var foo = Foo()

debugPrint(foo.bar) // nil
debugPrint(foo.bar!.dynamicType) // _dynamicType.Bar

It seems dynamicType is somehow able to fallback to the defined type of bar – without crashing.

Note this only appears to happen when Bar is defined as a value type (as @dfri says), Foo is a struct or final class (as pointed out by @MartinR) & foo is mutable.

I did initially consider that it could just be a compiler optimisation due to the fact that the type of bar is known at compile time, so therefore the force unwrapping could have been optimised away – but it also crashes when Bar is defined as a final class. Furthermore, if I set the "Optimisation Level" to -Onone, it still works.

I’m inclined to think this is a weird bug, but would like some confirmation.

Is this a bug or a feature with dynamicType, or am I simply missing something here?

(Using Xcode 7.3 w/ Swift 2.2)


Swift 4.0.3 update

This is still reproducible (with an even more minimal example) in Swift 4.0.3:

var foo: String?
print(type(of: foo!)) // String

Here we're using dynamicType's successor, type(of:), to get the dynamic type; and like the previous example, it doesn't crash.

like image 932
Hamish Avatar asked Apr 24 '16 13:04

Hamish


People also ask

What will happen if you try to unwrap an optional that contains nil like so?

Unwrap an optional type with the nil coalescing operator If a nil value is found when an optional value is unwrapped, an additional default value is supplied which will be used instead. You can also write default values in terms of objects.

What is an implicitly unwrapped optional?

Checking an optionals value is called “unwrapping”, because we're looking inside the optional box to see what it contains. Implicitly unwrapping that optional means that it's still optional and might be nil, but Swift eliminates the need for unwrapping.

What is forced unwrapping in Swift?

In these cases, Swift lets you force unwrap the optional: convert it from an optional type to a non-optional type. That makes num an optional Int because you might have tried to convert a string like “Fish” rather than “5”.


1 Answers

This is indeed a bug, which has been fixed by this pull request, which should make it into the release of Swift 4.2, all things going well.


If anyone’s interested in the seemingly bizarre requirements to reproduce it, here’s a (not really) brief overview on what was happening...

Calls to the standard library function type(of:) are resolved as a special case by the type-checker; they’re replaced in the AST by a special “dynamic type expression“. I didn't investigate how it's predecessor dynamicType was handled, but I suspect it did something similar.

When emitting an intermediate representation (SIL to be specific) for a such an expression, the compiler checks to see if the resulting metatype is “thick” (for class and protocol-typed instances), and if so emits the sub-expression (i.e the argument passed) and gets its dynamic type.

However, if the resulting metatype is “thin” (for structs and enums), the compiler knows the metatype value at compile time. Therefore the sub-expression only needs to be evaluated if it has side effects. Such an expression is emitted as an “ignored expression”.

The problem was with the logic in emitting ignored expressions that were also lvalues (an expression that can be assigned to and passed as inout).

Swift lvalues can be made up of multiple components (for example, accessing a property, performing a force unwrap, etc.). Some components are “physical”, meaning that they produce an address to work with, and other components are “logical”, meaning that they comprise of a getter and setter (just like computed variables).

The problem was that physical components were incorrectly assumed to be side-effect free; however force unwrapping is a physical component and is not side effect free (a key-path expression is also a non-pure physical component).

So ignored expression lvalues with force unwrap components will incorrectly not evaluate the force unwrapping if they’re only made up of physical components.

Let’s take a look at a couple of cases that currently crash (in Swift 4.0.3), and explain why the bug was side-stepped and the force unwrap was correctly evaluated:

let foo: String? = nil
print(type(of: foo!)) // crash!

Here, foo is not an lvalue (as it’s declared let), so we just get its value and force unwrap.

class C {} // also crashes if 'C' is 'final', the metatype is still "thick"
var foo: C? = nil
let x = type(of: foo!) // crash!

Here, foo is an lvalue, but the compiler sees that the resulting metatype is “thick”, and so depends on the value of foo!, so the lvalue is loaded, and the force unwrap is therefore evaluated.

Let’s also take a look at this interesting case:

class Foo {
  var bar: Bar?
}
struct Bar {}

var foo = Foo()
print(type(of: foo.bar!)) // crash!

It crashes, but it won’t if Foo is marked as final. The resulting metatype is “thin” either way, so what difference does Foo being final make?

Well, when Foo is non-final, the compiler cannot just refer to the property bar by address, as it may be overridden by a subclass, which may well re-implement it as a computed property. So, the lvalue will contain a logical component (the call to bar’s getter), therefore the compiler will perform a load to ensure the potential side effects of this getter call are evaluated (and the force unwrap will also be evaluated in the load).

However when Foo is final, the property access to bar can be modelled as a physical component, i.e it can be referred to by address. Therefore the compiler incorrectly assumed that because all the lvalue's components are physical, it could skip evaluating it.

Anyway, this issue is fixed now. Hopefully someone finds the above ramble useful and/or interesting :)

like image 118
Hamish Avatar answered Oct 23 '22 06:10

Hamish