Swift 的 AnyObject 的奇怪行为

The strange behaviour of Swift's AnyObject

今天在和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 函数只能 return 一个 AnyObject

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

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

下一个意想不到的行为和开始这一切的事情是试图访问 name 属性 returns 为零。查看调试器中的各种变量,我可以看到 typeAnyObject 指向原始 TestClass 实例,并且 name 属性 的值为 "name"。

Swift 在访问 typeAnyObject.name 时不会抛出错误,所以我希望它能找到 return "name"。但是我得到的却是零。

如果有人能阐明这里发生的事情,我会很感兴趣?

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

它与 NSException 无关!

来自 Apple 文档:

protocol AnyObject { ... }

所有 类 隐式遵守的协议。

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

name 是@objc 属性,xyz 不是。

试试这个:-)

let typeAnyObject = instance as Any

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

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

let typeAnyObject = instance as AnyObject

typeAnyObject.name // will not compile now

类似于 Objective-C,您可以向 id 发送任意消息, 可以在 AnyObject 的实例上调用任意属性和方法 在 Swift。但是细节有所不同,它记录在 Interacting with Objective-C APIs 在 "Using Swift with Cocoa and Objective-C" 书中。

Swift includes an AnyObject type that represents some kind of object. This is similar to Objective-C’s id type. Swift imports id as AnyObject, which allows you to write type-safe Swift code while maintaining the flexibility of an untyped object.
...
You can call any Objective-C method and access any property on an AnyObject value without casting to a more specific class type. This includes Objective-C compatible methods and properties marked with the @objc attribute.
...
When you call a method on a value of AnyObject type, that method call behaves like an implicitly unwrapped optional. You can use the same optional chaining syntax you would use for optional methods in protocols to optionally invoke a method on 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 进行测试)。

如果您的 class 来自 NSObject

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

然后

let xyz = typeAnyObject.xyz // String?!

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

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

不再编译。原因是 TestClassNSExceptionname 属性,但类型不同(String? vs String), 所以该表达式的类型是不明确的。这种暧昧只能 通过(可选)将 AnyObject 转换回 TestClass:

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

结论:

  • 您可以在 AnyObject 的实例上调用任何 method/property 如果 method/property 与 Objective-C 兼容。
  • 你必须针对 nil 或测试隐式展开的可选 使用可选绑定来检查实例是否确实具有 method/property.
  • 如果多个 class 具有 (Objective-C) 兼容,则会出现歧义 名称相同但类型不同的方法。

特别是因为最后一点,我会尽量避免这种情况 如果可能,机制,并可选择转换为已知的 class (如上一个示例所示)。