为什么目标 API 使用 Any 而不是 AnyObject?

Why do target APIs use Any instead of AnyObject?

提问人:Pyrettt Pyrettt 提问时间:6/20/2023 最后编辑:Pyrettt Pyrettt 更新时间:7/3/2023 访问量:70

问:

为什么 Objective-C API 喜欢或导入 Swift 时带有 的观察者类型 ,而不是 ?NotificationCenter.addObserverUIButton.addTargetAnyAnyObject

这有什么原因吗?是否有可能正确地将选择器指定给除类方法之外的东西?@objc

快速 选择器 调度

评论


答:

2赞 Joakim Danielson 6/21/2023 #1

原因之一是,您可以为对象使用协议类型,并且引用类型和值类型都可能符合该协议。

一个例子

protocol Observer {
    func observe()
}

class LookOut: Observer {
    @objc func observe() {}
}

这将起作用,因为参数的类型是Any

let observer: Observer = LookOut()

NotificationCenter.default.addObserver(observer,
                                       selector: #selector(LookOut.observe),
                                       name: .NSCalendarDayChanged,
                                       object: nil)

但是使用这种方法它会失败,因为协议不符合AnyObject

func fakeObserver(_ object: AnyObject, selector: Selector) {}

所以

fakeObserver(observer, selector: #selector(LookOut.observe)) 

生成错误

参数类型“any Observer”应为类或类约束类型的实例

当然,像下面这样的值类型在任何一种情况下都不起作用,因为该函数不能被注释@objc

struct LookIn: Observer {
    func observe() {}
}
4赞 Rob Napier 7/3/2023 #2

其原因在 Objective-C id 中解释为 Swift Any。在 Swift 2 中,正如你所建议的那样。 参数被桥接为 AnyObject。在 Swift 3 中,它们被更改为 Any:id

这一变化使 Objective-C API 在 Swift 中更加灵活,因为 Swift 定义的值类型可以传递给 Objective-C API 并提取为 Swift 类型,从而消除了对手动“框”类型的需要。这些好处也延伸到集合:Objective-C 集合类型 NSArray、NSDictionary 和 NSSet,以前只接受 AnyObject 的元素,现在可以保存 Any 类型的元素。

SE-0116 Import Objective-C id as Swift Any type(SE-0116 导入 Objective-C ID 为 Swift Any 类型)中详细介绍了此更改。

这使得 Swift/ObjC 互操作比 Swift 2 中的互操作简单得多,但它确实创建了一些特定的情况,例如操作目标,在这些情况下,创建在运行时会失败的代码可能更容易一些。然而,在实践中,对参数的限制是更重要的保护。目标和选择器不匹配一直是 ObjC 中的一个风险,在这里允许 Any 实际上并没有让情况变得更糟。您仍然需要正确使用,如果您引用非@objc方法,它会抛出错误。selector#selector

Joakim 也提出了一些很好的论点,尽管在这种情况下,我通常希望 Observer 需要 NSObjectProtocol。我怀疑这更多的是不值得为这些情况添加NS_REFINED_FOR_SWIFT宏。如果您发现由于缺乏 AnyObject 强制执行而发生实际错误,那么值得在 Swift 论坛上提出,作为可能的改进。

但是对于你的次要问题“是否有可能正确地将选择器指定给除类方法之外@objc东西”,答案是肯定的,如果你所说的“正确”是指“它会起作用”。

例如,你可以通过引用附加到 NSString 的方法将选择器发送到 String,即使它是一个结构:

let string: String = "This is a Swift string"

// This is an extension on **NSString**, not String.
extension NSString {
    @objc func strangeButTrue() { print("Yes, this will print") }
}

Timer.scheduledTimer(timeInterval: 1, 
                     target: string, 
                     selector: #selector(NSString.strangeButTrue),
                     userInfo: nil, 
                     repeats: false)

另请注意,这是编译时的便利。这也可以这样表示:#selector(NSString.strangeButTrue)

let nameOfSelectorDecidedAtRunTime = "strangeButTrue"
Timer.scheduledTimer(timeInterval: 1,
                     target: string,
                     selector: Selector(nameOfSelectorDecidedAtRunTime),
                     userInfo: nil,
                     repeats: false)

在运行时从字符串构造选择器在 ObjC 中很常见。我参与过的许多大型项目都至少有一个地方已经完成。能够以这种方式处理桥接对象确实使移植现有的 ObjC 代码变得更加容易,即使这不是你在 Swift 中真正应该做的事情。