【问题标题】:Weak method argument semantics弱方法参数语义
【发布时间】:2015-06-18 00:43:13
【问题描述】:

有没有办法指定特定方法参数的语义较弱?

详细来说,这是一个按预期工作的 Objective-C 示例代码:

- (void)runTest {  
    __block NSObject *object = [NSObject new];  
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{  
        [self myMethod:object];  
    });  
    // to make sure it happens after `myMethod:` call  
    dispatch_async(dispatch_get_main_queue(), ^{  
        object = nil;  
    });  
}  
- (void)myMethod:(__weak id)arg0 {  
    NSLog(@"%@", arg0); // <NSObject: 0x7fb0bdb1eaa0>  
    sleep(1);  
    NSLog(@"%@", arg0); // nil  
}  

这是 Swift 版本,不是

public func runTest() {  
    var object: NSObject? = NSObject()  
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)) {  
        self.myMethod(object)  
    }  
    dispatch_async(dispatch_get_main_queue()) {  
        object = nil  
    }  
}  
private func myMethod(arg0: AnyObject?) {  
    println("\(arg0)") //Optional(<NSObject: 0x7fc778f26cf0>)  
    sleep(1)  
    println("\(arg0)") //Optional(<NSObject: 0x7fc778f26cf0>)  
}  

我是否正确假设在 Swift 版本中的方法调用之间 arg0 无法变为 nil ? 谢谢!

更新一位来自 Apple Dev.Forums 的用户指出 sleep 不是一个好用的函数,连续的分派可能会导致竞争条件。虽然这些可能是合理的担忧,但这只是一个示例代码,问题的重点是传递弱参数。

【问题讨论】:

  • 目前不可能,您可以在 Swift 2.0 之前使用 [weak yourObject] 在关闭开始时这样做。另请参阅here,其中显示了一些解决方法
  • 我知道弱闭包可能是一种解决方案,但它并不能解决问题,我不希望 API 的所有消费者都知道我的实现细节。不过在 Swift 2 中有可能吗?
  • 消费者如何知道你的 API 实现是这样的?
  • 因为在函数签名中封装弱语义似乎是不可能的,这意味着我的 API 的使用者将始终需要注意传递弱引用。否则,我将在函数体的生命周期内延长传递参数的生命周期
  • 为什么你认为第二个 dispatch_async 调用发生在 myMethod: 被调用之前?可能,是的 - 保证?不,我想。

标签: swift arguments automatic-ref-counting weak-references


【解决方案1】:

Swift 没有“弱 args”……但这可能只是因为 Swift (3.0) args 是不可变的(相当于 lets)和 weak Swift 中的东西需要同时是 varOptional.

也就是说,确实有一种非常简单的方法可以实现与弱 args 等效的操作——使用 weak var 本地 (释放要释放的 arg-var)。这是有效的,因为 Swift 直到当前作用域结束时才会挂起 vars(就像 C++ 那样严格);而是在最后一次使用它们之后从作用域中释放变量 (这使得 lldb-ing 有时成为 PitA,但无论如何)

以下示例在 macOS 10.11.6 上的 Xcode 8.2.1 上的 Swift 3.0.2 中始终有效:

class Test
{
    func runTest() {
        var object:NSObject? = NSObject()
        myMethod(arg0: object)

        DispatchQueue.main.asyncAfter(
            deadline: DispatchTime.now() + 1.0,
            qos: .userInteractive,
            flags: DispatchWorkItemFlags.enforceQoS
        ){
            object = nil
        }
    }

    func myMethod(arg0:AnyObject?) {
        weak var arg0Weak = arg0
        // `arg0` get “released” at this point.  Note: It's essential that you 
        //   don't use `arg0` in the rest of this method; only use `arg0Weak`.

        NSLog("\(arg0Weak)"); // Optional(<NSObject: 0x600000000810>)

        DispatchQueue.main.asyncAfter(
            deadline: DispatchTime.now() + 2.0,
            qos: .userInteractive,
            flags: DispatchWorkItemFlags.enforceQoS
        ){
            NSLog("\(arg0Weak)"); // nil
        }
    }
}

Test().runTest()

请注意,如果您在 Playground 中尝试此操作,则 Playground 将在 DispatchQueues 触发之前完成执行。让可执行文件无限期运行的最简单方法(我所做的)是创建一个新的 Cocoa 应用程序并将上面的所有代码粘贴到 func applicationDidFinishLaunching(_:Notification) { … } (是的,逐字逐句——Swift 允许嵌套在方法中的类定义).


作为对 thread-safety-leguring 的回应,您已经在示例中使用 dispatch_asyncsleep 来证明弱 args 确实是真正的交易,这是一个完整的 - main.m-source 测试的单线程和无队列变体:

#import <Foundation/Foundation.h>


@interface Test : NSObject 
- (void)runTest;
- (void)myMethod:(__weak id)arg0 callback:(void (^)())callback;
@end


int main(int argc, const char * argv[]) {
    @autoreleasepool {
        [[Test new] runTest];
    }
    return 0;
}


@implementation Test

- (void)runTest {
    __block NSObject *object = [NSObject new];
    [self myMethod:object callback:^{
        object = nil;
    }];
}

- (void)myMethod:(__weak id)arg0 callback:(void (^)())callback {
    NSLog(@"%@", arg0); // <NSObject: 0x100400bc0>
    callback();
    NSLog(@"%@", arg0); // (null)
}

@end

【讨论】:

  • 我相信你也可以通过隐藏参数来避免重用:即弱 var arg0 = arg0
  • 我的测试在这种情况下失败了:gist.github.com/garvankeeley/e85c2adb8e3079e4fe3c17ae62df478c
  • “这很有效,因为 Swift 在当前范围结束之前不会挂在 vars 上”--> 你有这方面的文档吗?
  • @GarvanKeeley 不,只是观察行为,尤其是使用 LLDB。有很多次我想检查一个 var 的值,但我已经超越了最后一次使用,它已经被释放了。 Swift 4 中的情况可能发生了变化(尽管我对此表示怀疑),但我在 Swift 2 和 3 中经常观察到这一点。
【解决方案2】:

暂时无法使用语言语法。

我认为这是目前最接近的解决方法。

public struct Weak<T> where T: AnyObject {
    public weak var object: T?

    public init(_ object: T?) {
        self.object = object
    }
}

func run(_ a: Weak<A>) { 
    guard let a = a.object else { return }
}

【讨论】:

    【解决方案3】:

    有没有办法指定特定方法参数的语义较弱?

    这不是您的 Objective-C 代码示例所做的。您意外地获得了几乎弱的语义,并且您具有真正的弱引用所没有的未定义行为(竞争条件)。

    myMethod 可以在任何序列点(第一个 NSLog 语句或第二个语句,甚至在 NSLog 的中间某处......即使 ARC 没有忽略)发送消息到 la-la-land arg0 的保留,您仍在竞争主队列释放或更糟 - 保留僵尸对象)。

    声明为__block 只是意味着在堆环境中为块分配一个槽(因为dispatch_async 保证让块逃逸,它将从堆栈分配的块提升到堆块,并且其中之一该堆块环境中的存储槽将用于您的__block变量。在ARC下,该块将自动调用Block_copy,也许更恰当地命名为Block_copy_to_heap)。

    这意味着块的两个执行实例都将指向同一个内存位置。

    如果它有帮助,想象一下这个非常愚蠢的代码,它有一个明显的竞争条件。有 1000 个块同时排队等待修改 unsafe。我们几乎可以保证在 if 块中执行讨厌的语句,因为我们的赋值和比较不是原子的,而且我们正在争夺同一个内存位置。

        static volatile size_t unsafe = 0;
    
        dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    
        dispatch_apply(1000, queue, ^(size_t instance) {
            unsafe = instance;
            if (unsafe != instance) {
                FORMAT_ALL_STORAGE();
                SEND_RESIGNATION_EMAIL();
                WATCH_THE_WORLD_BURN();
            }
        });
    

    您的 Swift 示例没有同样的问题,因为不修改值的块可能会捕获(并保留)对象,因此看不到其他块的修改。

    由闭包的创建者来处理内存管理后果,因此您不能创建在闭包中强制执行不保留循环的 API 合同,除非将某些内容标记为 @noescape 在这种情况下 Swift 赢了'不要做任何保留/释放或其他内存管理,因为该块不会超过当前堆栈帧。出于显而易见的原因,这排除了异步调度。

    如果您想提供解决此问题的 API 合约,您可以让类型采用协议 protocol Consumer: class { func consume(thing: Type) },然后在您的 API 中保留对 Consumer 实例的弱引用。

    另一种技术是接受实例函数的柯里化版本并弱捕获self

    protocol Protocol: class { }
    typealias FuncType = () -> Void
    var _responders = [FuncType]()
    
    func registerResponder<P: Protocol>(responder: P, usingHandler handler: (P) -> () -> Void) {
        _responders.append({ [weak responder] in
            guard let responder = responder else { return }
            handler(responder)()
        })
    }
    
    class Inst: Protocol {
        func myFunc() {
    
        }
    }
    let inst = Inst()
    registerResponder(inst, usingHandler: Inst.myFunc)
    

    【讨论】:

    • 我不确定这是否能回答问题
    • 从技术上讲,您是正确的,因为最初的问题是基于对实际情况的误解,并且包含线程和内存安全错误;没有弱方法参数之类的东西,我试图在我的回答中清楚地解释。我认为详细解释会有所帮助,以便每个人都可以学习。
    • @russbishop 如果“没有弱方法参数之类的东西”,那么为什么编译器允许在方法参数上使用__weak 关键字?通常,如果您将关键字放在错误的位置,Clang 会引发错误。
    猜你喜欢
    • 2016-01-03
    • 2020-01-11
    • 2014-08-02
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2015-07-12
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多