强制解开已在同一行代码中选择性访问的变量是不是安全?

Posted

技术标签:

【中文标题】强制解开已在同一行代码中选择性访问的变量是不是安全?【英文标题】:Is it safe to force unwrap variables that have been optionally accessed in the same line of code?强制解开已在同一行代码中选择性访问的变量是否安全? 【发布时间】:2018-01-01 19:01:51 【问题描述】:
someFunction(completion:  [weak self] in
    self?.variable = self!.otherVariable
)

总是安全吗?我在语句的开头访问了可选的self,我个人认为如果selfnil,则该语句的第二部分将永远不会执行。这是真的?如果self 确实是nil,那么第二部分永远不会发生?在这行代码中,self 永远不会被“取消”?

【问题讨论】:

这可能会帮助你***.com/questions/24468336/… 这是一个有趣的问题。投票赞成。今天测试它的工作方式很容易,但能否保证始终工作是个问题。它看起来很可疑,即使我知道它有效,我也不会使用它。使用if letguard 来表达你的意图很容易。 更好的问题是:你为什么要这样使用可选项? 我认为没有人能回答这个问题Is this always safe?。线程有很多边缘情况。 someFunction 是做什么的?在哪里调用完成?无论哪种方式,都不要这样做。 @MuhammadHassan,MartinR 在下面的回答解决了这个问题。 【参考方案1】:

不,这不安全

正如@Hamish 在下面的评论中指出的那样,Swift 编译器工程师Joe Groff describes 不能保证在 RHS 的评估期间持有强有力的参考 [强调我的]

确认操作顺序

Rod_Brown:

你好,

我想知道对弱变量进行某种访问的安全性:

class MyClass 

    weak var weakProperty: MyWeakObject?

    func perform() 
        // Case 1
        weakProperty?.variable = weakProperty!.otherVariable

        // Case 2
        weakProperty?.performMethod(weakProperty!)
    

对于上述两种情况,Swift 是否保证 weakProperty可以在这些位置强制解包吗?

我很好奇 Swift 在访问期间做出的保证 可选链weakProperty! 访问器是否保证 仅当可选链接首先确定该值是 已经不是nil了?

另外,弱对象是否保证被保留 此评估的持续时间,或者弱变量可能是 能够在可选访问和方法之间解除分配 叫什么?

Joe_Groff:

这不能保证。发布可能会被优化为更早发生 比这,在最后一次正式使用强之后的任何一点 参考。 由于加载了强引用以便评估 左边的weakProperty?.variable 之后就不用了, 没有什么可以让它活着,所以它可以立即 已发布。 如果变量的 getter 有任何副作用 导致weakProperty 引用的对象被释放, nil-ing 出弱引用,那么 会导致 在右侧强制展开失败。你应该使用 if let 来测试 弱引用,并引用由 if 绑定的强引用 让:

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

这应该更安全,也更有效,因为弱引用是 加载和测试一次而不是四次。


鉴于上面 Joe Groff 引用的答案,我之前的答案没有实际意义,但我将把它留在这里,作为深入 Swift 编译器深度的一次可能有趣(尽管失败)的旅程。


历史答案得出了一个不正确的最终论点,但经历了一段有趣的旅程

我将根据我对@appzYourLife:s 已删除答案的评论来回答这个问题:

这纯属猜测,但考虑到有些接近 许多经验丰富的 Swift 核心开发人员和 C++:s 之间的联系 Boost lib,我假设weak 引用被锁定在一个 表达式的生命周期中的强项,如果这分配/变异 self 中的一些东西,很像明确使用的 std::weak_ptr::lock() 对应的 C++。

让我们看一下您的示例,其中self 已被weak 引用捕获,并且在访问赋值表达式的左侧时不是nil

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

我们可以看看 Swift 运行时中weak (Swift) 引用的底层处理,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);

这里的关键是评论

如果当前值是一个已经开始释放的非空对象, 返回空值;否则,在返回之前保留对象

由于这是基于后端运行时代码注释,它仍然有些推测,但我会说上面暗示当尝试访问 weak 引用指向的值时,确实会保留引用为一个强大的调用生命周期(“...直到返回”)。


为了尝试弥补上面的“有点投机”部分,我们可能会继续深入研究 Swift 如何通过 weak 引用处理对值的访问。从@idmean:s comment below(研究生成的 SIL 代码,例如 OP:s)我们知道调用了 swift_weakLoadStrong(...) 函数。

所以我们将首先研究swift/stdlib/public/runtime/HeapObject.cppswift_weakLoadStrong(...) 函数的实现,看看我们将从那里得到什么:

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

我们从swift/include/swift/Runtime/HeapObject.h 中找到WeakReferencenativeLoadStrong()方法的实现

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

来自the same file,实现nativeLoadStrongFromBits(...)

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

继续沿着调用链,tryRetain()HeapObjectSideTableEntry 的一个方法(这对于the object lifecycle state machine 是必不可少的),我们在swift/stdlib/public/SwiftShims/RefCount.h 中找到了它的实现

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

RefCounts 类型的tryIncrement() 方法的实现(这里通过typedef:ed specialization of it 的实例调用)可以在in the same file as above 找到:

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

我相信这里的评论足以让我们将此方法用作终点:如果对象没有取消初始化(我们在上面假设它没有,就像 OP:s 中的分配 lhs假设成功),对象的(强)引用计数将增加,HeapObject 指针(由强引用计数增量支持)将传递给赋值运算符。我们不需要研究如何在赋值结束时最终执行相应的引用计数递减,但现在可以推测,与 weak 引用关联的对象将在赋值的生命周期内作为强对象保留,考虑到它在左侧访问时还没有被释放/解除分配(在这种情况下,它的右侧永远不会被处理,正如@MartinR:s answer 中所解释的那样)。

【讨论】:

很好的答案!我只需要检查一下,程序集似乎确实调用了这个函数,并且还调用了_swift_rt_swift_release,这似乎是这个调用的对应部分。 (虽然我真的觉得 Swift 汇编很难理解。) " for the life of the expression" you mean self?.variable = self!.otherVariable this的生命周期是从读取左操作数开始到读取右操作数结束? @idmean 仍然有些投机性的想法,因为我没有遵循调用链或最终对象释放和释放之前所需的所有条件。但是这个兔子洞对我来说有点太深了,我现在无法深入了解......很高兴为swift_weakLoadStrong 调用生成您生成的 SIL 验证,谢谢! @Honey 赋值运算符在 Swift 中有些特殊,但当我提到生命周期时,我的意思是直到赋值运算符完成其两个操作数的工作。与只是一个函数的常规 Swift 运算符相比:当函数返回被调用者站点的表达式评估完成时(即对运算符的调用),这将对应于(有点推测)锁的端点在weak 参考。 (复制我在 Martin 的回答下的评论,让您看到):According to Joe Groff,不能保证在 RHS 评估期间持有强有力的参考。【参考方案2】:

Optional Chaining 来自“Swift 编程语言” 举个例子:

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

后跟(强调):

在此示例中,尝试设置 john.residence 的地址属性将失败,因为 john.residence 当前为 nil。

赋值是可选链的一部分,这意味着=运算符右侧的代码都不会被计算。

适用于您的案例:在

self?.variable = self!.otherVariable

如果selfnil,则评估右侧。 因此,您的问题的答案

如果 self 确实是 nil,那么第二部分永远不会发生?

是“是”。关于第二个问题

在这行代码中 self 永远不会被“nilled”?

我最初的假设是一旦self被确定为!= nil, 在整个评估过程中都强烈引用了self! 声明,以免发生这种情况。但是(正如@Hamish 指出的那样), 这不能保证。 Apple 工程师 Joe Groff 在Confirming order of operations 写信 Swift 论坛:

这不能保证。发布可能会被优化为比这更早,直到最后一次正式使用强引用之后的任何时间。由于为评估左侧weakProperty?.variable而加载的强引用在之后没有使用,因此没有任何东西可以保持它的活力,因此可以立即释放它。 如果变量的getter 中有任何副作用导致weakProperty 引用的对象被释放,nil-ing 弱引用,那么这将导致右侧的强制展开失败。 您应该使用 if let 来测试弱引用,并引用 if let 绑定的强引用

【讨论】:

我认为这是正确的答案。如果左侧是任何赋值操作的nil,则不会计算右侧。考虑这样的事情:instance?.val = ([] as [Int])[0](假设valInt)。该表达式的右侧将导致崩溃,但如果 instancenil,则不会计算。 @JAL true 但想象一下如果左侧不是nil,然后因为它是一个异步操作,在读取右侧操作数之前,self 变成了nil。这可能会导致正确的操作数崩溃? @Honey 如果对 self 的强引用被捕获为 dfri 回答状态中的代码注释,则不会。我认为那里的表达式的生命周期意味着整行(赋值的两侧)。 @Honey 左侧不是 nil 不适用于我的(简化)示例。我指望左边是 nil 来展示一个赋值运算符短路的例子。 According to Joe Groff,不能保证在 RHS 评估期间保持强参考。【参考方案3】:

更正前:

我认为其他人对您问题的详细信息的回答比我要好得多。

但除了学习。如果您确实希望您的代码能够可靠地工作,那么最好这样做:

someFunction(completion:  [weak self] in
    guard let _ = self else
        print("self was nil. End of discussion")
        return
    
    print("we now have safely 'captured' a self, no need to worry about this issue")
    self?.variable = self!.otherVariable
    self!.someOthervariable = self!.otherVariable

修正后。

感谢 MartinR 在下面的解释,我学到了很多东西。

阅读这个伟大的post on closure capturing。我盲目地认为,每当您在括号 [] 中看到某些内容时,这意味着它已被捕获并且其值不会改变。但我们在括号中所做的唯一事情是我们将weak 化为它并让我们自己知道它的价值可以变成nil。如果我们做了像[x = self] 这样的事情,我们会成功捕获它,但我们仍然会遇到问题:持有指向self 本身的强指针并创建内存循环。 (从某种意义上说,这很有趣,从创建内存周期到创建崩溃,因为值被释放,因为你弱化了它)。

所以总结一下:

    [capturedSelf = self]

    创建内存循环。不好!

    [weak self] 
    in guard let _ = self else
    return
     
    

    (如果你在之后强制 self 解包可能会导致崩溃) guard let 完全没用。因为下一行,self 仍然可以变成nil。不好!

    [weak self] 
    self?.method1()
    

    (如果你在之后强制self 解包,可能会导致崩溃。如果self 不是nil,则会通过。如果selfnil,则会安全失败。)这很可能是什么你要。这

    [weak self] in 
    guard let strongSelf = self else 
    return
     
    

    如果self 被释放,将安全失败;如果不是nil,则继续。但这有点违背了目的,因为当它删除它自己的引用时,您不需要与self 通信。我想不出一个好的用例。这可能没用!

【讨论】:

我看不出这与Matt's answer 有何不同(或更好)。 情况更糟。这里没有明确地引用自我。 请注意,guard let _ = self 确实安全地捕获自我。你必须绑定它,例如作为guard let strongSelf = self,然后在闭包中使用strongSelf [weak self] 仅表示 self 被弱捕获,如果调用闭包则可以为 nil。您在条目上验证 self!=nil,但它可能稍后会变为 nil。 如果你要留下答案,不要只把“这个答案不正确”放在顶部。编辑答案以解释您最初犯的错误是什么,为什么是错误的,以及如何避免它。换句话说,如果是您认为有价值的 cmets,请将它们整合到答案本身中,将功劳归于最初发布它们的用户(按名称)。【参考方案4】:

这总是安全的

没有。你不是在做“弱强舞”。去做吧!每当你使用weak self 时,你应该安全地解开 Optional,然后只引用解开的结果——就像这样:

someFunction(completion:  [weak self] in
    if let sself = self  // safe unwrap
        // now refer only to `sself` here
        sself.variable = sself.otherVariable
        // ... and so on ...
    
)

【讨论】:

@Sti 这并不能直接回答您的问题,这是一个理论问题而不是实际问题。但它确实为您在实践中应该做什么提供了指导。即使在您知道它会很好的情况下强制展开仍然不是解决问题的好方法。它不是错误的答案,而是为您提供了正确的做事方式。【参考方案5】:

文档清楚地states 指出,如果赋值的左侧被确定为 nil,则不会评估右侧。 但是,在给定的示例中,self弱引用,可能会在可选检查通过后立即释放(并无效),但就在 force-unwrap 发生之前,使整个表达式 nil-unsafe .

【讨论】:

以上是关于强制解开已在同一行代码中选择性访问的变量是不是安全?的主要内容,如果未能解决你的问题,请参考以下文章

如何强制 ListView 在同一行中显示来自 ObservableCollection 不同元素的两个不同数据

如何强制内联 div 保持在同一行?

在同一行声明的多个字段的可访问性

Kotlin Null 安全警告 Unexpected tokens(使用 ; 在同一行分隔表达式)

四月二十五号日报

如何在 Swift 4 中安全地解开字典值?