You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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
   }
}

想请教两个问题:

  1. Swift String.enumerateSubstrings为什么要把body参数标记为@escaping?
  2. 用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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.26 06:32:43