代码之家  ›  专栏  ›  技术社区  ›  Airsource Ltd

Swift to Objective-C调用和尾随闭包调用错误的方法

  •  0
  • Airsource Ltd  · 技术社区  · 5 年前

    给定以下Objective-C方法

    @import Foundation;
    
    NS_ASSUME_NONNULL_BEGIN
    
    @interface TestClass : NSObject
    // Variant 1. Notice that arg1 is nullable, and that all closure arguments are nullable.
    + (void)test:(NSString *_Nullable)arg1
                     action:(nullable void (^)(void))action;
    
    // Variant 2. Notice that arg2 is non null, there is an additional, nullable, closure argument
    + (void)test:(NSString *)title
          action:(nullable void (^)(void))action
         action2:(nullable void (^)(void))action2;
    
    @end
    
    NS_ASSUME_NONNULL_END
    

    我发现,当我试图用完全指定的参数从Swift调用第一个方法时,当我使用尾随闭包时,它实际上调用了第二个变量

    // Calls first variant (This is an erroneous claim, hence Matt's answer)
    TestClass.test("arg1", action: {})
    
    // Calls second variant
    TestClass.test("arg2") {}
    

    我希望在这两种情况下都能调用变体1。我不清楚我是否做错了什么。我似乎也忽略了这样一个事实,即Swift在调用Obj-C方法时可以提供生成参数,而且我很难找到相关的文档。

    如果我用等价的

    
    class TestClass {
        class func test(_ arg1: String?, action: (() -> ())? = nil) {
            
        }
    
        class func test(_ arg1: String!, action: (() -> ())? = nil, action2: (() -> ())? = nil) {
            // Should not get here
            assert(false)
        }
    
    }
    

    然后我得到一个编译器警告,在两个调用中都使用了不明确的“test”。

    在Xcode 12.3和12.4上测试。

    0 回复  |  直到 5 年前
        1
  •  2
  •   matt    5 年前

    显然,我希望在这两种情况下都能调用变体1。

    实际上,我发现在这两种情况下都调用了变体2。

    首先为Objective-C接口生成Swift接口。你会发现:

    open class func test(_ arg1: String?, action: (() -> Void)? = nil)
    open class func test(_ title: String, action: (() -> Void)?, action2: (() -> Void)? = nil)
    

    我们现在已经从故事中删除了Objective-C组件,可以在测试中使用以下方法:

    typealias VoidVoid = () -> Void
    func test(_ arg1: String?, action: VoidVoid? = nil) { print("1") }
    func test(_ title: String, action: VoidVoid?, action2: VoidVoid? = nil) { print("2") }
    func f() {
        test("arg1", action: {})
        test("arg2") {}
    }
    

    如果我们打电话 f() ,控制台会打印两次“2”。我没有得到任何编译错误,但第一个变量似乎是不可访问的,除非您省略 二者都 函数参数。

        2
  •  2
  •   Rob Md Fahim Faez Abir    5 年前

    在matts answer中,他分享了自己的经验,即编译器将解决对该方法的第二个Objective-C变体(具有 action2 参数)。我们应该注意到,这种行为是独一无二的,因为这两种方法对第一个参数具有不同的可空性,在第一个变量中是可空的,而在第二个变量中不是可空的。

    切换两个方法中第一个参数的可空性,行为就会发生变化,偏向于第一个变量。同样,如果两种格式副本都使用相同的可空性,那么将再次使用第一种变体。考虑:

    NS_ASSUME_NONNULL_BEGIN
    
    @interface TestClass : NSObject
    
    + (void)test:(NSString * _Nullable)title
          action:(nullable void (^)(void))action;
    
    + (void)test:(NSString * _Nullable)title
          action:(nullable void (^)(void))action
         action2:(nullable void (^)(void))action2;
    
    @end
    
    NS_ASSUME_NONNULL_END
    

    这转化为以下Swift界面:

    open class TestClass : NSObject {
        open class func test(_ title: String?, action: (() -> Void)? = nil)    
        open class func test(_ title: String?, action: (() -> Void)?, action2: (() -> Void)? = nil)
    }
    

    现在,两个Swift调用都将调用第一个变量,而不是第二个变量:

    TestClass.test("foo", action: {}) // variant 1
    TestClass.test("foo") {}          // variant 1
    

    在您的示例中(第一个变量使第一个参数为空,但第二个变量不为空),它正在解析为第二个变量,因为我们传递了一个非可选字符串。

    考虑到(希望如此)第一个变量只是第二个变量的Objective-C便利方法,调用哪一个可能无关紧要,但故事的寓意是,对适当的Objective-C方法的解决取决于非常微妙的考虑,这些考虑可能不是一目了然的。


    冒着过早优化的风险,如果你真的关心确保Swift总是调用第二个Objective-C变量,而不管这两个变量中第一个参数的可空性/可选性,你可以通过 NS_REFINED_FOR_SWIFT 如中所述 Improving Objective-C API Declarations for Swift .例如:

    NS_ASSUME_NONNULL_BEGIN
    
    @interface TestClass : NSObject
    
    + (void)test:(NSString * _Nullable)title
          action:(nullable void (^)(void))action NS_REFINED_FOR_SWIFT;
    
    + (void)test:(NSString * _Nullable)title
          action:(nullable void (^)(void))action
         action2:(nullable void (^)(void))action2 NS_REFINED_FOR_SWIFT;
    
    @end
    
    NS_ASSUME_NONNULL_END
    

    然后手动声明自己的显式Swift接口:

    extension TestClass {
        @inlinable
        static func test(_ title: String? = nil, action: (() -> Void)? = nil) {
            __test(title, action: action, action2: nil)
        }
    
        @inlinable
        static func test(_ title: String? = nil, action: (() -> Void)?, action2: (() -> Void)? = nil) {
            __test(title, action: action, action2: action2)
        }
    }
    

    这将始终调用Objective-C方法的第二种变体。这消除了方法解析行为,这些行为在其他情况下可能并不明显,从而使您的意图变得明确。