First, it seems you're conflating two different concepts in Swift: nilability (nullability) and ARC ownership reference types.
In Swift, variables (regardless of whether they're value or reference types) can only be set to nil
(by your code or the type system) if they're wrapped in Optional<MyType>
(AKA MyType?
/MyType!
) because nil
is just an alias for Optional
enum's .none
case. (Caveat: This discussion is only about managed Swift/Obj-C objects; it excludes direct memory access via Unmanaged
/UnsafePointer
/etc.)
Swift object references tell ARC (the Automatic Reference Counting system) how to count ownership of an object, and it gives you the options of strong (the default), weak
, unowned
(AKA unowned(safe)
), and unowned(unsafe)
. All of those can be used with or without Optional
-wrapping except weak
— weak
is a special case that requires Optional
-wrapping.
A strong reference maintains a +1 reference count on the object. So as long as your variable is in scope the object is guaranteed to remain in scope.
If your object ref is wrapped in an Optional
, that won't have any affect on the reference counting— when the optional is .some(myTypeReference)
it's the same as a non-optional, and when the optional is .none
/nil
, it's the same as your variable being out of scope (e.g. you had a local var ref in a function and the function has ended).
A weak
reference does not affect the reference count of the object directly, and requires the variable to be wrapped in an Optional
. When the number of strong references to the object elsewhere drops down to 0 refs, Swift/Obj-C/LLVM will deinit/dealloc the object, and all weak Optional<MyType>
refs are set to .none
/nil
.
An unowned
/ unowned(safe)
reference is effectively a weak reference without the Optional
wrapping, and where the Swift runtime functionality doesn't do the nil
-setting when the object is deallocated but will abort hard if you try to use the reference after it's been inited.
It's best to think about it in terms of what happens in the Swift runtime source code— whenever you try to use an unowned reference, Swift effectively runs if (object->refCounts == 0) swift::swift_abortRetainUnowned(object);
. (The actual implementation is a bit more complex and does stuff ahead of time; see the HeapObject source.)
You can use an Optional
wrapping with unowned
but like strong, that doesn't directly affect the object ownership— if the optional is .some(myTypeReference)
then everything above about unowned
applies, and if the optional is .none
then none of it applies (effectively the same as a non-Optional
that has gone out of scope, as above).
An unowned(unsafe)
reference is effectively a plain C pointer to your object— MyType *myVar
. According to the Swift Book “If you try to access an unsafe unowned reference after the instance that it refers to is deallocated, your program will try to access the memory location where the instance used to be, which is an unsafe operation.” so the same semantics as a raw C pointer.
There's a little more than goes on in the Swift runtime with unowned(unsafe)
in debug builds, but I believe in release builds that's stripped out and it is literally an unsafe memory pointer. (That's true for a lot of things in Swift; e.g. simd operations are horribly slowly implemented with Swift code and for loops in debug builds, but in release builds they compile into simple simd opcodes).
It really is helpful to think of NULL
in Swift as not existing; instead you have a built-in Optional
enum that can be .none
or can be .some(…)
with a value type's value or a nonnull reference type's pointer. And only Swift code you write or a Swift lib you use changes the Optional
's state, with a special exception for only weak
.
So to address your assertions and questions:
“we have weak typing: the object can become nil”
Effectively, but not exactly. Weak requires Optional
wrapping around your var's type and the Swift runtime will set that Optional
to .none
/nil
for you.
“we have unowned typing: the object can become nil”
False. The object reference isn't required to be wrapped in Optional
, so there is no such thing as a nil
/.none
state for that reference.
Also, unowned
is not a typing (it doesn't change the type of data you're referencing), it's a variable annotation (it affects how arc handles memory management when your variable is in scope or is used).
True assertions would be:
- “we have unowned/unowned(safe) variable annotation: the runtime will abort if the object reference is used after the object has been dealloced”
- “we have unowned(unsafe) variable annotation: the reference will just use the literal RAM if it can (which can lead to garbage data for encapsulated value type vars, and EXC_BAD_ACCESS for encapsulated ref type vars)”
“The only difference between unowned and weak, is performance. Since unowned has no checking, it is faster. There is absolutely no other difference.”
False. Unowned(safe) and weak are similar in implementation— the difference is that when the object is deallocated, the compiler will flag an unowned(safe)
var to abort the process if used, whereas a weak
var is expected to be Optional
-wrapped (nilable) and the runtime will set the Optional
to nil
for you.
Taking that as “The only difference between unowned(unsafe) and weak…”— sort of true but not exactly. Again, weak
is only allowed on a variable of Optional<…>
type, so there's a lot of tracking of the object so it can set your Optional
to .none
. unowned(unsafe)
is far closer to a strong reference without the ref count +1— it just accesses the memory, so yeah, there would be a mild performance increase there.
So change that to:
- “The only difference between unowned/unowned(safe) and weak, is the runtime will set the weak var to
nil
for you, whereas unowned(safe) will check if the object is still alive, and abort the process if not.”
- “The only difference between unowned(unsafe) and strong, is the runtime maintains +1 ref count for every strong reference, guaranteeing the object is kept alive, whereas unowned(unsafe) doesn't touch the ref count, and just assumes the memory it points to is correct.”
"There is, absolutely, no reason to use unowned, other than if the extra performance over weak is needed."
For unowned(safe)
, there's no performance benefit, so there's no real reason to use unowned
, period. It should be noted that I read somewhere that the intent of unowned
is to be unowned(safe)
in debug builds, and unowned(unsafe)
in release builds, but I'm not sure if that's actually implemented in the current Swift runtime.
The Swift docs say “Unlike a weak reference, however, an unowned reference is used when the other instance has the same lifetime or a longer lifetime.” So effectively, there's no reason to use unowned(safe)
instead of weak
. But semantically you might want a stronger check that the code is only run when the object exists— the same use-case of using MyType!
instead of MyType?
.
So the benefit is that unowned(safe)
will fail hard and fast if the object is gone. Weak requires using an Optional
which has a .none
state that your code can set explicitly. unowned(safe)
does not— it's a valid reference as soon as it's assigned an object ref, and permanently invalid as soon as the object has begun deallocating.
Taking that as "There is, absolutely, no reason to use unowned(unsafe), other than if the extra performance over weak is needed."
There are situations where the compiler has already invalidated weak
and unowned(safe)
references, but you know the data's still there— calling code from within deinit { … }
is a common use-case, since the Swift runtime invalidates weak
/unowned(safe)
before the reference-owning object's deinit
starts. So for example if you have a class that needs to run a self
-capturing closure one last time during deinit
, that closure needs to be { [unowned(unsafe) self] in … }
— using [weak self]
will already be nil and using [unowned self]
will already be abort-triggered (and of course, just using [self]
will cause retain cycles and the deinit will never be called).
There are also situations where the memory you're accessing may not be managed by ARC (maybe you're doing something unusual in C code with remote objects or lazy-deserialized objects, or a bridge to another language like Obj-C or C++ or Ruby or C#). So yeah, any situation where ARC actually isn't handling the lifespan of the object because you're doing something special on the C/Obj-C side— which is less uncommon than you think, since Swift bridges nicely to Obj-C, and Obj-C can either be in ARC or MRR mode (Manual Retain Release). Unusual Obj-C MRR lifecycles weren't uncommon 1-2 decades ago before ARC (Modern Objective-C / LLVM 3.0 / Xcode 4.2 / 2011), when many Objective-C programmers were very familiar with handling memory themselves like in C/C++.