【问题标题】:The strange behaviour of Swift's AnyObjectSwift 的 AnyObject 的奇怪行为
【发布时间】:2016-01-28 02:17:21
【问题描述】:

今天在玩 Swift 时,我遇到了一件奇怪的事情。这是我开发的单元测试,它显示了使用 Swift 的 AnyObject 时的一些意外行为。

class SwiftLanguageTests: XCTestCase {

    class TestClass {
        var name:String?
        var xyz:String?
    }

    func testAccessingPropertiesOfAnyObjectInstancesReturnsNils() {
        let instance = TestClass()
        instance.xyz = "xyz"
        instance.name = "name"

        let typeAnyObject = instance as AnyObject

        // Correct: Won't compile because 'xyz' is an unknown property in any class.
        XCTAssertEqual("xyz", typeAnyObject.xyz)

        // Unexpected: Will compile because 'name' is a property of NSException
        // Strange: But returns a nil when accessed.
        XCTAssertEqual("name", typeAnyObject.name)

    }
}

此代码是其他一些代码的简化,其中有一个 Swift 函数只能返回 AnyObject

正如预期的那样,在创建TestClass 的实例后,将其强制转换为AnyObject 并设置另一个变量,访问属性xyz 将无法编译,因为AnyObject 没有这样的属性。

但令人惊讶的是,编译器接受了一个名为 name 的属性,因为在 NSException 上有一个名为该名称的属性。看起来 Swift 很乐意接受任何属性名称,只要它存在于运行时的某个位置。

下一个意想不到的行为和开始这一切的原因是尝试访问name 属性返回一个nil。观察调试器中的各种变量,我可以看到 typeAnyObject 指向原始的 TestClass 实例,它的 name 属性的值为“名称”。

Swift 在访问typeAnyObject.name 时不会抛出错误,所以我希望它能够找到并返回“名称”。但相反,我得到了 nil。

如果有人能对这里发生的事情有所了解,我会很感兴趣?

我主要担心的是,我希望 Swift 在访问 AnyObject 上不存在的属性时抛出错误,或者找到并返回正确的值。目前两者都没有发生。

【问题讨论】:

    标签: swift runtime


    【解决方案1】:

    类似于Objective-C,你可以发送任意消息到id, 可以在AnyObject 的实例上调用任意属性和方法 在斯威夫特。但是,细节有所不同,并记录在 Interacting with Objective-C APIs 在“将 Swift 与 Cocoa 和 Objective-C 结合使用”一书中。

    Swift 包含一个代表某种对象的AnyObject 类型。这类似于 Objective-C 的 id 类型。 Swift 将 id 导入为 AnyObject,这允许您编写类型安全的 Swift 代码,同时保持无类型对象的灵活性。
    ...
    您可以调用任何 Objective-C 方法并访问 AnyObject 值的任何属性,而无需转换为更具体的类类型。这包括标有 @objc 属性的 Objective-C 兼容方法和属性。
    ...
    当您对 AnyObject 类型的值调用方法时,该方法调用的行为类似于隐式展开的可选项。您可以使用与协议中的可选方法相同的可选链接语法来可选地调用AnyObject 上的方法。

    这是一个例子:

    func tryToGetTimeInterval(obj : AnyObject) {
        let ti = obj.timeIntervalSinceReferenceDate // NSTimeInterval!
        if let theTi = ti {
            print(theTi)
        } else {
            print("does not respond to `timeIntervalSinceReferenceDate`")
        }
    }
    
    tryToGetTimeInterval(NSDate(timeIntervalSinceReferenceDate: 1234))
    // 1234.0
    
    tryToGetTimeInterval(NSString(string: "abc"))
    // does not respond to `timeIntervalSinceReferenceDate`
    

    obj.timeIntervalSinceReferenceDate 是一个隐式展开的可选项 nil 如果对象没有该属性。

    这里是一个检查和调用一个方法的例子:

    func tryToGetFirstCharacter(obj : AnyObject) {
        let fc = obj.characterAtIndex // ((Int) -> unichar)!
        if let theFc = fc {
            print(theFc(0))
        } else {
            print("does not respond to `characterAtIndex`")
        }
    }
    
    tryToGetFirstCharacter(NSDate(timeIntervalSinceReferenceDate: 1234))
    // does not respond to `characterAtIndex`
    
    tryToGetFirstCharacter(NSString(string: "abc"))
    // 97
    

    obj.characterAtIndex 是一个隐式展开的可选闭包。那个代码 可以使用可选链来简化:

    func tryToGetFirstCharacter(obj : AnyObject) {
        if let c = obj.characterAtIndex?(0) {
            print(c)
        } else {
            print("does not respond to `characterAtIndex`")
        }
    }
    

    在您的情况下,TestClass 没有任何 @objc 属性。

    let xyz = typeAnyObject.xyz // error: value of type 'AnyObject' has no member 'xyz'
    

    无法编译,因为xyz 属性对于编译器来说是未知的。

    let name = typeAnyObject.name // String!
    

    会编译因为——正如你所注意到的——NSException 有一个name 属性。 但是,该值是 nil,因为 TestClass 没有 Objective-C 兼容 name 方法。如上所述,您应该使用可选 绑定以安全地展开值(或针对 nil 进行测试)。

    如果你的类派生自NSObject

    class TestClass : NSObject {
        var name : String?
        var xyz : String?
    }
    

    然后

    let xyz = typeAnyObject.xyz // String?!
    

    编译。 (或者,使用@objc 标记类或属性。) 但是现在

    let name = typeAnyObject.name // error: Ambigous use of `name`
    

    不再编译。原因是TestClassNSException 有一个name 属性,但有不同的类型(String? vs String), 所以那个表达式的类型是模棱两可的。这种暧昧只能 通过(可选)将AnyObject 转换回TestClass 解决:

    if let name = (typeAnyObject as? TestClass)?.name {
        print(name)
    }
    

    结论:

    • 您可以在AnyObject 的实例上调用任何方法/属性,如果那样的话 方法/属性与 Objective-C 兼容。
    • 您必须针对nil 或 使用可选绑定来检查实例是否确实具有 方法/属性。
    • 如果多个类具有 (Objective-C) 兼容,则会出现歧义 名称相同但类型不同的方法。

    特别是因为最后一点,我会尽量避免这种情况 如果可能,可以选择机制,并且可以选择转换为已知类 (和上一个例子一样)。

    【讨论】:

    • 感谢 Martin 的详细解释。真的很有帮助。
    【解决方案2】:

    它与 NSException 无关!

    来自 Apple 文档:

    协议 AnyObject { ... }

    所有类都隐式遵循的协议。

    当用作具体类型时,所有已知的 @objc 方法和属性都可用,分别作为 AnyObject 的每个实例上的隐式解包可选方法和属性

    name 是@objc 属性,xyz 不是。

    试试这个:-)

    让 typeAnyObject = instance as Any

    @objc 类 TestClass: NSObject { 变量名称:字符串? var xyz:字符串? }

    让 instance = TestClass() instance.xyz = "xyz" instance.name = "name"

    让 typeAnyObject = 实例为 AnyObject

    typeAnyObject.name // 现在不会编译

    【讨论】:

      猜你喜欢
      • 2019-01-23
      • 2019-11-30
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2016-05-25
      • 1970-01-01
      • 2015-12-22
      相关资源
      最近更新 更多