为何@preconcurrency import无法消除Swift 6中闭包的Sendable错误?
问题场景
我有一个Objective-C框架FooModule,核心代码如下:
Objective-C头文件(Foo2.h)
#import <Foundation/Foundation.h> NS_ASSUME_NONNULL_BEGIN typedef void (^Reply)(id data); typedef void (^Handler)(Reply reply); @interface Foo2 : NSObject - (id)initWithQueue: (dispatch_queue_t)queue; - (void)setupHandler: (Handler)handler; @end NS_ASSUME_NONNULL_END
Objective-C实现文件(Foo2.m)
#import "Foo2.h" @implementation Foo2 { dispatch_queue_t _queue; } - (id)initWithQueue: (dispatch_queue_t)queue { if ((self = [super init])) { _queue = queue; } return self; } - (void)setupHandler:(Handler)handler { dispatch_async(_queue, ^{ handler(^(id data){ NSLog(@"data is %@", data); }); }); } @end
在Swift 6模式下,使用@preconcurrency import导入框架后编写代码:
@preconcurrency import FooModule func aGlobalFreeFunction() { let foo = Foo2(queue: .global()) foo.setupHandler { reply in Task { reply("foo") } } }
这段代码触发错误:
Passing closure as a 'sending' parameter risks causing data races between code in the current task and concurrent execution of the closure
但测试普通Objective-C对象时,@preconcurrency import却能正常消除并发安全警告:
普通Objective-C类
@interface FooNonSendable : NSObject @property int foo; @end @implementation FooNonSendable @end
Swift使用代码
@preconcurrency import FooModule func aGlobalFreeFunction() { let fooNonSendable = FooNonSendable() Task { Task { @MainActor in print(fooNonSendable) } } }
这段代码无任何错误或警告,说明@preconcurrency对普通对象有效,但对闭包无效。
原因分析
@preconcurrency的作用范围有限
@preconcurrency import的核心作用是告诉Swift:导入的Objective-C API已经由开发者保证了并发安全性,因此Swift会跳过对这些API本身的Sendable合规性检查,但这个豁免只针对非闭包类型或者闭包的传递过程,无法覆盖你在Swift代码中对API返回的闭包的后续并发使用。Objective-C闭包的默认Swift推断规则
Objective-C中定义的闭包在Swift中默认会被推断为@escaping且非Sendable。在你的代码中,reply闭包被捕获后在新的Task中调用,Swift 6的严格并发检查会检测到:这个闭包可能在与原上下文不同的执行上下文中被调用,存在潜在的数据竞争风险——而@preconcurrency import无法豁免这种用户代码层面的闭包并发调用检查。普通对象与闭包的差异
对于FooNonSendable这类普通Objective-C对象,@preconcurrency import直接豁免了Swift对其Sendable合规性的检查,因此跨Actor使用不会触发警告。但闭包属于执行代码块,Swift需要确保其捕获的变量、执行上下文的并发安全,这部分不在@preconcurrency的豁免范围内。
解决方案
方案1:在Objective-C端标记闭包为Sendable
最规范的方式是在Objective-C头文件中,用NS_SWIFT_SENDABLE标记闭包类型,明确告知Swift这些闭包是线程安全的:
#import <Foundation/Foundation.h> NS_ASSUME_NONNULL_BEGIN typedef void (^Reply)(id data) NS_SWIFT_SENDABLE; typedef void (^Handler)(Reply reply) NS_SWIFT_SENDABLE; @interface Foo2 : NSObject - (id)initWithQueue: (dispatch_queue_t)queue; - (void)setupHandler: (Handler)handler; @end NS_ASSUME_NONNULL_END
修改后,Swift会将这些闭包视为Sendable类型,并发安全检查会自动通过。
方案2:在Swift端显式豁免检查
如果你无法修改Objective-C框架的代码,可以在Swift代码中用@preconcurrency标记捕获reply的闭包,告诉Swift这个闭包的并发使用是安全的:
@preconcurrency import FooModule func aGlobalFreeFunction() { let foo = Foo2(queue: .global()) foo.setupHandler { @preconcurrency reply in Task { reply("foo") } } }
或者使用@unchecked Sendable标记闭包(仅当你能确认绝对安全时使用):
foo.setupHandler { reply in Task { @unchecked Sendable in reply("foo") } }
内容的提问来源于stack exchange,提问作者HL666

