swift - Swift 的 AnyObject 的奇怪行为

标签 swift runtime

今天在使用 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)

    }
}

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

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

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

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

访问 typeAnyObject.name 时,Swift 不会抛出错误,因此我希望它能够找到并返回“name”。但我却得到了零。

如果有人能解释一下这里发生的事情,我会很感兴趣?

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

最佳答案

与 Objective-C 类似,您可以向 id 发送任意消息, 可以在 AnyObject 的实例上调用任意属性和方法 在 swift .但是细节有所不同,并且记录在 Interacting with Objective-C APIs 在《将 Swift 与 Cocoa 和 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 方法。如上所述,您应该使用可选的 绑定(bind)以安全地解开该值(或针对 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?String), 所以该表达式的类型是不明确的。这种暧昧只能是 通过(可选)将 AnyObject 强制转换回 TestClass 来解决:

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

结论:

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

特别是由于最后一点,我会尽力避免这种情况 如果可能的话,机制,并且可以选择转换为已知类 (如上一个示例)。

关于swift - Swift 的 AnyObject 的奇怪行为,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/33388830/

相关文章:

ios - 如何使用 Collection View Swift 3 创建 'sticky' 水平滚动效果

swift - 在 Firebase 实时数据库中实现用户名 (swift 4)

objective-c - 没有传输安全的解决方案?

iphone - 为 iPad 和 iPhone 运行时创建不同的@properties

linux - 为什么运行时 undefined symbol 会通过将/usr/lib 添加到ld.so.conf 来修复?

c - 尝试为 hermite 多项式暴力求根

nullpointerexception - java修改kotlin类内容为null

ios - 加快像素迭代

c# - .NET 运行时如何移动内存?

ios - 使用 swift 检查 Firebase 中的用户类型