Swift String.enumerateSubstrings的body参数为何标记@escaping?能否用withoutActuallyEscaping?
问题
我在实现NSTextSelectionDataSource的enumerateSubstrings方法时遇到了麻烦:我的底层存储是Swift String,原本想直接调用Swift String的同名方法,把返回的范围转换成NSTextRange后使用,但发现Swift String的这个方法虽然是同步执行的,但其body参数被标记为@escaping,导致编译报错——逃逸闭包捕获了非逃逸参数block。
我的实现代码示例如下:
func enumerateSubstrings( from location: any NSTextLocation, options: NSString.EnumerationOptions = [], using block: (String?, NSTextRange, NSTextRange?, UnsafeMutablePointer<ObjCBool>) -> Void ) { let string = // 获取底层存储字符串 string.enumerateSubstrings(in:.., options: options) { substring, substringRange, enclosingRange, shouldStop in // 转换substringRange和enclosingRange let substringTextRange = ... let enclosingTextRange = ... block(substring, substringTextRange, enclosingTextRange, ...) // 编译错误:逃逸闭包捕获非逃逸参数block } }
想请教两个问题:
- Swift String.enumerateSubstrings为什么要把body参数标记为
@escaping? - 用
withoutActuallyEscaping包裹来自NSTextSelectionDataSource.enumerateSubstrings的block是否安全?
回答
1. 为什么Swift String.enumerateSubstrings的body参数是@escaping?
这个标记主要源于两个原因:
- 桥接兼容Objective-C:NSString的同名方法没有逃逸/非逃逸的概念(Objective-C类型系统不区分这个),Swift在桥接该方法时,为了避免类型冲突,直接给闭包参数加上了
@escaping标记。 - 内部实现细节:Swift String的
enumerateSubstrings底层可能会把闭包传递给NSString的实现,或者内部某些需要逃逸上下文的逻辑——即使对外表现是同步执行的,只要闭包可能被方法返回后持有/传递,Swift类型系统就要求标记为@escaping。
2. 使用withoutActuallyEscaping是否安全?
在这个场景下完全安全,理由如下:
- Swift String的
enumerateSubstrings是同步执行的,传入的闭包会在方法返回前完全执行完毕,不会被持有到方法外部。 withoutActuallyEscaping的作用就是告诉编译器:“我保证这个非逃逸闭包只会在当前同步上下文里使用,不会真的被逃逸出去”——这里正好符合这个前提:你只是把非逃逸的block传递给了一个标记为逃逸但实际同步执行的方法,没有任何异步持有闭包的风险。
修复后的示例代码可以参考:
func enumerateSubstrings( from location: any NSTextLocation, options: NSString.EnumerationOptions = [], using block: (String?, NSTextRange, NSTextRange?, UnsafeMutablePointer<ObjCBool>) -> Void ) { let string = // 获取底层存储字符串 withoutActuallyEscaping(block) { escapedBlock in string.enumerateSubstrings(in: ..<string.endIndex, options: options) { substring, substringRange, enclosingRange, shouldStop in // 转换substringRange和enclosingRange为NSTextRange let substringTextRange = ... let enclosingTextRange = ... // 处理shouldStop的类型转换:Swift Bool <-> ObjCBool let objcShouldStop = UnsafeMutablePointer<ObjCBool>.allocate(capacity: 1) defer { objcShouldStop.deallocate() } objcShouldStop.pointee = shouldStop.pointee ? true : false escapedBlock(substring, substringTextRange, enclosingTextRange, objcShouldStop) // 把ObjCBool的结果同步回Swift的shouldStop shouldStop.pointee = objcShouldStop.pointee.boolValue } } }
内容的提问来源于stack exchange,提问作者georgemp
相关产品推荐
相关产品推荐

