Swift中有没有NSInvocation的替代方案?

8

我试图调用一个带有多个参数(参数数量可以确定)的选择器。但是,选择器在编译时是未知的(实际上是通过NSSelectorFromString生成的)。

在Objective-C中,我可以创建一个调用并为其设置参数,然后调用它。但在Swift中不可用。有没有解决这个问题的办法?像这样:

let obj = SomeClass()
let selector = NSSelectorFromString("arg1:arg2:arg3:") //selector, arguments known only at runtime
//invoke selector

1
这种代码的动态性通常是糟糕的架构(无意冒犯)的结果。虽然有一些非常特定的例外情况,但在95%的情况下,您不需要使用NSInvocation,甚至不应该在Objective-C中使用它。一个可能的解决方案是在字典中使用命名闭包,但即使如此也有点“代码异味”。 - Sulthan
1
@Sulthan 我不会这么说,为什么是糟糕的架构? - user1094081
@ Sulthan:没问题,但您可以通过将结果转换为所需的类型来添加必要的安全性(或者您的意思不同?) - user1094081
1
@3000 不行。问题在于方法的名称是一个字符串,因此编译器无法检查是否存在这样的方法。使用 #selector 语法部分解决了选择器的问题,但是对于 NSInvocation 没有这样的语法。闭包更加安全。由于 Obj-C 中方法的名称会影响内存管理等其他方面,因此还有一些相关方面需要考虑。总之,这是旧的做法,我们现在有更好、更安全的替代方案。 - Sulthan
@Sulthan:好的,谢谢您的友善回复。 - user1094081
显示剩余5条评论
2个回答

10

Swift 3.1

NSInvocation可以动态使用,但只是为了锻炼而已,绝不适用于严肃的应用程序。有更好的替代方案

import Foundation

class Test: NSObject {
    @objc var name: String? {
        didSet {
            NSLog("didSetCalled")
        }
    }

    func invocationTest() {
        // This is the selector we want our Invocation to send
        let namePropertySetterSelector = #selector(setter:name)
        
        // Look up a bunch of methods/impls on NSInvocation
        let nsInvocationClass: AnyClass = NSClassFromString("NSInvocation")!
        
        // Look up the "invocationWithMethodSignature:" method
        let nsInvocationInitializer = unsafeBitCast(
            method_getImplementation(
                class_getClassMethod(nsInvocationClass, NSSelectorFromString("invocationWithMethodSignature:"))!
            ),
            to: (@convention(c) (AnyClass?, Selector, Any?) -> Any).self
        )
        
        // Look up the "setSelector:" method
        let nsInvocationSetSelector = unsafeBitCast(
            class_getMethodImplementation(nsInvocationClass, NSSelectorFromString("setSelector:")),
            to:(@convention(c) (Any, Selector, Selector) -> Void).self
        )
        
        // Look up the "setArgument:atIndex:" method
        let nsInvocationSetArgAtIndex = unsafeBitCast(
            class_getMethodImplementation(nsInvocationClass, NSSelectorFromString("setArgument:atIndex:")),
            to:(@convention(c)(Any, Selector, OpaquePointer, NSInteger) -> Void).self
        )
        
        // Get the method signiture for our the setter method for our "name" property.
        let methodSignatureForSelector = NSSelectorFromString("methodSignatureForSelector:")
        let getMethodSigniatureForSelector = unsafeBitCast(
            method(for: methodSignatureForSelector)!,
            to: (@convention(c) (Any?, Selector, Selector) -> Any).self
        )
        
        // ObjC:
        // 1. NSMethodSignature *mySignature = [self methodSignatureForSelector: @selector(setName:)];
        // 2. NSInvocation *myInvocation = [NSInvocation invocationWithMethodSignature: mySignature];
        // 3. [myInvocation setSelector: @selector(setName:)];
        // 4. [myInvocation setArgument: @"new name", atIndex: 2];
        // 5. [myInvocation invokeWithTarget: self];
        
        // 1.
        let namyPropertyMethodSigniature = getMethodSigniatureForSelector(self, methodSignatureForSelector, namePropertySetterSelector)

        // 2.
        let invocation = nsInvocationInitializer(
            nsInvocationClass,
            NSSelectorFromString("invocationWithMethodSignature:"),
            namyPropertyMethodSigniature
        ) as! NSObject // Really it's an NSInvocation, but that can't be expressed in Swift.
        
        // 3.
        nsInvocationSetSelector(
            invocation,
            NSSelectorFromString("setSelector:"),
            namePropertySetterSelector
        )
        
        var localName = "New name" as NSString
        
        // 4.
        withUnsafePointer(to: &localName) { stringPointer in
            nsInvocationSetArgAtIndex(
                invocation,
                NSSelectorFromString("setArgument:atIndex:"),
                OpaquePointer(stringPointer),
                2
            )
        }
        
        // 5.
        invocation.perform(NSSelectorFromString("invokeWithTarget:"), with: self)
    }
}

let object = Test()
object.invocationTest()

5

很抱歉,Swift中没有这种方法。

但是,您可以使用Objective-C类来管理动态调用。在那里可以使用NSInvocation


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接