Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Is it safe to force unwrap variables that have been optionally accessed in the same line of code?

someFunction(completion: { [weak self] in     self?.variable = self!.otherVariable }) 

Is this always safe? I access the optional self in the beginning of the statement, and personally I assume that the second part of this statement will never be executed if self is nil. Is this true? If self indeed is nil, the second part will never happen? And it will never happen that self could be 'nilled' during this single line of code?

like image 947
Sti Avatar asked Jul 26 '17 12:07

Sti


People also ask

Which operator is used to force unwrap an optional?

When you're certain that an instance of Optional contains a value, you can unconditionally unwrap the value by using the forced unwrap operator (postfix ! ). For example, the result of the failable Int initializer is unconditionally unwrapped in the example below.

What is forced unwrapping?

Forced unwrapping is an action done on the normal Optionals. Implicitly unwrapped Optionals are Optionals, usually used for class initialization and will pass values without exclamation mark when used.

Why use implicitly unwrapped optional?

Implicitly unwrapped optionals are a compromise between safety and convenience. If you declare a property as optional, then you need to use optional binding or optional chaining every time you access the value of the optional.

What is implicitly unwrapped optional in Swift?

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.


2 Answers

Optional Chaining from "The Swift Programming Language" gives the following example:

 let john = Person()  // ...  let someAddress = Address()  // ...  john.residence?.address = someAddress 

followed by (emphasis added):

In this example, the attempt to set the address property of john.residence will fail, because john.residence is currently nil.

The assignment is part of the optional chaining, which means none of the code on the right hand side of the = operator is evaluated.

Applied to your case: In

self?.variable = self!.otherVariable 

the right-hand side is not evaluated if self is nil. Therefore the answer to your question

If self indeed is nil, the second part will never happen?

is "yes". With regard to the second question

And it will never happen that self could be 'nilled' during this single line of code?

My original assumption was that once self has been determined to be != nil, a strong reference to self! is held throughout the evaluation of the statement, so that this can not happen. However (as @Hamish pointed out), this is not guaranteed. Apple engineer Joe Groff writes at Confirming order of operations in the Swift forum:

This isn't guaranteed. Releases may be optimized to happen earlier than this, to any point after the last formal use of the strong reference. Since the strong reference loaded in order to evaluate the left-hand side weakProperty?.variable is not used afterward, there is nothing keeping it alive, so it could be immediately released.
If there are any side effects in the getter for variable that cause the object referenced by weakProperty to be deallocated, nil-ing out the weak reference, then that would cause the force-unwrap on the right side to fail. You should use if let to test the weak reference, and reference the strong reference bound by the if let

like image 64
Martin R Avatar answered Oct 14 '22 11:10

Martin R


No, this is not safe

As pointed out by @Hamish in a comment below, Swift Compiler Engineer Joe Groff describes that there is no guarantee that a strong reference is held for the duration of the RHS' evaluation [emphasis mine]

Confirming order of operations

Rod_Brown:

Hi there,

I’m wondering about the safety of a type of access on a weak variable:

class MyClass {      weak var weakProperty: MyWeakObject?      func perform() {         // Case 1         weakProperty?.variable = weakProperty!.otherVariable          // Case 2         weakProperty?.performMethod(weakProperty!)     } } 

With the two cases above, is it guaranteed by Swift that the weakProperty can be force unwrapped at these positions?

I’m curious about the guarantees Swift makes about access during optional chaining E.g. are the weakProperty! accessors guaranteed to only fire iff the optional chaining determines first that the value is already non-nil?

Additionally, is the weak object guaranteed to be retained for the duration of this evaluation, or can the weak variable potentially be able to deallocate between the optional access and the method being called?

Joe_Groff:

This isn't guaranteed. Releases may be optimized to happen earlier than this, to any point after the last formal use of the strong reference. Since the strong reference loaded in order to evaluate the left-hand side weakProperty?.variable is not used afterward, there is nothing keeping it alive, so it could be immediately released. If there are any side effects in the getter for variable that cause the object referenced by weakProperty to be deallocated, nil-ing out the weak reference, then that would cause the force-unwrap on the right side to fail. You should use if let to test the weak reference, and reference the strong reference bound by the if let:

if let property = weakProperty {   property.variable = property.otherVariable   property.performMethod(property) } 

This should safer and also more efficient, since the weak reference is loaded and tested once instead of four times.


Given the answer quoted answer by Joe Groff above, my previous answer is moot, but I will leave it here as a possibly interesting (albeit failed) journey into the depths of the Swift compiler.


Historical answer reaching an in-correct final argument, but through an interesting journey, nonetheless

I'll base this answer upon my comment to @appzYourLife:s deleted answer:

This is pure speculation, but considering the somewhat close connection between many of the experienced Swift core devs and C++:s Boost lib, I would assume that weak reference is locked into a strong one for the lifetime of the expression, if this assigns/mutates something in self, much like the explicitly used std::weak_ptr::lock() of the C++ counterpart.

Let's have a look at your example, where self has been captured by a weak reference and is not nil when accessing the left hand side of the assignment expression

self?.variable = self!.otherVariable /* ^             ^^^^^-- what about this then?    |     \-- we'll assume this is a success */ 

We may look at the underlying treatment of weak (Swift) references in the Swift runtime, swift/include/swift/Runtime/HeapObject.h specifically:

/// Load a value from a weak reference.  If the current value is a /// non-null object that has begun deallocation, returns null; /// otherwise, retains the object before returning. /// /// \param ref - never null /// \return can be null SWIFT_RUNTIME_EXPORT HeapObject *swift_weakLoadStrong(WeakReference *ref); 

The key here is the comment

If the current value is a non-null object that has begun deallocation, returns null; otherwise, retains the object before returning.

Since this is based on backend runtime code comment, it is still somewhat speculative, but I would say that the above implies that when attempting to access the value pointed to by a weak reference, there reference will indeed be retained as a strong one for lifetime of the call ("... until returning").


To try to redeem the "somewhat speculative" part from above, we may continue to dig into how Swift handles access of a value via a weak reference. From @idmean:s comment below (studying the generated SIL code for an example like the OP:s) we know that the swift_weakLoadStrong(...) function is called.

So we'll start by looking into the implementation of the swift_weakLoadStrong(...) function in swift/stdlib/public/runtime/HeapObject.cpp and see where we'll get from there:

HeapObject *swift::swift_weakLoadStrong(WeakReference *ref) {   return ref->nativeLoadStrong(); } 

We find the implementation of the nativeLoadStrong() method of WeakReference from swift/include/swift/Runtime/HeapObject.h

HeapObject *nativeLoadStrong() {   auto bits = nativeValue.load(std::memory_order_relaxed);   return nativeLoadStrongFromBits(bits); } 

From the same file, the implementation of nativeLoadStrongFromBits(...):

HeapObject *nativeLoadStrongFromBits(WeakReferenceBits bits) {   auto side = bits.getNativeOrNull();   return side ? side->tryRetain() : nullptr; } 

Continuing along the call chain, tryRetain() is a method of HeapObjectSideTableEntry (which is essential for the object lifecycle state machine), and we find its implementation in swift/stdlib/public/SwiftShims/RefCount.h

HeapObject* tryRetain() {   if (refCounts.tryIncrement())     return object.load(std::memory_order_relaxed);   else     return nullptr; } 

The implementation of the tryIncrement() method of the RefCounts type (here invoked via an instance of a typedef:ed specialization of it) can be found in the same file as above:

// Increment the reference count, unless the object is deiniting. bool tryIncrement() {   ... } 

I believe the comment here suffices for us to use this method as an end point: if the object is not deiniting (which we've assumed above that it does not, as the lhs of assignment in the OP:s example is assumed to be successful), the (strong) reference count on the object will be increased, and a HeapObject pointer (backed by a strong reference count increment) will be passed to the assignment operator. We needn't study how the corresponding reference count decrement is eventually performed at the end of the assignment, but now know beyond speculation that the object associated with the weak reference will be retained as a strong one for the lifetime of the assignment, given that it has not been freed/deallocated at the time of the left hand side access of it (in which case the right hand side of it will never be processed, as has been explained in @MartinR:s answer).

like image 22
dfrib Avatar answered Oct 14 '22 10:10

dfrib